|
@@ -0,0 +1,171 @@
|
|
|
|
+using System.Buffers;
|
|
|
|
+using System.IO.Pipelines;
|
|
|
|
+using Microsoft.AspNetCore.Connections;
|
|
|
|
+using System.Net;
|
|
|
|
+using System.Security.Cryptography.X509Certificates;
|
|
|
|
+using DnsClient;
|
|
|
|
+using System.Text;
|
|
|
|
+
|
|
|
|
+#region INIT
|
|
|
|
+
|
|
|
|
+using Microsoft.Extensions.Logging.Console;
|
|
|
|
+using System.Net.Security;
|
|
|
|
+using System.Net.Sockets;
|
|
|
|
+
|
|
|
|
+var builder = WebApplication.CreateBuilder(args);
|
|
|
|
+
|
|
|
|
+//控制台日志格式
|
|
|
|
+builder.Services.AddLogging(opt =>
|
|
|
|
+{
|
|
|
|
+ opt.AddSimpleConsole(p =>
|
|
|
|
+ {
|
|
|
|
+ p.TimestampFormat = "[dd HH:mm:ss] ";
|
|
|
|
+ p.SingleLine = true;
|
|
|
|
+ p.ColorBehavior = LoggerColorBehavior.Enabled;
|
|
|
|
+ });
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+builder.WebHost.UseKestrel(opt =>
|
|
|
|
+{
|
|
|
|
+ opt.Listen(IPAddress.Any, 0, lisOpt =>
|
|
|
|
+ {
|
|
|
|
+ lisOpt.UseConnectionHandler<NoSniProxyHandler>();
|
|
|
|
+ });
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+await using var host = builder.Build();
|
|
|
|
+
|
|
|
|
+var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
|
|
|
+logger.LogInformation("Hello, World!");
|
|
|
|
+await host.RunAsync();
|
|
|
|
+
|
|
|
|
+#endregion INIT
|
|
|
|
+
|
|
|
|
+public class NoSniProxyHandler : ConnectionHandler
|
|
|
|
+{
|
|
|
|
+ private const string dnsServerName = "reliable-dns-server-in-hosts";
|
|
|
|
+ private static readonly IPAddress? dnsServerIp = Dns.GetHostEntry(dnsServerName).AddressList.FirstOrDefault();
|
|
|
|
+ private static readonly LookupClient lookup = new(dnsServerIp);
|
|
|
|
+
|
|
|
|
+ public override async Task OnConnectedAsync(ConnectionContext connection)
|
|
|
|
+ {
|
|
|
|
+ var requestStream = connection.Transport.Input;
|
|
|
|
+ var responseStream = connection.Transport.Output;
|
|
|
|
+
|
|
|
|
+ var firstLine = await ReadLineAsync(requestStream);
|
|
|
|
+ if (firstLine == null)
|
|
|
|
+ {
|
|
|
|
+ connection.Abort(new ConnectionAbortedException("Canceled: First line rad fail"));
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var firstLineParts = firstLine.Split(' ', 3);
|
|
|
|
+ if (firstLineParts.Length < 3)
|
|
|
|
+ {
|
|
|
|
+ connection.Abort(new ConnectionAbortedException("Canceled: First line bad"));
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var method = firstLineParts[0];
|
|
|
|
+ var url = firstLineParts[1];
|
|
|
|
+ var ver = firstLineParts[2];
|
|
|
|
+
|
|
|
|
+ var uri = new Uri(url);
|
|
|
|
+ var targetHost = uri.Host;
|
|
|
|
+
|
|
|
|
+ var result = await lookup.QueryAsync(targetHost, QueryType.A);
|
|
|
|
+ var record = result.Answers.ARecords().FirstOrDefault();
|
|
|
|
+ var ip = record?.Address;
|
|
|
|
+
|
|
|
|
+ var tcpClient = new TcpClient();
|
|
|
|
+ await tcpClient.ConnectAsync(new IPEndPoint(ip, 443));
|
|
|
|
+ var ssl = new SslStream(tcpClient.GetStream());
|
|
|
|
+ var sslOptions = new SslClientAuthenticationOptions
|
|
|
|
+ {
|
|
|
|
+ TargetHost = string.Empty, // Leave this empty to avoid sending SNI
|
|
|
|
+ RemoteCertificateValidationCallback = (o, certificate, chain, errors) => VerifyServerCert(targetHost, certificate, chain, errors),
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ await ssl.AuthenticateAsClientAsync(sslOptions, new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token);
|
|
|
|
+ ssl.Write(Encoding.ASCII.GetBytes($"{method} {uri.PathAndQuery} {ver}\r\n"));
|
|
|
|
+
|
|
|
|
+ var outgoing = requestStream.CopyToAsync(ssl);
|
|
|
|
+ var inbound = ssl.CopyToAsync(responseStream);
|
|
|
|
+
|
|
|
|
+ await Task.WhenAll(outgoing, inbound);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private async Task<string?> ReadLineAsync(PipeReader requestStream)
|
|
|
|
+ {
|
|
|
|
+ while (true)
|
|
|
|
+ {
|
|
|
|
+ var readResult = await requestStream.ReadAsync();
|
|
|
|
+ if (readResult.IsCanceled) return null;
|
|
|
|
+
|
|
|
|
+ var (seq, exm) = ExtractLine(readResult.Buffer, out var line);
|
|
|
|
+ requestStream.AdvanceTo(seq, exm);
|
|
|
|
+ if (line != null) return line;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private (SequencePosition pos, SequencePosition exm) ExtractLine(ReadOnlySequence<byte> buffer, out string? line)
|
|
|
|
+ {
|
|
|
|
+ var reader = new SequenceReader<byte>(buffer);
|
|
|
|
+ if (reader.TryReadTo(out ReadOnlySpan<byte> span, "\r\n"u8))
|
|
|
|
+ {
|
|
|
|
+ if (span.Length > 4096) throw new InvalidDataException("Too long for line");
|
|
|
|
+ line = Encoding.ASCII.GetString(span);
|
|
|
|
+ return (reader.Position, reader.Position);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ line = null;
|
|
|
|
+ return (buffer.Start, buffer.End);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private bool VerifyServerCert(string targetHost, X509Certificate certificate, X509Chain? chain, SslPolicyErrors errs)
|
|
|
|
+ {
|
|
|
|
+ if (errs == SslPolicyErrors.None) return true;
|
|
|
|
+
|
|
|
|
+ if (errs != SslPolicyErrors.RemoteCertificateNameMismatch) return false;
|
|
|
|
+
|
|
|
|
+ if (certificate is not X509Certificate2 cert2) return false;
|
|
|
|
+
|
|
|
|
+ // 比较证书名称和主机名称
|
|
|
|
+ var certName = cert2.GetNameInfo(X509NameType.DnsName, false);
|
|
|
|
+ if (certName.StartsWith("*."))
|
|
|
|
+ {
|
|
|
|
+ if (!targetHost.EndsWith(certName[2..], StringComparison.OrdinalIgnoreCase)) return false;
|
|
|
|
+ }
|
|
|
|
+ else if (!certName.Equals(targetHost, StringComparison.OrdinalIgnoreCase)) return false;
|
|
|
|
+
|
|
|
|
+ // 验证证书的有效期
|
|
|
|
+ if (DateTime.Now < cert2.NotBefore || DateTime.Now > cert2.NotAfter) return false;
|
|
|
|
+
|
|
|
|
+ // 构建证书链
|
|
|
|
+ if (chain == null) return false;
|
|
|
|
+
|
|
|
|
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // 不检查吊销,太耗时了
|
|
|
|
+ chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
|
|
|
|
+ chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 10);
|
|
|
|
+ chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
|
|
|
|
+
|
|
|
|
+ var isValidChain = chain.Build(cert2);
|
|
|
|
+ if (isValidChain) return true;
|
|
|
|
+
|
|
|
|
+ foreach (X509ChainStatus chainStatus in chain.ChainStatus)
|
|
|
|
+ {
|
|
|
|
+ // 仅处理会影响安全性的错误状态
|
|
|
|
+ if (chainStatus.Status == X509ChainStatusFlags.RevocationStatusUnknown ||
|
|
|
|
+ chainStatus.Status == X509ChainStatusFlags.OfflineRevocation ||
|
|
|
|
+ chainStatus.Status == X509ChainStatusFlags.NoError)
|
|
|
|
+ {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 其他任何错误状态都认为证书无效
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+}
|