Pārlūkot izejas kodu

HttpClientHandler多ip连接尝试

陈国伟 3 gadi atpakaļ
vecāks
revīzija
dee494b754

+ 3 - 6
FastGithub.DomainResolve/DnsClient.cs

@@ -17,7 +17,7 @@ namespace FastGithub.DomainResolve
     {
     {
         private readonly IPEndPoint dns;
         private readonly IPEndPoint dns;
         private readonly IRequestResolver resolver;
         private readonly IRequestResolver resolver;
-        private readonly TimeSpan timeout = TimeSpan.FromSeconds(5d);
+        private readonly int timeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds;
 
 
         /// <summary>
         /// <summary>
         /// DNS客户端
         /// DNS客户端
@@ -28,7 +28,7 @@ namespace FastGithub.DomainResolve
             this.dns = dns;
             this.dns = dns;
             this.resolver = dns.Port == 53
             this.resolver = dns.Port == 53
                 ? new TcpRequestResolver(dns)
                 ? new TcpRequestResolver(dns)
-                : new UdpRequestResolver(dns, new TcpRequestResolver(dns));
+                : new UdpRequestResolver(dns, new TcpRequestResolver(dns), this.timeout);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -46,10 +46,7 @@ namespace FastGithub.DomainResolve
             };
             };
             request.Questions.Add(new Question(new Domain(domain), RecordType.A));
             request.Questions.Add(new Question(new Domain(domain), RecordType.A));
             var clientRequest = new ClientRequest(this.resolver, request);
             var clientRequest = new ClientRequest(this.resolver, request);
-
-            using var timeoutTokenSource = new CancellationTokenSource(this.timeout);
-            using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
-            var response = await clientRequest.Resolve(linkedTokenSource.Token);
+            var response = await clientRequest.Resolve(cancellationToken);
             return response.AnswerRecords.OfType<IPAddressResourceRecord>().Select(item => item.IPAddress).ToArray();
             return response.AnswerRecords.OfType<IPAddressResourceRecord>().Select(item => item.IPAddress).ToArray();
         }
         }
 
 

+ 71 - 174
FastGithub.DomainResolve/DomainResolver.cs

@@ -8,6 +8,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
 using System.Net.Sockets;
 using System.Net.Sockets;
+using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -18,21 +19,14 @@ namespace FastGithub.DomainResolve
     /// </summary> 
     /// </summary> 
     sealed class DomainResolver : IDomainResolver
     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 DnscryptProxy dnscryptProxy;
         private readonly DnscryptProxy dnscryptProxy;
         private readonly FastGithubConfig fastGithubConfig;
         private readonly FastGithubConfig fastGithubConfig;
         private readonly ILogger<DomainResolver> logger;
         private readonly ILogger<DomainResolver> 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<DnsEndPoint, SemaphoreSlim> semaphoreSlims = new();
+        private readonly ConcurrentDictionary<IPEndPoint, SemaphoreSlim> 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);
 
 
         /// <summary>
         /// <summary>
         /// 域名解析器
         /// 域名解析器
@@ -41,7 +35,6 @@ namespace FastGithub.DomainResolve
         /// <param name="fastGithubConfig"></param>
         /// <param name="fastGithubConfig"></param>
         /// <param name="logger"></param>
         /// <param name="logger"></param>
         public DomainResolver(
         public DomainResolver(
-
             DnscryptProxy dnscryptProxy,
             DnscryptProxy dnscryptProxy,
             FastGithubConfig fastGithubConfig,
             FastGithubConfig fastGithubConfig,
             ILogger<DomainResolver> logger)
             ILogger<DomainResolver> logger)
@@ -51,224 +44,128 @@ namespace FastGithub.DomainResolve
             this.logger = logger;
             this.logger = logger;
         }
         }
 
 
