using DNS.Client; using DNS.Client.RequestResolver; using DNS.Protocol; using DNS.Protocol.ResourceRecords; 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.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace FastGithub.DomainResolve { /// /// DNS客户端 /// sealed class DnsClient { private const int DNS_PORT = 53; private const string LOCALHOST = "localhost"; private readonly DnscryptProxy dnscryptProxy; private readonly FastGithubConfig fastGithubConfig; private readonly ILogger logger; private readonly ConcurrentDictionary semaphoreSlims = new(); private readonly IMemoryCache dnsCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly TimeSpan dnsExpiration = TimeSpan.FromMinutes(1d); private readonly int resolveTimeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds; /// /// DNS客户端 /// /// /// /// public DnsClient( DnscryptProxy dnscryptProxy, FastGithubConfig fastGithubConfig, ILogger logger) { this.dnscryptProxy = dnscryptProxy; this.fastGithubConfig = fastGithubConfig; this.logger = logger; } /// /// 解析域名 /// /// 域名 /// /// public async IAsyncEnumerable ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken) { var hashSet = new HashSet(); foreach (var dns in this.GetDnsServers()) { var addresses = await this.LookupAsync(dns, domain, cancellationToken); var value = Filter(hashSet, addresses).ToArray(); if (value.Length > 0) { yield return value; } } static IEnumerable Filter(HashSet hashSet, IPAddress[] addresses) { foreach (var address in addresses) { if (hashSet.Add(address) == true) { yield return address; } } } } /// /// 获取dns服务 /// /// private IEnumerable GetDnsServers() { var cryptDns = this.dnscryptProxy.LocalEndPoint; if (cryptDns != null) { yield return cryptDns; yield return cryptDns; } foreach (var fallbackDns in this.fastGithubConfig.FallbackDns) { yield return fallbackDns; } } /// /// 解析域名 /// /// /// /// /// private async Task LookupAsync(IPEndPoint dns, string domain, CancellationToken cancellationToken = default) { var key = $"{dns}:{domain}"; var semaphore = this.semaphoreSlims.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); await semaphore.WaitAsync(CancellationToken.None); try { if (this.dnsCache.TryGetValue(key, out var value) == false) { value = await this.LookupCoreAsync(dns, domain, cancellationToken); this.dnsCache.Set(key, value, this.dnsExpiration); var items = string.Join(", ", value.Select(item => item.ToString())); this.logger.LogInformation($"dns://{dns}:{domain}->[{items}]"); } return value; } catch (Exception ex) { this.logger.LogWarning($"dns://{dns}无法解析{domain}:{ex.Message}"); return Array.Empty(); } finally { semaphore.Release(); } } /// /// 解析域名 /// /// /// /// /// private async Task LookupCoreAsync(IPEndPoint dns, string domain, CancellationToken cancellationToken = default) { if (domain == LOCALHOST) { return new[] { IPAddress.Loopback }; } var resolver = dns.Port == DNS_PORT ? (IRequestResolver)new TcpRequestResolver(dns) : new UdpRequestResolver(dns, new TcpRequestResolver(dns), this.resolveTimeout); var request = new Request { RecursionDesired = true, OperationCode = OperationCode.Query }; request.Questions.Add(new Question(new Domain(domain), RecordType.A)); var clientRequest = new ClientRequest(resolver, request); var response = await clientRequest.Resolve(cancellationToken); return response.AnswerRecords .OfType() .Where(item => IPAddress.IsLoopback(item.IPAddress) == false) .Select(item => item.IPAddress) .ToArray(); } } }