增加域名配置

This commit is contained in:
xljiulang 2021-07-17 23:55:39 +08:00
parent 6559644c99
commit 68c5791323
12 changed files with 313 additions and 102 deletions

View File

@ -18,7 +18,7 @@ namespace FastGithub
/// <summary> /// <summary>
/// 端口 /// 端口
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; set; } = 53;
/// <summary> /// <summary>
/// 转换为IPEndPoint /// 转换为IPEndPoint

View File

@ -1,4 +1,6 @@
namespace FastGithub using System;
namespace FastGithub
{ {
/// <summary> /// <summary>
/// 域名配置 /// 域名配置
@ -12,9 +14,8 @@
/// <summary> /// <summary>
/// 目的地 /// 目的地
/// 支持ip或域名 /// 格式为相对或绝对uri
/// 留空则本域名
/// </summary> /// </summary>
public string? Destination { get; set; } public Uri? Destination { get; set; }
} }
} }

View File

@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
namespace FastGithub
{
/// <summary>
/// FastGithub配置
/// </summary>
public class FastGithubConfig
{
private readonly Dictionary<DomainMatch, DomainConfig> domainConfigs;
/// <summary>
/// 获取信任dns
/// </summary>
public IPEndPoint TrustedDns { get; }
/// <summary>
/// 获取非信任dns
/// </summary>
public IPEndPoint UnTrustedDns { get; }
/// <summary>
/// FastGithub配置
/// </summary>
/// <param name="options"></param>
public FastGithubConfig(FastGithubOptions options)
{
this.TrustedDns = options.TrustedDns.ToIPEndPoint();
this.UnTrustedDns = options.UntrustedDns.ToIPEndPoint();
this.domainConfigs = options.DomainConfigs.ToDictionary(kv => new DomainMatch(kv.Key), kv => kv.Value);
}
/// <summary>
/// 是否匹配指定的域名
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
public bool IsMatch(string domain)
{
return this.domainConfigs.Keys.Any(item => item.IsMatch(domain));
}
/// <summary>
/// 尝试获取域名配置
/// </summary>
/// <param name="domain"></param>
/// <param name="domainConfig"></param>
/// <returns></returns>
public bool TryGetDomainConfig(string domain, [MaybeNullWhen(false)] out DomainConfig domainConfig)
{
var key = this.domainConfigs.Keys.FirstOrDefault(item => item.IsMatch(domain));
if (key == null)
{
domainConfig = default;
return false;
}
return this.domainConfigs.TryGetValue(key, out domainConfig);
}
}
}

View File

@ -1,7 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Net;
namespace FastGithub namespace FastGithub
{ {
@ -10,13 +7,6 @@ namespace FastGithub
/// </summary> /// </summary>
public class FastGithubOptions public class FastGithubOptions
{ {
/// <summary>
/// 域名
/// </summary>
private DomainMatch[]? domainMatches;
private IPEndPoint? trustedDnsIPEndPoint;
private IPEndPoint? unTrustedDnsIPEndPoint;
/// <summary> /// <summary>
/// 受信任的dns服务 /// 受信任的dns服务
/// </summary> /// </summary>
@ -28,50 +18,29 @@ namespace FastGithub
public DnsConfig UntrustedDns { get; set; } = new DnsConfig { IPAddress = "114.114.114.114", Port = 53 }; public DnsConfig UntrustedDns { get; set; } = new DnsConfig { IPAddress = "114.114.114.114", Port = 53 };
/// <summary> /// <summary>
/// 代理的域名表达式 /// 代理的域名配置
/// </summary> /// </summary>
public HashSet<string> DomainPatterns { get; set; } = new(); public Dictionary<string, DomainConfig> DomainConfigs { get; set; } = new();
/// <summary> /// <summary>
/// 验证选项值 /// 初始化选项为配置
/// </summary> /// </summary>
/// <exception cref="FastGithubException"></exception> /// <exception cref="FastGithubException"></exception>
public void Validate() public void InitConfig()
{ {
this.trustedDnsIPEndPoint = this.TrustedDns.ToIPEndPoint(); this.fastGithubConfig = new FastGithubConfig(this);
this.unTrustedDnsIPEndPoint = this.UntrustedDns.ToIPEndPoint();
this.domainMatches = this.DomainPatterns.Select(item => new DomainMatch(item)).ToArray();
}
/// <summary>
/// 受信任的dns服务节点
/// </summary>
public IPEndPoint GetTrustedDns()
{
return this.trustedDnsIPEndPoint ?? throw new InvalidOperationException();
} }
/// <summary> /// <summary>
/// 不受信任的dns服务节点 /// 配置
/// </summary> /// </summary>
public IPEndPoint GetUnTrustedDns() private FastGithubConfig? fastGithubConfig;
{
return this.unTrustedDnsIPEndPoint ?? throw new InvalidOperationException();
}
/// <summary> /// <summary>
/// 是否匹配指定的域名 /// 获取配置
/// </summary> /// </summary>
/// <param name="domain"></param> public FastGithubConfig Config => this.fastGithubConfig!;
/// <returns></returns>
public bool IsMatch(string domain)
{
if (this.domainMatches == null)
{
throw new InvalidOperationException();
}
return this.domainMatches.Any(item => item.IsMatch(domain));
}
} }
} }

