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;
}
}
}
}