-        /// <summary>
-        /// 设置ip不可用
-        /// </summary>
-        /// <param name="address">ip</param>
-        public void SetDisabled(IPAddress address)
-        {
-            this.disableIPAddressCache.Set(address, address, this.disableIPExpiration);
-        }
-
-        /// <summary>
-        /// 刷新域名解析结果
-        /// </summary>
-        /// <param name="domain">域名</param>
-        public void FlushDomain(DnsEndPoint domain)
-        {
-            this.domainResolveCache.Remove(domain);
-        }
-
-        /// <summary>
-        /// 解析域名
-        /// </summary>
-        /// <param name="domain"></param>
-        /// <param name="cancellationToken"></param>
-        /// <exception cref="OperationCanceledException"></exception>
-        /// <exception cref="FastGithubException"></exception>
-        /// <returns></returns>
-        public async Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default)
-        {
-            var semaphore = this.semaphoreSlims.GetOrAdd(domain, _ => new SemaphoreSlim(1, 1));
-            try
-            {
-                await semaphore.WaitAsync(CancellationToken.None);
-                return await this.ResolveCoreAsync(domain, cancellationToken);
-            }
-            finally
-            {
-                semaphore.Release();
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// 解析域名
         /// 解析域名
         /// </summary>
         /// </summary>
         /// <param name="domain"></param>
         /// <param name="domain"></param>
         /// <param name="cancellationToken"></param>
         /// <param name="cancellationToken"></param>
-        /// <exception cref="OperationCanceledException"></exception>
-        /// <exception cref="FastGithubException"></exception>
-        /// <returns></returns>
-        private async Task<IPAddress> ResolveCoreAsync(DnsEndPoint domain, CancellationToken cancellationToken)
-        {
-            if (domain.Host == "localhost")
-            {
-                return IPAddress.Loopback;
-            }
-
-            if (this.domainResolveCache.TryGetValue<IPAddress>(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;
-        }
-
-
-        /// <summary>
-        /// Dnscrypt查找ip
-        /// </summary>
-        /// <param name="domain"></param>
-        /// <param name="cancellationToken"></param>
         /// <returns></returns>
         /// <returns></returns>
-        private async Task<IPAddress?> LookupByDnscryptAsync(DnsEndPoint domain, CancellationToken cancellationToken)
+        public async Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken)
         {
         {
-            var dns = this.dnscryptProxy.LocalEndPoint;
-            if (dns == null)
+            await foreach (var address in this.ResolveAsync(domain.Host, cancellationToken))
             {
             {
-                return null;
-            }
-
-            var dnsClient = new DnsClient(dns);
-            var address = await this.LookupAsync(dnsClient, domain, cancellationToken);
-            return address ?? await this.LookupAsync(dnsClient, domain, cancellationToken);
-        }
-
-        /// <summary>
-        /// 回退查找ip
-        /// </summary> 
-        /// <param name="domain"></param>
-        /// <param name="cancellationToken"></param>
-        /// <exception cref="OperationCanceledException"></exception>
-        /// <returns></returns>        
-        private async Task<IPAddress?> LookupByFallbackAsync(DnsEndPoint domain, CancellationToken cancellationToken)
-        {
-            foreach (var dns in this.fastGithubConfig.FallbackDns)
-            {
-                var dnsClient = new DnsClient(dns);
-                var address = await this.LookupAsync(dnsClient, domain, cancellationToken);
-                if (address != null)
+                if (await this.IsAvailableAsync(new IPEndPoint(address, domain.Port), cancellationToken))
                 {
                 {
                     return address;
                     return address;
                 }
                 }
             }
             }
-            return default;
+            throw new FastGithubException($"解析不到{domain.Host}可用的IP");
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// 查找ip
+        /// 验证远程节点是否可连接
         /// </summary>
         /// </summary>
-        /// <param name="dnsClient"></param>
-        /// <param name="domain"></param>
+        /// <param name="ipEndPoint"></param>
         /// <param name="cancellationToken"></param>
         /// <param name="cancellationToken"></param>
+        /// <exception cref="OperationCanceledException"></exception>
         /// <returns></returns>
         /// <returns></returns>
-        private async Task<IPAddress?> LookupAsync(DnsClient dnsClient, DnsEndPoint domain, CancellationToken cancellationToken)
+        private async Task<bool> IsAvailableAsync(IPEndPoint ipEndPoint, CancellationToken cancellationToken)
         {
         {
+            var semaphore = this.semaphoreSlims.GetOrAdd(ipEndPoint, _ => new SemaphoreSlim(1, 1));
             try
             try
             {
             {
-                var addresses = await dnsClient.LookupAsync(domain.Host, cancellationToken);
-                var address = await this.FindFastValueAsync(addresses, domain.Port, cancellationToken);
+                await semaphore.WaitAsync(CancellationToken.None);
+                if (this.ipEndPointAvailableCache.TryGetValue<bool>(ipEndPoint, out var available))
+                {
+                    return available;
+                }
 
 
-                if (address == null)
+                try
                 {
                 {
-                    this.logger.LogWarning($"{dnsClient}解析不到{domain.Host}可用的ip解析");
+                    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;
                 }
                 }
-                else
+                catch (Exception)
                 {
                 {
-                    this.logger.LogInformation($"{dnsClient}: {domain.Host}->{address}");
+                    cancellationToken.ThrowIfCancellationRequested();
+                    available = false;
                 }
                 }
-                return address;
+
+                this.ipEndPointAvailableCache.Set(ipEndPoint, available, ipEndPointExpiration);
+                return available;
             }
             }
-            catch (Exception ex)
+            finally
             {
             {
-                cancellationToken.ThrowIfCancellationRequested();
-                this.logger.LogWarning($"{dnsClient}无法解析{domain.Host}:{ex.Message}");
-                return default;
+                semaphore.Release();
             }
             }
         }
         }
 
 
+
         /// <summary>
         /// <summary>
-        /// 获取最快的ip
+        /// 解析域名
         /// </summary>
         /// </summary>
-        /// <param name="addresses"></param>
-        /// <param name="port"></param>
+        /// <param name="domain">域名</param>
         /// <param name="cancellationToken"></param>
         /// <param name="cancellationToken"></param>
-        /// <exception cref="OperationCanceledException"></exception>
         /// <returns></returns>
         /// <returns></returns>
-        private async Task<IPAddress?> FindFastValueAsync(IEnumerable<IPAddress> addresses, int port, CancellationToken cancellationToken)
+        public async IAsyncEnumerable<IPAddress> ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
         {
         {
-            addresses = addresses.Where(IsEnableIPAddress).ToArray();
-            if (addresses.Any() == false)
+            if (domain == "localhost")
             {
             {
-                return default;
+                yield return IPAddress.Loopback;
+                yield break;
             }
             }
 
 
-            if (port <= 0)
+            var hashSet = new HashSet<IPAddress>();
+            var cryptDns = this.dnscryptProxy.LocalEndPoint;
+            if (cryptDns != null)
             {
             {
-                return addresses.FirstOrDefault();
+                var dnsClient = new DnsClient(cryptDns);
+                foreach (var address in await this.LookupAsync(dnsClient, domain, cancellationToken))
+                {
+                    if (hashSet.Add(address) == true)
+                    {
+                        yield return address;
+                    }
+                }
             }
             }
 
 
-            var tasks = addresses.Select(address => this.IsAvailableAsync(address, port, cancellationToken));
-            var fastTask = await Task.WhenAny(tasks);
-            return await fastTask;
-
-            bool IsEnableIPAddress(IPAddress address)
+            foreach (var fallbackDns in this.fastGithubConfig.FallbackDns)
             {
             {
-                return this.disableIPAddressCache.TryGetValue(address, out _) == false;
+                var dnsClient = new DnsClient(fallbackDns);
+                foreach (var address in await this.LookupAsync(dnsClient, domain, cancellationToken))
+                {
+                    if (hashSet.Add(address) == true)
+                    {
+                        yield return address;
+                    }
+                }
             }
             }
         }
         }
 
 
-
         /// <summary>
         /// <summary>
-        /// 验证远程节点是否可连接
+        /// 查找ip
         /// </summary>
         /// </summary>
-        /// <param name="address"></param>
-        /// <param name="port"></param>
+        /// <param name="dnsClient"></param>
+        /// <param name="domain"></param>
         /// <param name="cancellationToken"></param>
         /// <param name="cancellationToken"></param>
-        /// <exception cref="OperationCanceledException"></exception>
         /// <returns></returns>
         /// <returns></returns>
-        private async Task<IPAddress?> IsAvailableAsync(IPAddress address, int port, CancellationToken cancellationToken)
+        private async Task<IPAddress[]> LookupAsync(DnsClient dnsClient, string domain, CancellationToken cancellationToken)
         {
         {
             try
             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;
+                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 (OperationCanceledException)
+            catch (Exception ex)
             {
             {
                 cancellationToken.ThrowIfCancellationRequested();
                 cancellationToken.ThrowIfCancellationRequested();
-                this.SetDisabled(address);
-                return default;
-            }
-            catch (Exception)
-            {
-                this.SetDisabled(address);
-                await Task.Delay(this.connectTimeout, cancellationToken);
-                return default;
+                this.logger.LogWarning($"{dnsClient}无法解析{domain}:{ex.Message}");
+                return Array.Empty<IPAddress>();
             }
             }
         }
         }
     }
     }

+ 8 - 11
FastGithub.DomainResolve/IDomainResolver.cs

@@ -1,4 +1,5 @@
-using System.Net;
+using System.Collections.Generic;
+using System.Net;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -10,16 +11,12 @@ namespace FastGithub.DomainResolve
     public interface IDomainResolver
     public interface IDomainResolver
     {
     {
         /// <summary>
         /// <summary>
-        /// 设置ip不可用
-        /// </summary>
-        /// <param name="address">ip</param> 
-        void SetDisabled(IPAddress address);
-
-        /// <summary>
-        /// 刷新域名解析结果
+        /// 解析域名
         /// </summary>
         /// </summary>
-        /// <param name="domain">域名</param>
-        void FlushDomain(DnsEndPoint domain);
+        /// <param name="domain"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default);
 
 
         /// <summary>
         /// <summary>
         /// 解析域名
         /// 解析域名
@@ -27,6 +24,6 @@ namespace FastGithub.DomainResolve
         /// <param name="domain">域名</param>
         /// <param name="domain">域名</param>
         /// <param name="cancellationToken"></param>
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
         /// <returns></returns>
-        Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken = default);
+        IAsyncEnumerable<IPAddress> ResolveAsync(string domain, CancellationToken cancellationToken = default);
     }
     }
 }
 }

