diff --git a/FastGithub.Configuration/DomainPattern.cs b/FastGithub.Configuration/DomainPattern.cs index 12f425e..0e841f2 100644 --- a/FastGithub.Configuration/DomainPattern.cs +++ b/FastGithub.Configuration/DomainPattern.cs @@ -7,7 +7,7 @@ namespace FastGithub.Configuration /// 表示域名表达式 /// *表示除.之外任意0到多个字符 /// - sealed class DomainPattern : IComparable + public class DomainPattern : IComparable { private readonly Regex regex; private readonly string domainPattern; diff --git a/FastGithub.Configuration/FastGithubConfig.cs b/FastGithub.Configuration/FastGithubConfig.cs index 17fcb40..bf929e5 100644 --- a/FastGithub.Configuration/FastGithubConfig.cs +++ b/FastGithub.Configuration/FastGithubConfig.cs @@ -117,9 +117,9 @@ namespace FastGithub.Configuration /// 获取所有域名表达式 /// /// - public string[] GetDomainPatterns() + public DomainPattern[] GetDomainPatterns() { - return this.domainConfigs.Keys.Select(item => item.ToString()).ToArray(); + return this.domainConfigs.Keys.ToArray(); } } } diff --git a/FastGithub.DomainResolve/DnsClient.cs b/FastGithub.DomainResolve/DnsClient.cs index e60f20a..2751488 100644 --- a/FastGithub.DomainResolve/DnsClient.cs +++ b/FastGithub.DomainResolve/DnsClient.cs @@ -2,13 +2,16 @@ 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; @@ -21,22 +24,72 @@ namespace FastGithub.DomainResolve { 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(2d); + private readonly TimeSpan dnsExpiration = TimeSpan.FromMinutes(1d); private readonly int resolveTimeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds; /// /// DNS客户端 - /// + /// + /// + /// /// - public DnsClient(ILogger logger) + 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()) + { + foreach (var address in await this.LookupAsync(dns, domain, cancellationToken)) + { + 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; + } + } + /// /// 解析域名 /// @@ -44,7 +97,7 @@ namespace FastGithub.DomainResolve /// /// /// - public async Task LookupAsync(IPEndPoint dns, string domain, CancellationToken cancellationToken = default) + 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)); @@ -96,10 +149,16 @@ namespace FastGithub.DomainResolve 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().Select(item => item.IPAddress).ToArray(); + + return response.AnswerRecords + .OfType() + .Where(item => IPAddress.IsLoopback(item.IPAddress) == false) + .Select(item => item.IPAddress) + .ToArray(); } } } diff --git a/FastGithub.DomainResolve/DnscryptProxy.cs b/FastGithub.DomainResolve/DnscryptProxy.cs index 7905e0d..e00dc0e 100644 --- a/FastGithub.DomainResolve/DnscryptProxy.cs +++ b/FastGithub.DomainResolve/DnscryptProxy.cs @@ -1,4 +1,5 @@ using FastGithub.Configuration; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; @@ -20,6 +21,8 @@ namespace FastGithub.DomainResolve private const string PATH = "dnscrypt-proxy"; private const string NAME = "dnscrypt-proxy"; + private readonly ILogger logger; + /// /// 相关进程 /// @@ -30,12 +33,38 @@ namespace FastGithub.DomainResolve /// public IPEndPoint? LocalEndPoint { get; private set; } + /// + /// DnscryptProxy服务 + /// + /// + public DnscryptProxy(ILogger logger) + { + this.logger = logger; + } + /// /// 启动dnscrypt-proxy /// /// /// public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + await this.StartCoreAsync(cancellationToken); + } + catch (Exception ex) + { + this.logger.LogWarning($"{NAME}启动失败:{ex.Message}"); + } + } + + /// + /// 启动dnscrypt-proxy + /// + /// + /// + private async Task StartCoreAsync(CancellationToken cancellationToken) { var tomlPath = Path.Combine(PATH, $"{NAME}.toml"); var port = GetAvailablePort(IPAddress.Loopback.AddressFamily); @@ -70,7 +99,32 @@ namespace FastGithub.DomainResolve } } - + /// + /// 停止服务 + /// + public void Stop() + { + try + { + if (OperatingSystem.IsWindows()) + { + StartDnscryptProxy("-service stop")?.WaitForExit(); + StartDnscryptProxy("-service uninstall")?.WaitForExit(); + } + if (this.process != null && this.process.HasExited == false) + { + this.process.Kill(); + } + } + catch (Exception ex) + { + this.logger.LogWarning($"{NAME}停止失败:{ex.Message }"); + } + finally + { + this.LocalEndPoint = null; + } + } /// /// 获取可用的随机端口 @@ -113,24 +167,6 @@ namespace FastGithub.DomainResolve this.LocalEndPoint = null; } - /// - /// 停止dnscrypt-proxy - /// - public void Stop() - { - if (OperatingSystem.IsWindows()) - { - StartDnscryptProxy("-service stop")?.WaitForExit(); - StartDnscryptProxy("-service uninstall")?.WaitForExit(); - } - - if (this.process != null && this.process.HasExited == false) - { - this.process.Kill(); - } - this.LocalEndPoint = null; - } - /// /// 启动DnscryptProxy进程 /// diff --git a/FastGithub.DomainResolve/DnscryptProxyHostedService.cs b/FastGithub.DomainResolve/DomainResolveHostedService.cs similarity index 51% rename from FastGithub.DomainResolve/DnscryptProxyHostedService.cs rename to FastGithub.DomainResolve/DomainResolveHostedService.cs index 219c068..ba2a4df 100644 --- a/FastGithub.DomainResolve/DnscryptProxyHostedService.cs +++ b/FastGithub.DomainResolve/DomainResolveHostedService.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; @@ -7,58 +6,54 @@ using System.Threading.Tasks; namespace FastGithub.DomainResolve { /// - /// DnscryptProxy后台服务 + /// 域名解析后台服务 /// - sealed class DnscryptProxyHostedService : BackgroundService + sealed class DomainResolveHostedService : BackgroundService { - private readonly ILogger logger; private readonly DnscryptProxy dnscryptProxy; + private readonly DomainSpeedTester speedTester; + + private readonly TimeSpan speedTestDueTime = TimeSpan.FromSeconds(10d); + private readonly TimeSpan speedTestPeriod = TimeSpan.FromMinutes(2d); /// - /// DnscryptProxy后台服务 + /// 域名解析后台服务 /// /// - /// - public DnscryptProxyHostedService( + /// + public DomainResolveHostedService( DnscryptProxy dnscryptProxy, - ILogger logger) + DomainSpeedTester speedTester) { this.dnscryptProxy = dnscryptProxy; - this.logger = logger; + this.speedTester = speedTester; } /// - /// 启动dnscrypt-proxy + /// 后台任务 /// /// /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - try + await this.dnscryptProxy.StartAsync(stoppingToken); + await Task.Delay(this.speedTestDueTime, stoppingToken); + + while (stoppingToken.IsCancellationRequested == false) { - await this.dnscryptProxy.StartAsync(stoppingToken); - } - catch (Exception ex) - { - this.logger.LogWarning($"{this.dnscryptProxy}启动失败:{ex.Message}"); + await this.speedTester.TestSpeedAsync(stoppingToken); + await Task.Delay(this.speedTestPeriod, stoppingToken); } } /// - /// 停止dnscrypt-proxy + /// 停止服务 /// /// /// public override Task StopAsync(CancellationToken cancellationToken) { - try - { - this.dnscryptProxy.Stop(); - } - catch (Exception ex) - { - this.logger.LogWarning($"{this.dnscryptProxy}停止失败:{ex.Message}"); - } + this.dnscryptProxy.Stop(); return base.StopAsync(cancellationToken); } } diff --git a/FastGithub.DomainResolve/DomainResolver.cs b/FastGithub.DomainResolve/DomainResolver.cs index fa88a8a..c0374af 100644 --- a/FastGithub.DomainResolve/DomainResolver.cs +++ b/FastGithub.DomainResolve/DomainResolver.cs @@ -12,24 +12,20 @@ namespace FastGithub.DomainResolve /// sealed class DomainResolver : IDomainResolver { - private readonly DnscryptProxy dnscryptProxy; - private readonly FastGithubConfig fastGithubConfig; private readonly DnsClient dnsClient; + private readonly DomainSpeedTester speedTester; /// /// 域名解析器 /// - /// - /// /// + /// public DomainResolver( - DnscryptProxy dnscryptProxy, - FastGithubConfig fastGithubConfig, - DnsClient dnsClient) + DnsClient dnsClient, + DomainSpeedTester speedTester) { - this.dnscryptProxy = dnscryptProxy; - this.fastGithubConfig = fastGithubConfig; this.dnsClient = dnsClient; + this.speedTester = speedTester; } /// @@ -55,35 +51,21 @@ namespace FastGithub.DomainResolve /// public async IAsyncEnumerable ResolveAllAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken) { - var hashSet = new HashSet(); - foreach (var dns in this.GetDnsServers()) + var addresses = this.speedTester.GetIPAddresses(domain); + if (addresses.Length > 0) { - foreach (var address in await this.dnsClient.LookupAsync(dns, domain, cancellationToken)) + foreach (var address in addresses) { - if (hashSet.Add(address) == true) - { - yield return address; - } + yield return address; } } - } - - /// - /// 获取dns服务 - /// - /// - private IEnumerable GetDnsServers() - { - var cryptDns = this.dnscryptProxy.LocalEndPoint; - if (cryptDns != null) + else { - yield return cryptDns; - yield return cryptDns; - } - - foreach (var fallbackDns in this.fastGithubConfig.FallbackDns) - { - yield return fallbackDns; + this.speedTester.Add(domain); + await foreach (var address in this.dnsClient.ResolveAsync(domain, cancellationToken)) + { + yield return address; + } } } } diff --git a/FastGithub.DomainResolve/DomainSpeedTester.cs b/FastGithub.DomainResolve/DomainSpeedTester.cs new file mode 100644 index 0000000..b832bcd --- /dev/null +++ b/FastGithub.DomainResolve/DomainSpeedTester.cs @@ -0,0 +1,151 @@ +using FastGithub.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// 域名的IP测速服务 + /// + sealed class DomainSpeedTester + { + private const string DOMAINS_JSON_FILE = "domains.json"; + + private readonly DnsClient dnsClient; + private readonly ILogger logger; + + private readonly object syncRoot = new(); + private readonly Dictionary domainIPAddressHashSet = new(); + + /// + /// 域名的IP测速服务 + /// + /// + /// + public DomainSpeedTester( + DnsClient dnsClient, + ILogger logger) + { + this.dnsClient = dnsClient; + this.logger = logger; + + try + { + this.LoadDomains(); + } + catch (Exception ex) + { + logger.LogWarning($"加载域名数据失败:{ex.Message}"); + } + } + + /// + /// 加载域名数据 + /// + private void LoadDomains() + { + if (File.Exists(DOMAINS_JSON_FILE) == false) + { + return; + } + + var utf8Json = File.ReadAllBytes(DOMAINS_JSON_FILE); + var domains = JsonSerializer.Deserialize(utf8Json); + if (domains == null) + { + return; + } + + foreach (var domain in domains) + { + this.domainIPAddressHashSet.TryAdd(domain, new IPAddressItemHashSet()); + } + } + + /// + /// 添加要测速的域名 + /// + /// + public void Add(string domain) + { + lock (this.syncRoot) + { + if (this.domainIPAddressHashSet.TryAdd(domain, new IPAddressItemHashSet())) + { + try + { + this.SaveDomains(); + } + catch (Exception ex) + { + logger.LogWarning($"保存域名数据失败:{ex.Message}"); + } + } + } + } + + /// + /// 保存域名 + /// + private void SaveDomains() + { + var domains = this.domainIPAddressHashSet.Keys + .Select(item => new DomainPattern(item)) + .OrderBy(item => item) + .Select(item => item.ToString()) + .ToArray(); + + var utf8Json = JsonSerializer.SerializeToUtf8Bytes(domains, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllBytes(DOMAINS_JSON_FILE, utf8Json); + } + + /// + /// 获取测试后排序的IP + /// + /// + /// + public IPAddress[] GetIPAddresses(string domain) + { + lock (this.syncRoot) + { + if (this.domainIPAddressHashSet.TryGetValue(domain, out var hashSet) && hashSet.Count > 0) + { + return hashSet.ToArray().OrderBy(item => item.PingElapsed).Select(item => item.Address).ToArray(); + } + return Array.Empty(); + } + } + + /// + /// 进行一轮IP测速 + /// + /// + /// + public async Task TestSpeedAsync(CancellationToken cancellationToken) + { + KeyValuePair[] keyValues; + lock (this.syncRoot) + { + keyValues = this.domainIPAddressHashSet.ToArray(); + } + + foreach (var keyValue in keyValues) + { + var domain = keyValue.Key; + var hashSet = keyValue.Value; + await foreach (var address in this.dnsClient.ResolveAsync(domain, cancellationToken)) + { + hashSet.Add(new IPAddressItem(address)); + } + await hashSet.PingAllAsync(); + } + } + } +} diff --git a/FastGithub.DomainResolve/FastGithub.DomainResolve.csproj b/FastGithub.DomainResolve/FastGithub.DomainResolve.csproj index 0ad99be..ba4e9a8 100644 --- a/FastGithub.DomainResolve/FastGithub.DomainResolve.csproj +++ b/FastGithub.DomainResolve/FastGithub.DomainResolve.csproj @@ -17,4 +17,16 @@ + + + + + + + PreserveNewest + true + PreserveNewest + + + diff --git a/FastGithub.DomainResolve/IPAddressItem.cs b/FastGithub.DomainResolve/IPAddressItem.cs new file mode 100644 index 0000000..0e058ea --- /dev/null +++ b/FastGithub.DomainResolve/IPAddressItem.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Net.NetworkInformation; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// IP地址项 + /// + [DebuggerDisplay("Address = {Address}, PingElapsed = {PingElapsed}")] + sealed class IPAddressItem : IEquatable + { + private readonly Ping ping = new(); + + /// + /// 地址 + /// + public IPAddress Address { get; } + + /// + /// Ping耗时 + /// + public TimeSpan PingElapsed { get; private set; } = TimeSpan.MaxValue; + + /// + /// IP地址项 + /// + /// + public IPAddressItem(IPAddress address) + { + this.Address = address; + } + + /// + /// 发起ping请求 + /// + /// + public async Task PingAsync() + { + try + { + var reply = await this.ping.SendPingAsync(this.Address); + this.PingElapsed = reply.Status == IPStatus.Success + ? TimeSpan.FromMilliseconds(reply.RoundtripTime) + : TimeSpan.MaxValue; + } + catch (Exception) + { + this.PingElapsed = TimeSpan.MaxValue; + } + } + + public bool Equals(IPAddressItem? other) + { + return other != null && other.Address.Equals(this.Address); + } + + public override bool Equals(object? obj) + { + return obj is IPAddressItem other && this.Equals(other); + } + + public override int GetHashCode() + { + return this.Address.GetHashCode(); + } + } +} diff --git a/FastGithub.DomainResolve/IPAddressItemHashSet.cs b/FastGithub.DomainResolve/IPAddressItemHashSet.cs new file mode 100644 index 0000000..d8de684 --- /dev/null +++ b/FastGithub.DomainResolve/IPAddressItemHashSet.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// IPAddressItem集合 + /// + sealed class IPAddressItemHashSet + { + private readonly object syncRoot = new(); + private readonly HashSet hashSet = new(); + + /// + /// 获取元素数量 + /// + public int Count => this.hashSet.Count; + + /// + /// 添加元素 + /// + /// + /// + public bool Add(IPAddressItem item) + { + lock (this.syncRoot) + { + return this.hashSet.Add(item); + } + } + + /// + /// 转换为数组 + /// + /// + public IPAddressItem[] ToArray() + { + lock (this.syncRoot) + { + return this.hashSet.ToArray(); + } + } + + /// + /// Ping所有IP + /// + /// + public Task PingAllAsync() + { + var items = this.ToArray(); + if (items.Length == 0) + { + return Task.CompletedTask; + } + if (items.Length == 1) + { + return items[0].PingAsync(); + } + var tasks = items.Select(item => item.PingAsync()); + return Task.WhenAll(tasks); + } + } +} diff --git a/FastGithub.DomainResolve/ServiceCollectionExtensions.cs b/FastGithub.DomainResolve/ServiceCollectionExtensions.cs index c35acc7..35234a8 100644 --- a/FastGithub.DomainResolve/ServiceCollectionExtensions.cs +++ b/FastGithub.DomainResolve/ServiceCollectionExtensions.cs @@ -18,8 +18,10 @@ namespace FastGithub { services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); - return services.AddHostedService(); + services.AddHostedService(); + return services; } } } diff --git a/FastGithub.DomainResolve/domains.json b/FastGithub.DomainResolve/domains.json new file mode 100644 index 0000000..71d5fc7 --- /dev/null +++ b/FastGithub.DomainResolve/domains.json @@ -0,0 +1,22 @@ +[ + "github.com", + "v2ex.com", + "alive.github.com", + "api.github.com", + "collector.githubapp.com", + "github.githubassets.com", + "avatars.githubusercontent.com", + "camo.githubusercontent.com", + "github-releases.githubusercontent.com", + "raw.githubusercontent.com", + "www.gravatar.com", + "onedrive.live.com", + "cdn.v2ex.com", + "microsoft.github.io", + "fonts.geekzu.org", + "gapis.geekzu.org", + "i.stack.imgur.com", + "skyapi.onedrive.live.com", + "codeproject.freetls.fastly.net", + "codeproject.global.ssl.fastly.net" +] \ No newline at end of file