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 blackIPAddressCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IMemoryCache domainResolveCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly FastGithubConfig fastGithubConfig; private readonly DnscryptProxy dnscryptProxy; private readonly ILogger logger; private readonly TimeSpan lookupTimeout = TimeSpan.FromSeconds(5d); private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(5d); private readonly TimeSpan dnscryptExpiration = TimeSpan.FromMinutes(5d); private readonly TimeSpan fallbackExpiration = TimeSpan.FromMinutes(1d); 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 SetBlack(IPAddress address, TimeSpan expiration) { this.blackIPAddressCache.Set(address, address, expiration); this.logger.LogWarning($"已将{address}关到小黑屋{expiration.TotalMinutes}分钟"); } /// /// 刷新域名解析结果 /// /// 域名 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.LookupAsync(domain, cancellationToken); } finally { semaphore.Release(); } } /// /// 查找ip /// /// /// /// private async Task LookupAsync(DnsEndPoint domain, CancellationToken cancellationToken) { if (this.domainResolveCache.TryGetValue(domain, out var address)) { return address; } var expiration = this.dnscryptExpiration; if (this.dnscryptProxy.LocalEndPoint != null) { address = await this.LookupCoreAsync(this.dnscryptProxy.LocalEndPoint, domain, cancellationToken); } if (address == null) { expiration = this.fallbackExpiration; address = await this.FallbackLookupAsync(domain, cancellationToken); } if (address == null) { throw new FastGithubException($"当前解析不到{domain.Host}可用的ip,请刷新重试"); } // 往往是被污染的dns if (address.Equals(IPAddress.Loopback) == true) { expiration = this.loopbackExpiration; } this.logger.LogInformation($"[{domain.Host}->{address}]"); this.domainResolveCache.Set(domain, address, expiration); return address; } /// /// 回退查找ip /// /// /// /// private async Task FallbackLookupAsync(DnsEndPoint domain, CancellationToken cancellationToken) { foreach (var dns in this.fastGithubConfig.FallbackDns) { var address = await this.LookupCoreAsync(dns, domain, cancellationToken); if (address != null) { return address; } } return default; } /// /// 查找ip /// /// /// /// /// private async Task LookupCoreAsync(IPEndPoint dns, DnsEndPoint domain, CancellationToken cancellationToken) { try { var dnsClient = new DnsClient(dns); using var timeoutTokenSource = new CancellationTokenSource(this.lookupTimeout); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token); var addresses = await dnsClient.Lookup(domain.Host, RecordType.A, linkedTokenSource.Token); return await this.FindFastValueAsync(addresses, domain.Port, cancellationToken); } catch (Exception ex) { 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; } 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) { if (port <= 0) { return address; } if (this.blackIPAddressCache.TryGetValue(address, out _)) { this.logger.LogWarning($"已跳过黑名单IP:{address}"); return default; } 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) { this.logger.LogWarning($"已忽略连接超时的IP:{address}"); return default; } catch (Exception) { this.logger.LogWarning($"已忽略不可连接的IP:{address}"); await Task.Delay(this.connectTimeout, cancellationToken); return default; } } } }