Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,23 @@ public static void ListenHttpProxy(this KestrelServerOptions options)
$"TCP port {httpProxyPort} is already occupied by other processes.");
}

options.Listen(IReverseProxyService.Constants.Instance.ProxyIp, httpProxyPort, listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
var proxyMiddleware = options.ApplicationServices.GetRequiredService<HttpProxyMiddleware>();
var tunnelMiddleware = options.ApplicationServices.GetRequiredService<TunnelMiddleware>();

listen.UseFlowAnalyze();
listen.Use(next => context => proxyMiddleware.InvokeAsync(next, context));
listen.UseTls();
listen.Use(next => context => tunnelMiddleware.InvokeAsync(next, context));
});

options.GetLogger().LogInformation(
var proxyMiddleware = options.ApplicationServices.GetRequiredService<HttpProxyMiddleware>();
var tunnelMiddleware = options.ApplicationServices.GetRequiredService<TunnelMiddleware>();

options.ListenAndLog(
IReverseProxyService.Constants.Instance.ProxyIp,
httpProxyPort,
listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listen.UseFlowAnalyze();
listen.Use(next => context => proxyMiddleware.InvokeAsync(next, context));
listen.UseTls();
listen.Use(next => context => tunnelMiddleware.InvokeAsync(next, context));
},
"Listened http://{ProxyIp}:{httpProxyPort}, HTTP proxy service startup completed.",
IReverseProxyService.Constants.Instance.ProxyIp, httpProxyPort);
IReverseProxyService.Constants.Instance.ProxyIp,
httpProxyPort);
}

#if WINDOWS
Expand All @@ -59,15 +61,10 @@ public static void ListenHttpProxy(this KestrelServerOptions options)
public static void ListenSshReverseProxy(this KestrelServerOptions options)
{
var sshPort = IReverseProxyConfig.SshPort;
options.ListenLocalhost(sshPort, listen =>
{
listen.UseFlowAnalyze();
listen.UseConnectionHandler<GithubSshReverseProxyHandler>();
});

var logger = options.GetLogger();
logger.LogInformation(
"Listened ssh://localhost:{sshPort}, the SSH reverse proxy service of GitHub is started.", sshPort);
options.ListenLocalReverseProxy<GithubSshReverseProxyHandler>(
sshPort,
"Listened ssh://localhost:{sshPort}, the SSH reverse proxy service of GitHub is started.",
sshPort);
}
#endif

Expand All @@ -80,15 +77,10 @@ public static void ListenSshReverseProxy(this KestrelServerOptions options)
public static void ListenGitReverseProxy(this KestrelServerOptions options)
{
var gitPort = IReverseProxyConfig.GitPort;
options.ListenLocalhost(gitPort, listen =>
{
listen.UseFlowAnalyze();
listen.UseConnectionHandler<GithubGitReverseProxyHandler>();
});

var logger = options.GetLogger();
logger.LogInformation(
"Listened git://localhost:{gitPort}, the Git reverse proxy service of GitHub has been started.", gitPort);
options.ListenLocalReverseProxy<GithubGitReverseProxyHandler>(
gitPort,
"Listened git://localhost:{gitPort}, the Git reverse proxy service of GitHub has been started.",
gitPort);
}
#endif

Expand All @@ -100,12 +92,29 @@ public static void ListenGitReverseProxy(this KestrelServerOptions options)
public static void ListenHttpReverseProxy(this KestrelServerOptions options)
{
var httpPort = IReverseProxyConfig.HttpPort;
options.Listen(IReverseProxyService.Constants.Instance.ProxyIp, httpPort);

var logger = options.GetLogger();
logger.LogInformation(
options.ListenAndLog(
IReverseProxyService.Constants.Instance.ProxyIp,
httpPort,
static _ => { },
"Listened http://{ProxyIp}:{httpPort}, HTTP reverse proxy service startup completed.",
IReverseProxyService.Constants.Instance.ProxyIp, httpPort);
IReverseProxyService.Constants.Instance.ProxyIp,
httpPort);
}

/// <summary>
/// 监听 CRL(证书吊销列表)服务,供 Schannel 等客户端完成本地 MITM 证书的吊销检查
/// </summary>
/// <param name="options"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ListenCrlReverseProxy(this KestrelServerOptions options)
{
var crlPort = IReverseProxyConfig.CrlPort;
options.ListenAndLog(
IPAddress.Loopback,
crlPort,
static listen => listen.Protocols = HttpProtocols.Http1,
"Listened http://127.0.0.1:{crlPort}, CRL service startup completed.",
crlPort);
}

