using DNS.Client; using DNS.Protocol; using FastGithub.Configuration; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; namespace FastGithub.DomainResolve { /// /// 域名解析器 /// sealed class DomainResolver : IDomainResolver { private readonly IMemoryCache domainResolveCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IMemoryCache disableIPAddressCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly FastGithubConfig fastGithubConfig; private readonly DnscryptProxy dnscryptProxy; private readonly ILogger logger; private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(5d); private readonly TimeSpan disableIPExpiration = TimeSpan.FromMinutes(2d); private readonly TimeSpan dnscryptExpiration = TimeSpan.FromMinutes(10d); private readonly TimeSpan fallbackExpiration = TimeSpan.FromMinutes(2d); private readonly TimeSpan loopbackExpiration = TimeSpan.FromSeconds(5d); private readonly ConcurrentDictionary semaphoreSlims = new(); /// /// 域名解析器 /// /// /// /// public DomainResolver( FastGithubConfig fastGithubConfig, DnscryptProxy dnscryptProxy, ILogger logger) { this.fastGithubConfig = fastGithubConfig; this.dnscryptProxy = dnscryptProxy; this.logger = logger; } /// /// 设置ip不可用 /// /// ip public void SetDisabled(IPAddress address) { this.disableIPAddressCache.Set(address, address, this.disableIPExpiration); } /// /// 刷新域名解析结果 /// /// 域名 public void FlushDomain(DnsEndPoint domain) { this.domainResolveCache.Remove(domain); } /// /// 解析域名 /// /// /// /// /// /// public async Task ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default) { var semaphore = this.semaphoreSlims.GetOrAdd(domain, _ => new SemaphoreSlim(1, 1)); try { await semaphore.WaitAsync(); return await this.ResolveCoreAsync(domain, cancellationToken); } finally { semaphore.Release(); } } /// /// 解析域名 /// /// /// /// /// /// private async Task ResolveCoreAsync(DnsEndPoint domain, CancellationToken cancellationToken) { if (this.domainResolveCache.TryGetValue(domain, out var address) && address != null) { return address; } var expiration = this.dnscryptExpiration; address = await this.LookupByDnscryptAsync(domain, cancellationToken); if (address == null) { expiration = this.fallbackExpiration; address = await this.LookupByFallbackAsync(domain, cancellationToken); } if (address == null) { throw new FastGithubException($"当前解析不到{domain.Host}可用的ip,请刷新重试"); } // 往往是被污染的dns if (address.Equals(IPAddress.Loopback) == true) { expiration = this.loopbackExpiration; } this.domainResolveCache.Set(domain, address, expiration); return address; } /// /// Dnscrypt查找ip /// /// /// /// /// private async Task LookupByDnscryptAsync(DnsEndPoint domain, CancellationToken cancellationToken, int maxTryCount = 2) { if (this.dnscryptProxy.LocalEndPoint != null) { for (var i = 0; i < maxTryCount; i++) { var address = await this.LookupAsync(this.dnscryptProxy.LocalEndPoint, domain, cancellationToken); if (address != null) { return address; } } } return default; } /// /// 回退查找ip /// /// /// /// /// private async Task LookupByFallbackAsync(DnsEndPoint domain, CancellationToken cancellationToken) { foreach (var dns in this.fastGithubConfig.FallbackDns) { var address = await this.LookupAsync(dns, domain, cancellationToken); if (address != null) { return address; } } return default; } /// /// 查找最快的可用ip /// /// /// /// /// /// private async Task LookupAsync(IPEndPoint dns, DnsEndPoint domain, CancellationToken cancellationToken) { try { var dnsClient = new DnsClient(dns); var addresses = await dnsClient.Lookup(domain.Host, RecordType.A, cancellationToken); addresses = addresses.Where(address => this.disableIPAddressCache.TryGetValue(address, out _) == false).ToList(); var address = await this.FindFastValueAsync(addresses, domain.Port, cancellationToken); if (address == null) { this.logger.LogWarning($"dns({dns})解析不到{domain.Host}可用的ip解析"); } else { this.logger.LogInformation($"dns({dns}): {domain.Host}->{address}"); } return address; } catch (Exception ex) { cancellationToken.ThrowIfCancellationRequested(); this.logger.LogWarning($"dns({dns})无法解析{domain.Host}:{ex.Message}"); return default; } } /// /// 获取最快的ip /// /// /// /// /// /// private async Task FindFastValueAsync(IEnumerable addresses, int port, CancellationToken cancellationToken) { if (addresses.Any() == false) { return default; } if (port <= 0) { return addresses.FirstOrDefault(); } var tasks = addresses.Select(address => this.IsAvailableAsync(address, port, cancellationToken)); var fastTask = await Task.WhenAny(tasks); return await fastTask; } /// /// 验证远程节点是否可连接 /// /// /// /// /// /// private async Task IsAvailableAsync(IPAddress address, int port, CancellationToken cancellationToken) { try { using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token); await socket.ConnectAsync(address, port, linkedTokenSource.Token); return address; } catch (OperationCanceledException) { cancellationToken.ThrowIfCancellationRequested(); this.SetDisabled(address); return default; } catch (Exception) { this.SetDisabled(address); await Task.Delay(this.connectTimeout, cancellationToken); return default; } } } }