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.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace FastGithub.DomainResolve { /// /// 域名解析器 /// sealed class DomainResolver : IDomainResolver { private readonly DnscryptProxy dnscryptProxy; private readonly FastGithubConfig fastGithubConfig; private readonly ILogger logger; private readonly ConcurrentDictionary semaphoreSlims = new(); private readonly IMemoryCache ipEndPointAvailableCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly TimeSpan ipEndPointExpiration = TimeSpan.FromMinutes(2d); private readonly TimeSpan ipEndPointConnectTimeout = TimeSpan.FromSeconds(5d); /// /// 域名解析器 /// /// /// /// public DomainResolver( DnscryptProxy dnscryptProxy, FastGithubConfig fastGithubConfig, ILogger logger) { this.dnscryptProxy = dnscryptProxy; this.fastGithubConfig = fastGithubConfig; this.logger = logger; } /// /// 解析域名 /// /// /// /// public async Task ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken) { await foreach (var address in this.ResolveAsync(domain.Host, cancellationToken)) { if (await this.IsAvailableAsync(new IPEndPoint(address, domain.Port), cancellationToken)) { return address; } } throw new FastGithubException($"解析不到{domain.Host}可用的IP"); } /// /// 验证远程节点是否可连接 /// /// /// /// /// private async Task IsAvailableAsync(IPEndPoint ipEndPoint, CancellationToken cancellationToken) { var semaphore = this.semaphoreSlims.GetOrAdd(ipEndPoint, _ => new SemaphoreSlim(1, 1)); try { await semaphore.WaitAsync(CancellationToken.None); if (this.ipEndPointAvailableCache.TryGetValue(ipEndPoint, out var available)) { return available; } try { using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); using var timeoutTokenSource = new CancellationTokenSource(this.ipEndPointConnectTimeout); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token); await socket.ConnectAsync(ipEndPoint, linkedTokenSource.Token); available = true; } catch (Exception) { cancellationToken.ThrowIfCancellationRequested(); available = false; } this.ipEndPointAvailableCache.Set(ipEndPoint, available, ipEndPointExpiration); return available; } finally { semaphore.Release(); } } /// /// 解析域名 /// /// 域名 /// /// public async IAsyncEnumerable ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken) { if (domain == "localhost") { yield return IPAddress.Loopback; yield break; } var hashSet = new HashSet(); var cryptDns = this.dnscryptProxy.LocalEndPoint; if (cryptDns != null) { var dnsClient = new DnsClient(cryptDns); foreach (var address in await this.LookupAsync(dnsClient, domain, cancellationToken)) { if (hashSet.Add(address) == true) { yield return address; } } } foreach (var fallbackDns in this.fastGithubConfig.FallbackDns) { var dnsClient = new DnsClient(fallbackDns); foreach (var address in await this.LookupAsync(dnsClient, domain, cancellationToken)) { if (hashSet.Add(address) == true) { yield return address; } } } } /// /// 查找ip /// /// /// /// /// private async Task LookupAsync(DnsClient dnsClient, string domain, CancellationToken cancellationToken) { try { var addresses = await dnsClient.LookupAsync(domain, cancellationToken); var items = string.Join(", ", addresses.Select(item => item.ToString())); this.logger.LogInformation($"{dnsClient}:{domain}->[{items}]"); return addresses; } catch (Exception ex) { cancellationToken.ThrowIfCancellationRequested(); this.logger.LogWarning($"{dnsClient}无法解析{domain}:{ex.Message}"); return Array.Empty(); } } } }