/// <summary>
Expand All @@ -122,17 +131,44 @@ public static void ListenHttpsReverseProxy(this KestrelServerOptions options)
domainResolver.CheckIpv6SupportAsync();

var httpsPort = IReverseProxyConfig.HttpsPort;
options.Listen(IReverseProxyService.Constants.Instance.ProxyIp, httpsPort, listen =>
options.ListenAndLog(
IReverseProxyService.Constants.Instance.ProxyIp,
httpsPort,
static listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listen.UseFlowAnalyze();
listen.UseTls();
},
"Listened https://{ProxyIp}:{httpsPort}, HTTPS reverse proxy service startup completed.",
IReverseProxyService.Constants.Instance.ProxyIp,
httpsPort);
}

/// <summary>
/// 监听本地反向代理(SSH / Git 通用)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void ListenLocalReverseProxy<TConnectionHandler>(this KestrelServerOptions options, int port, string message, params object[] args)
where TConnectionHandler : ConnectionHandler
{
options.ListenLocalhost(port, listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listen.UseFlowAnalyze();
listen.UseTls();
listen.UseConnectionHandler<TConnectionHandler>();
});

var logger = options.GetLogger();
logger.LogInformation(
"Listened https://{ProxyIp}:{httpsPort}, HTTPS reverse proxy service startup completed.",
IReverseProxyService.Constants.Instance.ProxyIp, httpsPort);
options.GetLogger().LogInformation(message, args);
}

/// <summary>
/// 监听指定地址并记录启动日志
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void ListenAndLog(this KestrelServerOptions options, IPAddress ip, int port, Action<ListenOptions> configure, string message, params object[] args)
{
options.Listen(ip, port, configure);
options.GetLogger().LogInformation(message, args);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ internal static IServiceCollection AddReverseProxyServer(this IServiceCollection
.AddMemoryCache()
.AddHttpForwarder()
.AddSingleton<CertService>()
.AddSingleton<CrlMiddleware>()
//.AddSingleton<ICaCertInstaller, CaCertInstallerOfMacOS>()
//.AddSingleton<ICaCertInstaller, CaCertInstallerOfWindows>()
//.AddSingleton<ICaCertInstaller, CaCertInstallerOfLinuxRedHat>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ partial interface IReverseProxyConfig
/// </summary>
static int HttpsPort { get; } = GetAvailableTcpPort(HttpsPortDefault);

const int CrlPortDefault = 26502;

/// <summary>
/// CRL(证书吊销列表)服务端口,用于为本地 MITM 证书提供吊销分发点
/// </summary>
static int CrlPort { get; } = GetAvailableTcpPort(CrlPortDefault);

/// <summary>
/// 获取已监听的端口
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// https://github.com/dotnetcore/FastGithub/blob/2.1.4/FastGithub.HttpServer/CertGenerator.cs

using System.Formats.Asn1;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -97,7 +98,8 @@ public static X509Certificate2 CreateEndCertificate(
IEnumerable<string>? extraDnsNames = default,
DateTimeOffset? notBefore = default,
DateTimeOffset? notAfter = default,
int rsaKeySizeInBits = 2048)
int rsaKeySizeInBits = 2048,
string? crlDistributionPointUrl = default)
{
using var rsa = RSA.Create(rsaKeySizeInBits);
var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
Expand Down Expand Up @@ -132,6 +134,11 @@ public static X509Certificate2 CreateEndCertificate(
var dnsNames = dnsBuilder.Build();
request.CertificateExtensions.Add(dnsNames);

if (!string.IsNullOrEmpty(crlDistributionPointUrl))
{
request.CertificateExtensions.Add(CreateCrlDistributionPointExtension(crlDistributionPointUrl));
}

if (notBefore == null || notBefore.Value < issuerCertificate.NotBefore)
{
notBefore = issuerCertificate.NotBefore;
Expand All @@ -147,6 +154,39 @@ public static X509Certificate2 CreateEndCertificate(
return certOnly.CopyWithPrivateKey(rsa);
}

/// <summary>
/// 创建 CRL 分发点扩展(OID 2.5.29.31),使 Schannel 等客户端可获取本地 CRL 完成吊销检查
/// </summary>
/// <param name="crlUrl">CRL 的 HTTP 地址</param>
/// <returns></returns>
static X509Extension CreateCrlDistributionPointExtension(string crlUrl)
{
// CRLDistributionPoints ::= SEQUENCE OF DistributionPoint
// DistributionPoint ::= SEQUENCE {
// distributionPoint [0] DistributionPointName OPTIONAL,
// ... }
// DistributionPointName ::= CHOICE { fullName [0] GeneralNames }
// GeneralName ::= CHOICE { uniformResourceIdentifier [6] IA5String }
var writer = new AsnWriter(AsnEncodingRules.DER);
using (writer.PushSequence()) // CRLDistributionPoints
{
using (writer.PushSequence()) // DistributionPoint
{
using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) // distributionPoint [0]
{
using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0))) // fullName [0]
{
writer.WriteCharacterString(
UniversalTagNumber.IA5String,
crlUrl,
new Asn1Tag(TagClass.ContextSpecific, 6)); // uniformResourceIdentifier
}
}
}
}
return new X509Extension("2.5.29.31", writer.Encode(), critical: false);
}

