DomainResolver.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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 Microsoft.Extensions.Options;
  7. using System;
  8. using System.Collections.Concurrent;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using System.Net;
  12. using System.Net.Sockets;
  13. using System.Threading;
  14. using System.Threading.Tasks;
  15. namespace FastGithub.DomainResolve
  16. {
  17. /// <summary>
  18. /// 域名解析器
  19. /// </summary>
  20. sealed class DomainResolver : IDomainResolver
  21. {
  22. private readonly IMemoryCache domainResolveCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
  23. private readonly IMemoryCache disableIPAddressCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
  24. private readonly FastGithubConfig fastGithubConfig;
  25. private readonly DnscryptProxy dnscryptProxy;
  26. private readonly ILogger<DomainResolver> logger;
  27. private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(5d);
  28. private readonly TimeSpan disableIPExpiration = TimeSpan.FromMinutes(2d);
  29. private readonly TimeSpan dnscryptExpiration = TimeSpan.FromMinutes(10d);
  30. private readonly TimeSpan fallbackExpiration = TimeSpan.FromMinutes(2d);
  31. private readonly TimeSpan loopbackExpiration = TimeSpan.FromSeconds(5d);
  32. private readonly ConcurrentDictionary<DnsEndPoint, SemaphoreSlim> semaphoreSlims = new();
  33. /// <summary>
  34. /// 域名解析器
  35. /// </summary>
  36. /// <param name="fastGithubConfig"></param>
  37. /// <param name="dnscryptProxy"></param>
  38. /// <param name="logger"></param>
  39. public DomainResolver(
  40. FastGithubConfig fastGithubConfig,
  41. DnscryptProxy dnscryptProxy,
  42. ILogger<DomainResolver> logger)
  43. {
  44. this.fastGithubConfig = fastGithubConfig;
  45. this.dnscryptProxy = dnscryptProxy;
  46. this.logger = logger;
  47. }
  48. /// <summary>
  49. /// 设置ip不可用
  50. /// </summary>
  51. /// <param name="address">ip</param>
  52. public void SetDisabled(IPAddress address)
  53. {
  54. this.disableIPAddressCache.Set(address, address, this.disableIPExpiration);
  55. }
  56. /// <summary>
  57. /// 刷新域名解析结果
  58. /// </summary>
  59. /// <param name="domain">域名</param>
  60. public void FlushDomain(DnsEndPoint domain)
  61. {
  62. this.domainResolveCache.Remove(domain);
  63. }
  64. /// <summary>
  65. /// 解析域名
  66. /// </summary>
  67. /// <param name="domain"></param>
  68. /// <param name="cancellationToken"></param>
  69. /// <exception cref="OperationCanceledException"></exception>
  70. /// <exception cref="FastGithubException"></exception>
  71. /// <returns></returns>
  72. public async Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default)
  73. {
  74. var semaphore = this.semaphoreSlims.GetOrAdd(domain, _ => new SemaphoreSlim(1, 1));
  75. try
  76. {
  77. await semaphore.WaitAsync();
  78. return await this.ResolveCoreAsync(domain, cancellationToken);
  79. }
  80. finally
  81. {
  82. semaphore.Release();
  83. }
  84. }
  85. /// <summary>
  86. /// 解析域名
  87. /// </summary>
  88. /// <param name="domain"></param>
  89. /// <param name="cancellationToken"></param>
  90. /// <exception cref="OperationCanceledException"></exception>
  91. /// <exception cref="FastGithubException"></exception>
  92. /// <returns></returns>
  93. private async Task<IPAddress> ResolveCoreAsync(DnsEndPoint domain, CancellationToken cancellationToken)
  94. {
  95. if (this.domainResolveCache.TryGetValue<IPAddress>(domain, out var address) && address != null)
  96. {
  97. return address;
  98. }
  99. var expiration = this.dnscryptExpiration;
  100. address = await this.LookupByDnscryptAsync(domain, cancellationToken);
  101. if (address == null)
  102. {
  103. expiration = this.fallbackExpiration;
  104. address = await this.LookupByFallbackAsync(domain, cancellationToken);
  105. }
  106. if (address == null)
  107. {
  108. throw new FastGithubException($"当前解析不到{domain.Host}可用的ip,请刷新重试");
  109. }
  110. // 往往是被污染的dns
  111. if (address.Equals(IPAddress.Loopback) == true)
  112. {
  113. expiration = this.loopbackExpiration;
  114. }
  115. this.domainResolveCache.Set(domain, address, expiration);
  116. return address;
  117. }
  118. /// <summary>
  119. /// Dnscrypt查找ip
  120. /// </summary>
  121. /// <param name="domain"></param>
  122. /// <param name="cancellationToken"></param>
  123. /// <param name="maxTryCount"></param>
  124. /// <returns></returns>
  125. private async Task<IPAddress?> LookupByDnscryptAsync(DnsEndPoint domain, CancellationToken cancellationToken, int maxTryCount = 2)
  126. {
  127. if (this.dnscryptProxy.LocalEndPoint != null)
  128. {
  129. for (var i = 0; i < maxTryCount; i++)
  130. {
  131. var address = await this.LookupAsync(this.dnscryptProxy.LocalEndPoint, domain, cancellationToken);
  132. if (address != null)
  133. {
  134. return address;
  135. }
  136. }
  137. }
  138. return default;
  139. }
  140. /// <summary>
  141. /// 回退查找ip
  142. /// </summary>
  143. /// <param name="domain"></param>
  144. /// <param name="cancellationToken"></param>
  145. /// <exception cref="OperationCanceledException"></exception>
  146. /// <returns></returns>
  147. private async Task<IPAddress?> LookupByFallbackAsync(DnsEndPoint domain, CancellationToken cancellationToken)
  148. {
  149. foreach (var dns in this.fastGithubConfig.FallbackDns)
  150. {
  151. var address = await this.LookupAsync(dns, domain, cancellationToken);
  152. if (address != null)
  153. {
  154. return address;
  155. }
  156. }
  157. return default;
  158. }
  159. /// <summary>
  160. /// 查找最快的可用ip
  161. /// </summary>
  162. /// <param name="dns"></param>
  163. /// <param name="domain"></param>
  164. /// <param name="cancellationToken"></param>
  165. /// <exception cref="OperationCanceledException"></exception>
  166. /// <returns></returns>
  167. private async Task<IPAddress?> LookupAsync(IPEndPoint dns, DnsEndPoint domain, CancellationToken cancellationToken)
  168. {
  169. try
  170. {
  171. var dnsClient = new DnsClient(dns);
  172. var addresses = await dnsClient.Lookup(domain.Host, RecordType.A, cancellationToken);
  173. addresses = addresses.Where(address => this.disableIPAddressCache.TryGetValue(address, out _) == false).ToList();
  174. var address = await this.FindFastValueAsync(addresses, domain.Port, cancellationToken);
  175. if (address == null)
  176. {
  177. this.logger.LogWarning($"dns({dns})解析不到{domain.Host}可用的ip解析");
  178. }
  179. else
  180. {
  181. this.logger.LogInformation($"dns({dns}): {domain.Host}->{address}");
  182. }
  183. return address;
  184. }
  185. catch (Exception ex)
  186. {
  187. cancellationToken.ThrowIfCancellationRequested();
  188. this.logger.LogWarning($"dns({dns})无法解析{domain.Host}:{ex.Message}");
  189. return default;
  190. }
  191. }
  192. /// <summary>
  193. /// 获取最快的ip
  194. /// </summary>
  195. /// <param name="addresses"></param>
  196. /// <param name="port"></param>
  197. /// <param name="cancellationToken"></param>
  198. /// <exception cref="OperationCanceledException"></exception>
  199. /// <returns></returns>
  200. private async Task<IPAddress?> FindFastValueAsync(IEnumerable<IPAddress> addresses, int port, CancellationToken cancellationToken)
  201. {
  202. if (addresses.Any() == false)
  203. {
  204. return default;
  205. }
  206. if (port <= 0)
  207. {
  208. return addresses.FirstOrDefault();
  209. }
  210. var tasks = addresses.Select(address => this.IsAvailableAsync(address, port, cancellationToken));
  211. var fastTask = await Task.WhenAny(tasks);
  212. return await fastTask;
  213. }
  214. /// <summary>
  215. /// 验证远程节点是否可连接
  216. /// </summary>
  217. /// <param name="address"></param>
  218. /// <param name="port"></param>
  219. /// <param name="cancellationToken"></param>
  220. /// <exception cref="OperationCanceledException"></exception>
  221. /// <returns></returns>
  222. private async Task<IPAddress?> IsAvailableAsync(IPAddress address, int port, CancellationToken cancellationToken)
  223. {
  224. try
  225. {
  226. using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
  227. using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
  228. using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
  229. await socket.ConnectAsync(address, port, linkedTokenSource.Token);
  230. return address;
  231. }
  232. catch (OperationCanceledException)
  233. {
  234. cancellationToken.ThrowIfCancellationRequested();
  235. this.SetDisabled(address);
  236. return default;
  237. }
  238. catch (Exception)
  239. {
  240. this.SetDisabled(address);
  241. await Task.Delay(this.connectTimeout, cancellationToken);
  242. return default;
  243. }
  244. }
  245. }
  246. }