HttpClientHandler多ip连接尝试

This commit is contained in:
陈国伟 2021-09-26 15:02:24 +08:00
parent e912f06cd4
commit dee494b754
7 changed files with 236 additions and 350 deletions

View File

@ -17,7 +17,7 @@ namespace FastGithub.DomainResolve
{
private readonly IPEndPoint dns;
private readonly IRequestResolver resolver;
private readonly TimeSpan timeout = TimeSpan.FromSeconds(5d);
private readonly int timeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds;
/// <summary>
/// DNS客户端
@ -28,7 +28,7 @@ namespace FastGithub.DomainResolve
this.dns = dns;
this.resolver = dns.Port == 53
? new TcpRequestResolver(dns)
: new UdpRequestResolver(dns, new TcpRequestResolver(dns));
: new UdpRequestResolver(dns, new TcpRequestResolver(dns), this.timeout);
}
/// <summary>
@ -46,10 +46,7 @@ namespace FastGithub.DomainResolve
};
request.Questions.Add(new Question(new Domain(domain), RecordType.A));
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();
}

View File

@ -8,6 +8,7 @@ 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;
@ -18,21 +19,14 @@ namespace FastGithub.DomainResolve
/// </summary>
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 FastGithubConfig fastGithubConfig;
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>
/// 域名解析器
@ -41,7 +35,6 @@ namespace FastGithub.DomainResolve
/// <param name="fastGithubConfig"></param>
/// <param name="logger"></param>
public DomainResolver(
DnscryptProxy dnscryptProxy,
FastGithubConfig fastGithubConfig,
ILogger<DomainResolver> logger)
@ -51,39 +44,58 @@ namespace FastGithub.DomainResolve
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)
public async Task<IPAddress> ResolveAsync(DnsEndPoint domain, CancellationToken cancellationToken)
{
var semaphore = this.semaphoreSlims.GetOrAdd(domain, _ => new SemaphoreSlim(1, 1));
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");
}
/// <summary>
/// 验证远程节点是否可连接
/// </summary>
/// <param name="ipEndPoint"></param>
/// <param name="cancellationToken"></param>
/// <exception cref="OperationCanceledException"></exception>
/// <returns></returns>
private async Task<bool> IsAvailableAsync(IPEndPoint ipEndPoint, CancellationToken cancellationToken)
{
var semaphore = this.semaphoreSlims.GetOrAdd(ipEndPoint, _ => new SemaphoreSlim(1, 1));
try
{
await semaphore.WaitAsync(CancellationToken.None);
return await this.ResolveCoreAsync(domain, cancellationToken);
if (this.ipEndPointAvailableCache.TryGetValue<bool>(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
{
@ -91,89 +103,46 @@ namespace FastGithub.DomainResolve
}
}
/// <summary>
/// 解析域名
/// </summary>
/// <param name="domain"></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="domain">域名</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<IPAddress?> LookupByDnscryptAsync(DnsEndPoint domain, CancellationToken cancellationToken)
public async IAsyncEnumerable<IPAddress> ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var dns = this.dnscryptProxy.LocalEndPoint;
if (dns == null)
if (domain == "localhost")
{
return null;
yield return IPAddress.Loopback;
yield break;
}
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 hashSet = new HashSet<IPAddress>();
var cryptDns = this.dnscryptProxy.LocalEndPoint;
if (cryptDns != null)
{
var dnsClient = new DnsClient(dns);
var address = await this.LookupAsync(dnsClient, domain, cancellationToken);
if (address != null)
var dnsClient = new DnsClient(cryptDns);
foreach (var address in await this.LookupAsync(dnsClient, domain, cancellationToken))
{
return address;
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;
}
}
}
return default;
}
/// <summary>
@ -183,92 +152,20 @@ namespace FastGithub.DomainResolve
/// <param name="domain"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<IPAddress?> LookupAsync(DnsClient dnsClient, DnsEndPoint domain, CancellationToken cancellationToken)
private async Task<IPAddress[]> LookupAsync(DnsClient dnsClient, string domain, CancellationToken cancellationToken)
{
try
{
var addresses = await dnsClient.LookupAsync(domain.Host, cancellationToken);
var address = await this.FindFastValueAsync(addresses, domain.Port, cancellationToken);
if (address == null)
{
this.logger.LogWarning($"{dnsClient}解析不到{domain.Host}可用的ip解析");
}
else
{
this.logger.LogInformation($"{dnsClient}: {domain.Host}->{address}");
}
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 (Exception ex)
{
cancellationToken.ThrowIfCancellationRequested();
this.logger.LogWarning($"{dnsClient}无法解析{domain.Host}{ex.Message}");
return default;
}
}
/// <summary>
/// 获取最快的ip
/// </summary>
/// <param name="addresses"></param>
/// <param name="port"></param>
/// <param name="cancellationToken"></param>
/// <exception cref="OperationCanceledException"></exception>
/// <returns></returns>
private async Task<IPAddress?> FindFastValueAsync(IEnumerable<IPAddress> addresses, int port, CancellationToken cancellationToken)
{
addresses = addresses.Where(IsEnableIPAddress).ToArray();
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;
bool IsEnableIPAddress(IPAddress address)
{
return this.disableIPAddressCache.TryGetValue(address, out _) == false;
}
}
/// <summary>
/// 验证远程节点是否可连接
/// </summary>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="cancellationToken"></param>
/// <exception cref="OperationCanceledException"></exception>
/// <returns></returns>
private async Task<IPAddress?> 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;
this.logger.LogWarning($"{dnsClient}无法解析{domain}{ex.Message}");
return Array.Empty<IPAddress>();
}
}
}