+ 91 - 121
FastGithub.Http/HttpClientHandler.cs

@@ -3,12 +3,13 @@ using FastGithub.DomainResolve;
 using System;
 using System;
 using System.Collections;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
 using System.Net.Http;
 using System.Net.Http;
 using System.Net.Security;
 using System.Net.Security;
 using System.Net.Sockets;
 using System.Net.Sockets;
-using System.Security.Authentication;
+using System.Runtime.CompilerServices;
 using System.Security.Cryptography.X509Certificates;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -27,7 +28,7 @@ namespace FastGithub.Http
         /// HttpClientHandler
         /// HttpClientHandler
         /// </summary>
         /// </summary>
         /// <param name="domainConfig"></param>
         /// <param name="domainConfig"></param>
-        /// <param name="domainResolver"></param>
+        /// <param name="domainResolver"></param> 
         public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver)
         public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver)
         {
         {
             this.domainResolver = domainResolver;
             this.domainResolver = domainResolver;
@@ -41,175 +42,144 @@ namespace FastGithub.Http
         /// <param name="request"></param>
         /// <param name="request"></param>
         /// <param name="cancellationToken"></param>
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
         /// <returns></returns>
-        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
         {
         {
-            try
+            var uri = request.RequestUri;
+            if (uri == null)
             {
             {
-                await this.ProcessRequestAsync(request, cancellationToken);
-                return await this.SendRequestAsync(request, cancellationToken);
+                throw new FastGithubException("必须指定请求的URI");
             }
             }
-            catch (HttpRequestException ex)
+
+            // 请求上下文信息
+            var isHttps = uri.Scheme == Uri.UriSchemeHttps;
+            var tlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom();
+            request.SetRequestContext(new RequestContext(isHttps, tlsSniValue));
+
+            // 设置请求host,修改协议为http
+            request.Headers.Host = uri.Host;
+            request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
+
+            if (this.domainConfig.Timeout != null)
             {
             {
-                this.InterceptRequestException(request, ex);
-                throw;
+                using var timeoutTokenSource = new CancellationTokenSource(this.domainConfig.Timeout.Value);
+                using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
+                return base.SendAsync(request, linkedTokenSource.Token);
             }
             }
+
+            return base.SendAsync(request, cancellationToken);
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// 处理请求
+        /// 创建转发代理的httpHandler
         /// </summary>
         /// </summary>
-        /// <param name="request"></param>
-        /// <param name="cancellationToken"></param>
         /// <returns></returns>
         /// <returns></returns>
-        private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+        private SocketsHttpHandler CreateSocketsHttpHandler()
         {
         {
-            var uri = request.RequestUri;
-            if (uri == null)
-            {
-                throw new FastGithubException("必须指定请求的URI");
-            }
-
-            // 请求上下文信息
-            var context = new RequestContext
-            {
-                Domain = uri.Host,
-                IsHttps = uri.Scheme == Uri.UriSchemeHttps,
-                TlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom()
-            };
-            request.SetRequestContext(context);
-
-            // 解析ip,替换https为http
-            var uriBuilder = new UriBuilder(uri)
+            return new SocketsHttpHandler
             {
             {
-                Scheme = Uri.UriSchemeHttp
+                Proxy = null,
+                UseProxy = false,
+                UseCookies = false,
+                AllowAutoRedirect = false,
+                AutomaticDecompression = DecompressionMethods.None,
+                ConnectCallback = this.ConnectCallback
             };
             };
-
-            if (uri.HostNameType == UriHostNameType.Dns)
-            {
-                if (IPAddress.TryParse(this.domainConfig.IPAddress, out var address) == false)
-                {
-                    var endPoint = new DnsEndPoint(uri.Host, uri.Port);
-                    address = await this.domainResolver.ResolveAsync(endPoint, cancellationToken);
-                }
-                uriBuilder.Host = address.ToString();
-                request.Headers.Host = context.Domain;
-                context.TlsSniValue = context.TlsSniValue.WithIPAddress(address);
-            }
-            request.RequestUri = uriBuilder.Uri;
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// 发送请求
+        /// 连接回调
         /// </summary>
         /// </summary>
-        /// <param name="request"></param>
+        /// <param name="context"></param>
         /// <param name="cancellationToken"></param>
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
         /// <returns></returns>
-        private async Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+        private async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
         {
         {
-            if (this.domainConfig.Timeout != null)
-            {
-                using var timeoutTokenSource = new CancellationTokenSource(this.domainConfig.Timeout.Value);
-                using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
-                return await base.SendAsync(request, linkedTokenSource.Token);
-            }
-            else
+            var innerExceptions = new List<Exception>();
+            var ipEndPoints = this.GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);
+
+            await foreach (var ipEndPoint in ipEndPoints)
             {
             {
-                return await base.SendAsync(request, cancellationToken);
+                try
+                {
+                    return await this.ConnectAsync(context, ipEndPoint, cancellationToken);
+                }
+                catch (Exception ex)
+                {
+                    innerExceptions.Add(ex);
+                }
             }
             }
+
+            throw new AggregateException("没有可连接成功的IP", innerExceptions);
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// 拦截请求异常
-        /// 查找TimedOut的ip地址添加到黑名单
+        /// 建立连接
         /// </summary>
         /// </summary>
-        /// <param name="request"></param>
-        /// <param name="exception"></param>
-        private void InterceptRequestException(HttpRequestMessage request, HttpRequestException exception)
+        /// <param name="context"></param>
+        /// <param name="ipEndPoint"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        private async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext context, IPEndPoint ipEndPoint, CancellationToken cancellationToken)
         {
         {
-            if (request.RequestUri == null || IsTimedOutSocketError(exception) == false)
-            {
-                return;
-            }
+            var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
+            await socket.ConnectAsync(ipEndPoint, cancellationToken);
+            var stream = new NetworkStream(socket, ownsSocket: true);
 
 
-            if (IPAddress.TryParse(request.RequestUri.Host, out var address))
+            var requestContext = context.InitialRequestMessage.GetRequestContext();
+            if (requestContext.IsHttps == false)
             {
             {
-                this.domainResolver.SetDisabled(address);
+                return stream;
             }
             }
 
 
-            if (request.Headers.Host != null)
+            var tlsSniValue = requestContext.TlsSniValue.WithIPAddress(ipEndPoint.Address);
+            var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
+            await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
             {
             {
-                this.domainResolver.FlushDomain(new DnsEndPoint(request.Headers.Host, request.RequestUri.Port));
-            }
+                TargetHost = tlsSniValue.Value,
+                RemoteCertificateValidationCallback = ValidateServerCertificate
+            }, cancellationToken);
 
 
+            return sslStream;
 
 
-            static bool IsTimedOutSocketError(HttpRequestException exception)
+            // 验证证书有效性
+            bool ValidateServerCertificate(object sender, X509Certificate? cert, X509Chain? chain, SslPolicyErrors errors)
             {
             {
-                var inner = exception.InnerException;
-                while (inner != null)
+                if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
                 {
                 {
-                    if (inner is SocketException socketException && socketException.SocketErrorCode == SocketError.TimedOut)
+                    if (this.domainConfig.TlsIgnoreNameMismatch == true)
                     {
                     {
                         return true;
                         return true;
                     }
                     }
-                    inner = inner.InnerException;
+
+                    var domain = context.DnsEndPoint.Host;
+                    var dnsNames = ReadDnsNames(cert);
+                    return dnsNames.Any(dns => IsMatch(dns, domain));
                 }
                 }
-                return false;
+
+                return errors == SslPolicyErrors.None;
             }
             }
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// 创建转发代理的httpHandler
+        /// 解析为IPEndPoint
         /// </summary>
         /// </summary>
+        /// <param name="dnsEndPoint"></param>
+        /// <param name="cancellationToken"></param>
         /// <returns></returns>
         /// <returns></returns>
-        private SocketsHttpHandler CreateSocketsHttpHandler()
+        private async IAsyncEnumerable<IPEndPoint> GetIPEndPointsAsync(DnsEndPoint dnsEndPoint, [EnumeratorCancellation] CancellationToken cancellationToken)
         {
         {
-            return new SocketsHttpHandler
+            if (IPAddress.TryParse(this.domainConfig.IPAddress, out var address) ||
+                IPAddress.TryParse(dnsEndPoint.Host, out address))
             {
             {
-                Proxy = null,
-                UseProxy = false,
-                UseCookies = false,
-                AllowAutoRedirect = false,
-                AutomaticDecompression = DecompressionMethods.None,
-                ConnectCallback = async (context, cancellationToken) =>
+                yield return new IPEndPoint(address, dnsEndPoint.Port);
+            }
+            else
+            {
+                await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint.Host, cancellationToken))
                 {
                 {
-                    var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
-                    await socket.ConnectAsync(context.DnsEndPoint, cancellationToken);
-                    var stream = new NetworkStream(socket, ownsSocket: true);
-
-                    var requestContext = context.InitialRequestMessage.GetRequestContext();
-                    if (requestContext.IsHttps == false)
-                    {
-                        return stream;
-                    }
-
-                    var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
-                    await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
-                    {
-                        EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
-                        TargetHost = requestContext.TlsSniValue.Value,
-                        RemoteCertificateValidationCallback = ValidateServerCertificate
-                    }, cancellationToken);
-                    return sslStream;
-
-
-                    bool ValidateServerCertificate(object sender, X509Certificate? cert, X509Chain? chain, SslPolicyErrors errors)
-                    {
-                        if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
-                        {
-                            if (this.domainConfig.TlsIgnoreNameMismatch == true)
-                            {
-                                return true;
-                            }
-
-                            var domain = requestContext.Domain;
-                            var dnsNames = ReadDnsNames(cert);
-                            return dnsNames.Any(dns => IsMatch(dns, domain));
-                        }
-
-                        return errors == SslPolicyErrors.None;
-                    }
+                    yield return new IPEndPoint(item, dnsEndPoint.Port);
                 }
                 }
-            };
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 11 - 5
FastGithub.Http/RequestContext.cs

@@ -10,16 +10,22 @@ namespace FastGithub.Http
         /// <summary>
         /// <summary>
         /// 获取或设置是否为https请求
         /// 获取或设置是否为https请求
         /// </summary>
         /// </summary>
-        public bool IsHttps { get; set; }
+        public bool IsHttps { get; }
 
 
         /// <summary>
         /// <summary>
-        /// 请求的域名
+        /// 获取或设置Sni值
         /// </summary>
         /// </summary>
-        public string? Domain { get; set; }
+        public TlsSniPattern TlsSniValue { get; }
 
 
         /// <summary>
         /// <summary>
-        /// 获取或设置Sni值
+        /// 请求上下文
         /// </summary>
         /// </summary>
-        public TlsSniPattern TlsSniValue { get; set; }
+        /// <param name="isHttps"></param>
+        /// <param name="tlsSniValue"></param>
+        public RequestContext(bool isHttps, TlsSniPattern tlsSniValue)
+        {
+            IsHttps = isHttps;
+            TlsSniValue = tlsSniValue;
+        }
     }
     }
 }
 }

+ 21 - 2
FastGithub.HttpServer/RequestLoggingMilldeware.cs

@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using System;
 using System;
 using System.Diagnostics;
 using System.Diagnostics;
-using System.IO;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
@@ -74,7 +73,7 @@ namespace FastGithub.HttpServer
                 return false;
                 return false;
             }
             }
 
 
-            if (exception is IOException ioException && ioException.InnerException is ConnectionAbortedException)
+            if (HasInnerException<ConnectionAbortedException>(exception))
             {
             {
                 return false;
                 return false;
             }
             }
@@ -82,6 +81,26 @@ namespace FastGithub.HttpServer
             return true;
             return true;
         }
         }
 
 
