DomainResolver.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. using DNS.Client;
  2. using DNS.Protocol;
  3. using FastGithub.Configuration;
  4. using Microsoft.Extensions.Caching.Memory;
  5. using Microsoft.Extensions.Logging;
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. using System.Net;
  11. using System.Net.Sockets;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. namespace FastGithub.DomainResolve
  15. {
  16. /// <summary>
  17. /// 域名解析器
  18. /// </summary>
  19. sealed class DomainResolver : IDomainResolver
  20. {
  21. private readonly IMemoryCache memoryCache;
  22. private readonly FastGithubConfig fastGithubConfig;
  23. private readonly DnscryptProxy dnscryptProxy;
  24. private readonly ILogger<DomainResolver> logger;
  25. private readonly TimeSpan lookupTimeout = TimeSpan.FromSeconds(2d);
  26. private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(2d);
  27. private readonly TimeSpan dnscryptExpiration = TimeSpan.FromMinutes(5d);
  28. private readonly TimeSpan fallbackExpiration = TimeSpan.FromMinutes(1d);
  29. private readonly TimeSpan loopbackExpiration = TimeSpan.FromSeconds(5d);
  30. private readonly ConcurrentDictionary<DnsEndPoint, SemaphoreSlim> semaphoreSlims = new();
  31. /// <summary>
  32. /// 域名解析器
  33. /// </summary>
  34. /// <param name="memoryCache"></param>
  35. /// <param name="fastGithubConfig"></param>
  36. /// <param name="dnscryptProxy"></param>
  37. /// <param name="logger"></param>
  38. public DomainResolver(
  39. IMemoryCache memoryCache,
  40. FastGithubConfig fastGithubConfig,
  41. DnscryptProxy dnscryptProxy,
  42. ILogger<DomainResolver> logger)
  43. {
  44. this.memoryCache = memoryCache;
  45. this.fastGithubConfig = fastGithubConfig;
  46. this.dnscryptProxy = dnscryptProxy;
  47. this.logger = logger;
  48. }
  49. /// <summary>
  50. /// 解析域名
  51. /// </summary>
  52. /// <param name="endPoint"></param>
  53. /// <param name="cancellationToken"></param>
  54. /// <returns></returns>
  55. public async Task<IPAddress> ResolveAsync(DnsEndPoint endPoint, CancellationToken cancellationToken = default)
  56. {
  57. var semaphore = this.semaphoreSlims.GetOrAdd(endPoint, _ => new SemaphoreSlim(1, 1));
  58. try
  59. {
  60. await semaphore.WaitAsync(cancellationToken);
  61. return await this.LookupAsync(endPoint, cancellationToken);
  62. }
  63. finally
  64. {
  65. semaphore.Release();
  66. }
  67. }
  68. /// <summary>
  69. /// 查找ip
  70. /// </summary>
  71. /// <param name="target"></param>
  72. /// <param name="cancellationToken"></param>
  73. /// <returns></returns>
  74. private async Task<IPAddress> LookupAsync(DnsEndPoint target, CancellationToken cancellationToken)
  75. {
  76. if (this.memoryCache.TryGetValue<IPAddress>(target, out var address))
  77. {
  78. return address;
  79. }
  80. var expiration = this.dnscryptExpiration;
  81. address = await this.LookupCoreAsync(this.dnscryptProxy.EndPoint, target, cancellationToken);
  82. if (address == null)
  83. {
  84. expiration = this.fallbackExpiration;
  85. address = await this.FallbackLookupAsync(target, cancellationToken);
  86. }
  87. if (address == null)
  88. {
  89. throw new FastGithubException($"当前解析不到{target.Host}可用的ip,请刷新重试");
  90. }
  91. // 往往是被污染的dns
  92. if (address.Equals(IPAddress.Loopback) == true)
  93. {
  94. expiration = this.loopbackExpiration;
  95. }
  96. this.logger.LogInformation($"[{target.Host}->{address}]");
  97. this.memoryCache.Set(target, address, expiration);
  98. return address;
  99. }
  100. /// <summary>
  101. /// 回退查找ip
  102. /// </summary>
  103. /// <param name="target"></param>
  104. /// <param name="cancellationToken"></param>
  105. /// <returns></returns>
  106. private async Task<IPAddress?> FallbackLookupAsync(DnsEndPoint target, CancellationToken cancellationToken)
  107. {
  108. foreach (var dns in this.fastGithubConfig.FallbackDns)
  109. {
  110. var address = await this.LookupCoreAsync(dns, target, cancellationToken);
  111. if (address != null)
  112. {
  113. return address;
  114. }
  115. }
  116. return default;
  117. }
  118. /// <summary>
  119. /// 查找ip
  120. /// </summary>
  121. /// <param name="dns"></param>
  122. /// <param name="target"></param>
  123. /// <param name="cancellationToken"></param>
  124. /// <returns></returns>
  125. private async Task<IPAddress?> LookupCoreAsync(IPEndPoint dns, DnsEndPoint target, CancellationToken cancellationToken)
  126. {
  127. try
  128. {
  129. var dnsClient = new DnsClient(dns);
  130. using var timeoutTokenSource = new CancellationTokenSource(this.lookupTimeout);
  131. using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
  132. var addresses = await dnsClient.Lookup(target.Host, RecordType.A, linkedTokenSource.Token);
  133. return await this.FindFastValueAsync(addresses, target.Port, cancellationToken);
  134. }
  135. catch (Exception ex)
  136. {
  137. this.logger.LogWarning($"dns({dns})无法解析{target.Host}:{ex.Message}");
  138. return default;
  139. }
  140. }
  141. /// <summary>
  142. /// 获取最快的ip
  143. /// </summary>
  144. /// <param name="addresses"></param>
  145. /// <param name="port"></param>
  146. /// <param name="cancellationToken"></param>
  147. /// <returns></returns>
  148. private async Task<IPAddress?> FindFastValueAsync(IEnumerable<IPAddress> addresses, int port, CancellationToken cancellationToken)
  149. {
  150. if (addresses.Any() == false)
  151. {
  152. return default;
  153. }
  154. if (port <= 0)
  155. {
  156. return addresses.FirstOrDefault();
  157. }
  158. var tasks = addresses.Select(address => this.IsAvailableAsync(address, port, cancellationToken));
  159. var fastTask = await Task.WhenAny(tasks);
  160. return await fastTask;
  161. }
  162. /// <summary>
  163. /// 验证远程节点是否可连接
  164. /// </summary>
  165. /// <param name="address"></param>
  166. /// <param name="port"></param>
  167. /// <param name="cancellationToken"></param>
  168. /// <returns></returns>
  169. private async Task<IPAddress?> IsAvailableAsync(IPAddress address, int port, CancellationToken cancellationToken)
  170. {
  171. try
  172. {
  173. using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
  174. using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
  175. using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
  176. await socket.ConnectAsync(address, port, linkedTokenSource.Token);
  177. return address;
  178. }
  179. catch (OperationCanceledException)
  180. {
  181. return default;
  182. }
  183. catch (Exception)
  184. {
  185. await Task.Delay(this.connectTimeout, cancellationToken);
  186. return default;
  187. }
  188. }
  189. }
  190. }