View File

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

View File

@ -3,12 +3,13 @@ using FastGithub.DomainResolve;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@ -41,27 +42,7 @@ namespace FastGithub.Http
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
await this.ProcessRequestAsync(request, cancellationToken);
return await this.SendRequestAsync(request, cancellationToken);
}
catch (HttpRequestException ex)
{
this.InterceptRequestException(request, ex);
throw;
}
}
/// <summary>
/// 处理请求
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri;
if (uri == null)
@ -70,91 +51,22 @@ namespace FastGithub.Http
}
// 请求上下文信息
var context = new RequestContext
{
Domain = uri.Host,
IsHttps = uri.Scheme == Uri.UriSchemeHttps,
TlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom()
};
request.SetRequestContext(context);
var isHttps = uri.Scheme == Uri.UriSchemeHttps;
var tlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom();
request.SetRequestContext(new RequestContext(isHttps, tlsSniValue));
// 解析ip替换https为http
var uriBuilder = new UriBuilder(uri)
{
Scheme = Uri.UriSchemeHttp
};
// 设置请求host修改协议为http
request.Headers.Host = uri.Host;
request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
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>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage request, 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
{
return await base.SendAsync(request, cancellationToken);
}
}
/// <summary>
/// 拦截请求异常
/// 查找TimedOut的ip地址添加到黑名单
/// </summary>
/// <param name="request"></param>
/// <param name="exception"></param>
private void InterceptRequestException(HttpRequestMessage request, HttpRequestException exception)
{
if (request.RequestUri == null || IsTimedOutSocketError(exception) == false)
{
return;
return base.SendAsync(request, linkedTokenSource.Token);
}
if (IPAddress.TryParse(request.RequestUri.Host, out var address))
{
this.domainResolver.SetDisabled(address);
}
if (request.Headers.Host != null)
{
this.domainResolver.FlushDomain(new DnsEndPoint(request.Headers.Host, request.RequestUri.Port));
}
static bool IsTimedOutSocketError(HttpRequestException exception)
{
var inner = exception.InnerException;
while (inner != null)
{
if (inner is SocketException socketException && socketException.SocketErrorCode == SocketError.TimedOut)
{
return true;
}
inner = inner.InnerException;
}
return false;
}
return base.SendAsync(request, cancellationToken);
}
/// <summary>
@ -170,48 +82,106 @@ namespace FastGithub.Http
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
ConnectCallback = async (context, 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;
}
}
ConnectCallback = this.ConnectCallback
};
}
/// <summary>
/// 连接回调
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
var ipEndPoints = this.GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);
await foreach (var ipEndPoint in ipEndPoints)
{
try
{
return await this.ConnectAsync(context, ipEndPoint, cancellationToken);
}
catch (Exception ex)
{
innerExceptions.Add(ex);
}
}
throw new AggregateException("没有可连接成功的IP", innerExceptions);
}
/// <summary>
/// 建立连接
/// </summary>
/// <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)
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(ipEndPoint, cancellationToken);
var stream = new NetworkStream(socket, ownsSocket: true);
var requestContext = context.InitialRequestMessage.GetRequestContext();
if (requestContext.IsHttps == false)
{
return stream;
}
var tlsSniValue = requestContext.TlsSniValue.WithIPAddress(ipEndPoint.Address);
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = 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 = context.DnsEndPoint.Host;
var dnsNames = ReadDnsNames(cert);
return dnsNames.Any(dns => IsMatch(dns, domain));
}
return errors == SslPolicyErrors.None;
}
}
/// <summary>
/// 解析为IPEndPoint
/// </summary>
/// <param name="dnsEndPoint"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async IAsyncEnumerable<IPEndPoint> GetIPEndPointsAsync(DnsEndPoint dnsEndPoint, [EnumeratorCancellation] CancellationToken cancellationToken)
{
if (IPAddress.TryParse(this.domainConfig.IPAddress, out var address) ||
IPAddress.TryParse(dnsEndPoint.Host, out address))
{
yield return new IPEndPoint(address, dnsEndPoint.Port);
}
else
{
await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint.Host, cancellationToken))
{
yield return new IPEndPoint(item, dnsEndPoint.Port);
}
}
}
/// <summary>
/// 读取使用的DNS名称
/// </summary>

View File

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

View File

@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
@ -74,7 +73,7 @@ namespace FastGithub.HttpServer
return false;
}
if (exception is IOException ioException && ioException.InnerException is ConnectionAbortedException)
if (HasInnerException<ConnectionAbortedException>(exception))
{
return false;
}
@ -82,6 +81,26 @@ namespace FastGithub.HttpServer
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>

View File

@ -4,7 +4,7 @@
"HttpProxyPort": 38457, // httplinux/osx使
"FallbackDns": [ // dnscrypt-proxy使
"114.114.114.114:53",
"8.8.8.8:53"
"119.29.29.29:53"
],
"DomainConfigs": {
"*.fastgithub.com": { // *.0