View File

@ -80,7 +80,7 @@ namespace FastGithub.Dns
} }
this.logger.LogInformation("dns服务启动成功"); this.logger.LogInformation("dns服务启动成功");
var secondary = IPAddress.Parse(options.CurrentValue.UntrustedDns.IPAddress); var secondary = options.CurrentValue.Config.UnTrustedDns.Address;
this.dnsAddresses = this.SetNameServers(IPAddress.Loopback, secondary); this.dnsAddresses = this.SetNameServers(IPAddress.Loopback, secondary);
FlushResolverCache(); FlushResolverCache();

View File

@ -32,7 +32,7 @@ namespace FastGithub.Dns
{ {
this.options = options; this.options = options;
this.logger = logger; this.logger = logger;
this.untrustedResolver = new UdpRequestResolver(options.CurrentValue.GetTrustedDns()); this.untrustedResolver = new UdpRequestResolver(options.CurrentValue.Config.TrustedDns);
} }
/// <summary> /// <summary>
@ -56,7 +56,7 @@ namespace FastGithub.Dns
} }
var domain = question.Name; var domain = question.Name;
if (this.options.CurrentValue.IsMatch(domain.ToString()) == true) if (this.options.CurrentValue.Config.IsMatch(domain.ToString()) == true)
{ {
var localAddress = remoteEndPointRequest.GetLocalAddress() ?? IPAddress.Loopback; var localAddress = remoteEndPointRequest.GetLocalAddress() ?? IPAddress.Loopback;
var record = new IPAddressResourceRecord(domain, localAddress, this.ttl); var record = new IPAddressResourceRecord(domain, localAddress, this.ttl);

View File

@ -1,10 +1,6 @@
using FastGithub.ReverseProxy; using FastGithub.ReverseProxy;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Net.Http;
using Yarp.ReverseProxy.Forwarder;
namespace FastGithub namespace FastGithub
{ {
@ -20,39 +16,8 @@ namespace FastGithub
/// <returns></returns> /// <returns></returns>
public static IApplicationBuilder UseHttpsReverseProxy(this IApplicationBuilder app) public static IApplicationBuilder UseHttpsReverseProxy(this IApplicationBuilder app)
{ {
var httpForwarder = app.ApplicationServices.GetRequiredService<IHttpForwarder>(); var middleware = app.ApplicationServices.GetRequiredService<ReverseProxyMiddleware>();
var httpClientHanlder = app.ApplicationServices.GetRequiredService<NoSniHttpClientHanlder>(); return app.Use(next => context => middleware.InvokeAsync(context));
var options = app.ApplicationServices.GetRequiredService<IOptionsMonitor<FastGithubOptions>>();
app.Use(next => async context =>
{
var host = context.Request.Host.Host;
if (options.CurrentValue.IsMatch(host) == false)
{
await context.Response.WriteAsJsonAsync(new { message = $"不支持以{host}访问" });
return;
}
var port = context.Request.Host.Port ?? 443;
var destinationPrefix = $"https://{host}:{port}/";
var httpClient = new HttpMessageInvoker(httpClientHanlder, disposeHandler: false);
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient);
if (error != ForwarderError.None)
{
var errorFeature = context.GetForwarderErrorFeature();
if (errorFeature != null)
{
await context.Response.WriteAsJsonAsync(new
{
error = error.ToString(),
message = errorFeature.Exception?.Message
});
}
}
});
return app;
} }
} }
} }

