DomainResolver.cs 9.9 KB

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