private static void Add(this SubjectAlternativeNameBuilder builder, string name)
{
if (IPAddress.TryParse(name, out var address))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ sealed class CertService
readonly ILogger<CertService> logger;
readonly IReverseProxyConfig reverseProxyConfig;
private X509Certificate2? caCert;
readonly Lazy<byte[]?> crlBytes;

ReverseProxyServiceImpl ReverseProxyService => reverseProxyConfig.Service;

Expand All @@ -35,6 +36,29 @@ public CertService(
this.serverCertCache = serverCertCache;
this.logger = logger;
this.reverseProxyConfig = reverseProxyConfig;

// 惰性初始化空 CRL(线程安全),避免对外暴露锁字段
crlBytes = new Lazy<byte[]?>(
() =>
{
try
{
caCert ??= new X509Certificate2(fileName: CaPfxFilePath, password: default(string));

return new CertificateRevocationListBuilder().Build(
caCert,
System.Numerics.BigInteger.One,
DateTimeOffset.Now.AddDays(30),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}
catch (Exception e)
{
logger.LogError(e, "CreateEmptyCrl Error");
return null;
}
},
LazyThreadSafetyMode.ExecutionAndPublication);
}

/// <summary>
Expand Down Expand Up @@ -62,6 +86,11 @@ public static bool GitConfigSslverify(bool value)
}
}

/// <summary>
/// 获取由根 CA 签名的空 CRL 字节,供本地 HTTP 服务对外提供,以完成 Schannel 的吊销检查
/// </summary>
public byte[]? CrlBytes => crlBytes.Value;

/// <summary>
/// 获取颁发给指定域名的证书
/// </summary>
Expand All @@ -83,7 +112,10 @@ X509Certificate2 GetOrCreateCert(ICacheEntry entry)
entry.SetAbsoluteExpiration(notAfter);

var subjectName = new X500DistinguishedName($"CN={domain}");
using var serverCert = CertGenerator.CreateEndCertificate(caCert, subjectName, GetDomains());
// 本地 CRL 服务的 HTTP 地址(写入 MITM 叶子证书的 CRL 分发点)
using var serverCert = CertGenerator.CreateEndCertificate(
caCert, subjectName, GetDomains(),
crlDistributionPointUrl: CrlBytes == null ? null : $"http://{IPAddress.Loopback}:{IReverseProxyConfig.CrlPort}/crl");
var serverCertPfx = serverCert.Export(X509ContentType.Pfx);
// 将生成的证书导出后重新创建一个
return new X509Certificate2(serverCertPfx);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// ReSharper disable once CheckNamespace
namespace BD.WTTS.Services.Implementation;

/// <summary>
/// CRL(证书吊销列表)服务中间件,用于对外提供本地 MITM 证书的吊销分发点
/// </summary>
sealed class CrlMiddleware
{
readonly CertService certService;

public CrlMiddleware(CertService certService)
{
this.certService = certService;
}

/// <summary>
/// 处理请求
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (HttpMethods.IsGet(context.Request.Method) &&
context.Request.Path.Equals(new PathString("/crl"), StringComparison.OrdinalIgnoreCase))
{
var crlBytes = certService.CrlBytes;
if (crlBytes == null)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
context.Response.ContentType = "application/pkix-crl";
await context.Response.Body.WriteAsync(crlBytes);
}
else
{
await next(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ static void StartupConfigure(IApplicationBuilder app)
{
app.UseHttpLocalRequest();

// 使用 CRL 证书吊销列表中间件
var crlMiddleware = app.ApplicationServices.GetRequiredService<CrlMiddleware>();
app.Use(next => context => crlMiddleware.InvokeAsync(context, next));

app.UseHttpProxyPac();
app.UseRequestLogging();
app.UseHttpReverseProxy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ StartProxyResult StartProxyCore()
else
{
options.ListenHttpsReverseProxy();
options.ListenCrlReverseProxy();
if (EnableHttpProxyToHttps)
options.ListenHttpReverseProxy();
}
Expand Down
Loading