View File

@ -0,0 +1,103 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Forwarder;
namespace FastGithub.ReverseProxy
{
/// <summary>
/// 反向代理中间件
/// </summary>
sealed class ReverseProxyMiddleware
{
private readonly IHttpForwarder httpForwarder;
private readonly SniHttpClientHanlder sniHttpClientHanlder;
private readonly NoSniHttpClientHanlder noSniHttpClientHanlder;
private readonly IOptionsMonitor<FastGithubOptions> options;
public ReverseProxyMiddleware(
IHttpForwarder httpForwarder,
SniHttpClientHanlder sniHttpClientHanlder,
NoSniHttpClientHanlder noSniHttpClientHanlder,
IOptionsMonitor<FastGithubOptions> options)
{
this.httpForwarder = httpForwarder;
this.sniHttpClientHanlder = sniHttpClientHanlder;
this.noSniHttpClientHanlder = noSniHttpClientHanlder;
this.options = options;
}
/// <summary>
/// 处理请求
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context)
{
var host = context.Request.Host.Host;
if (this.options.CurrentValue.Config.TryGetDomainConfig(host, out var domainConfig) == false)
{
await context.Response.WriteAsJsonAsync(new
{
error = ForwarderError.NoAvailableDestinations.ToString(),
message = $"不支持https反向代理{host}这个域名"
});
return;
}
var destinationPrefix = GetDestinationPrefix(host, domainConfig.Destination);
var httpClient = domainConfig.NoSni
? new HttpMessageInvoker(this.noSniHttpClientHanlder, disposeHandler: false)
: new HttpMessageInvoker(this.sniHttpClientHanlder, disposeHandler: false);
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient);
await ResponseErrorAsync(context, error);
}
/// <summary>
/// 获取目标前缀
/// </summary>
/// <param name="host"></param>
/// <param name="destination"></param>
/// <returns></returns>
private static string GetDestinationPrefix(string host, Uri? destination)
{
var defaultValue = $"https://{host}/";
if (destination == null)
{
return defaultValue;
}
var baseUri = new Uri(defaultValue);
return new Uri(baseUri, destination).ToString();
}
/// <summary>
/// 写入错误信息
/// </summary>
/// <param name="context"></param>
/// <param name="error"></param>
/// <returns></returns>
private static async Task ResponseErrorAsync(HttpContext context, ForwarderError error)
{
if (error == ForwarderError.None)
{
return;
}
var errorFeature = context.GetForwarderErrorFeature();
if (errorFeature == null)
{
return;
}
await context.Response.WriteAsJsonAsync(new
{
error = error.ToString(),
message = errorFeature.Exception?.Message
});
}
}
}

View File

@ -19,7 +19,9 @@ namespace FastGithub
.AddMemoryCache() .AddMemoryCache()
.AddHttpForwarder() .AddHttpForwarder()
.AddSingleton<DomainResolver>() .AddSingleton<DomainResolver>()
.AddTransient<NoSniHttpClientHanlder>(); .AddTransient<SniHttpClientHanlder>()
.AddTransient<NoSniHttpClientHanlder>()
.AddSingleton<ReverseProxyMiddleware>();
} }
} }
} }

View File