+        /// <summary>
+        /// 是否有内部异常异常
+        /// </summary>
+        /// <typeparam name="TInnerException"></typeparam>
+        /// <param name="exception"></param>
+        /// <returns></returns>
+        private static bool HasInnerException<TInnerException>(Exception exception) where TInnerException : Exception
+        {
+            var inner = exception.InnerException;
+            while (inner != null)
+            {
+                if (inner is TInnerException)
+                {
+                    return true;
+                }
+                inner = inner.InnerException;
+            }
+            return false;
+        }
+
         /// <summary>
         /// <summary>
         /// 获取异常信息
         /// 获取异常信息
         /// </summary>
         /// </summary>

+ 1 - 1
FastGithub/appsettings.json

@@ -4,7 +4,7 @@
     "HttpProxyPort": 38457, // http代理端口,linux/osx平台使用
     "HttpProxyPort": 38457, // http代理端口,linux/osx平台使用
     "FallbackDns": [ // dnscrypt-proxy不可用时使用
     "FallbackDns": [ // dnscrypt-proxy不可用时使用
       "114.114.114.114:53",
       "114.114.114.114:53",
-      "8.8.8.8:53"
+      "119.29.29.29:53"
     ],
     ],
     "DomainConfigs": {
     "DomainConfigs": {
       "*.fastgithub.com": { // 域名的*表示除.之外0到多个任意字符
       "*.fastgithub.com": { // 域名的*表示除.之外0到多个任意字符