@ -0,0 +1,60 @@
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.ReverseProxy
{
/// <summary>
/// 携带Sni的的HttpClientHandler
/// </summary>
class SniHttpClientHanlder : DelegatingHandler
{
private readonly DomainResolver domainResolver;
private readonly ILogger<NoSniHttpClientHanlder> logger;
/// <summary>
/// 携带Sni的HttpClientHandler
/// </summary>
/// <param name="domainResolver"></param>
public SniHttpClientHanlder(
DomainResolver domainResolver,
ILogger<NoSniHttpClientHanlder> logger)
{
this.domainResolver = domainResolver;
this.logger = logger;
this.InnerHandler = new SocketsHttpHandler
{
Proxy = null,
UseProxy = false,
AllowAutoRedirect = false,
};
}
/// <summary>
/// 替换域名为ip
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri;
if (uri != null && uri.HostNameType == UriHostNameType.Dns)
{
var address = await this.domainResolver.ResolveAsync(uri.Host, cancellationToken);
this.logger.LogInformation($"[{address}--Sni->{uri.Host}]");
var builder = new UriBuilder(uri)
{
Host = address.ToString()
};
request.RequestUri = builder.Uri;
request.Headers.Host = uri.Host;
}
return await base.SendAsync(request, cancellationToken);
}
}
}

View File

@ -39,7 +39,7 @@ namespace FastGithub
.AddDnscryptProxy() .AddDnscryptProxy()
.AddOptions<FastGithubOptions>() .AddOptions<FastGithubOptions>()
.Bind(ctx.Configuration.GetSection(nameof(FastGithub))) .Bind(ctx.Configuration.GetSection(nameof(FastGithub)))
.PostConfigure(opt => opt.Validate()); .PostConfigure(opt => opt.InitConfig());
}) })
.ConfigureWebHostDefaults(web => .ConfigureWebHostDefaults(web =>
{ {

View File

@ -1,23 +1,71 @@
{ {
"FastGithub": { "FastGithub": {
"TrustedDns": { // "TrustedDns": { // DomainConfigs
"IPAddress": "127.0.0.1", "IPAddress": "127.0.0.1",
"Port": 5533 // 5533Ö¸Ïòdnscrypt-proxy "Port": 5533 // 5533Ö¸Ïòdnscrypt-proxy
}, },
"UnTrustedDns": { // "UnTrustedDns": { // DomainConfigs
"IPAddress": "114.114.114.114", "IPAddress": "114.114.114.114",
"Port": 53 "Port": 53
}, },
"DomainPatterns": [ // *0 "DomainConfigs": { // *0
"github.com", "github.com": {
"githubstatus.com", "NoSni": true,
"*.github.com", "Destination": null
"*.github.io", },
"*.githubapp.com", "githubstatus.com": {
"*.githubassets.com", "NoSni": true,
"*.githubusercontent.com", "Destination": null
"*github*.s3.amazonaws.com" },
] "*.github.com": {
"NoSni": true,
"Destination": null
},
"*.github.io": {
"NoSni": true,
"Destination": null
},
"*.githubapp.com": {
"NoSni": true,
"Destination": null
},
"*.githubassets.com": {
"NoSni": true,
"Destination": null
},
"*.githubusercontent.com": {
"NoSni": true,
"Destination": null
},
"*github*.s3.amazonaws.com": {
"NoSni": true,
"Destination": null
},
"ajax.googleapis.com": {
"NoSni": false,
"Destination": "https://gapis.geekzu.org/ajax/"
},
"fonts.googleapis.com": {
"NoSni": false,
"Destination": "https://fonts.geekzu.org/"
},
"themes.googleusercontent.com": {
"NoSni": false,
"Destination": "https://gapis.geekzu.org/g-themes/"
},
"fonts.gstatic.com": {
"NoSni": false,
"Destination": "https://gapis.geekzu.org/g-fonts/"
},
"secure.gravatar.com": {
"NoSni": false,
"Destination": "https://sdn.geekzu.org/"
},
"*.gravatar.com": {
"NoSni": false,
"Destination": "https://fdn.geekzu.org/"
}
}
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {