chore: initial project import
This commit is contained in:
commit
3fc4e5ca53
347 files changed
+56172
No files matched your search
@@ -0,0 +1,337 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Monitor.Protocol;
|
||||
|
||||
namespace Monitor.Measurement;
|
||||
|
||||
/// <summary>拨号方的一条数据面 TCP 连接(已经完成 TLS 和首帧认证)。</summary>
|
||||
public sealed class TcpLink : IDisposable
|
||||
{
|
||||
private readonly TcpClient _client;
|
||||
|
||||
private TcpLink(TcpClient client, Stream stream, long tcpUs, long? tlsUs)
|
||||
{
|
||||
_client = client;
|
||||
Socket = client.Client;
|
||||
Stream = stream;
|
||||
TcpUs = tcpUs;
|
||||
TlsUs = tlsUs;
|
||||
}
|
||||
|
||||
public Stream Stream { get; }
|
||||
|
||||
/// <summary>底层套接字(内核采样用)。</summary>
|
||||
public Socket Socket { get; }
|
||||
|
||||
public long TcpUs { get; }
|
||||
|
||||
public long? TlsUs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 连接 → (TLS)→ 认证首帧。<paramref name="sendAuth"/> 为 false 时只测量建立连接的耗时(connect)。
|
||||
/// 对端证书按 <see cref="BlockEnv.PeerSpkiSha256"/> 固定;没有指纹(目标任务,目标是任意地址)就只测握手耗时、不校验证书。
|
||||
/// 失败时抛出 <see cref="SocketException"/>、<see cref="AuthenticationException"/>、<see cref="IOException"/> 或超时的 <see cref="OperationCanceledException"/>。
|
||||
/// </summary>
|
||||
public static async Task<TcpLink> OpenAsync(BlockEnv env, int port, bool tls, uint streamId, bool sendAuth, CancellationToken ct)
|
||||
{
|
||||
var client = new TcpClient { NoDelay = true };
|
||||
try
|
||||
{
|
||||
long start = env.Clock.NowUs();
|
||||
await client.ConnectAsync(env.PeerAddress!, port, ct);
|
||||
long tcpUs = env.Clock.NowUs() - start;
|
||||
Stream stream = client.GetStream();
|
||||
long? tlsUs = null;
|
||||
if (tls)
|
||||
{
|
||||
long tlsStart = env.Clock.NowUs();
|
||||
byte[]? pin = env.PeerSpkiSha256;
|
||||
#pragma warning disable CA5359 // 没有指纹时只测量握手耗时(目标任务的目标是任意地址)
|
||||
var ssl = new SslStream(stream, false, (_, certificate, _, _) => pin is null || (certificate is not null && Pinned(certificate, pin)));
|
||||
#pragma warning restore CA5359
|
||||
await ssl.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { TargetHost = env.PeerAddress!.ToString() }, ct);
|
||||
stream = ssl;
|
||||
tlsUs = env.Clock.NowUs() - tlsStart;
|
||||
}
|
||||
|
||||
if (sendAuth)
|
||||
{
|
||||
DataSession session = env.Session() ?? throw new InvalidOperationException("no data session");
|
||||
byte[] auth = new byte[TcpWire.AuthBytes];
|
||||
Span<byte> nonce = stackalloc byte[TcpWire.NonceBytes];
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
TcpWire.WriteAuth(auth, session.SessionId, streamId, nonce, session.Key);
|
||||
await stream.WriteAsync(auth, ct);
|
||||
}
|
||||
|
||||
return new TcpLink(client, stream, tcpUs, tlsUs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
client.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stream.Dispose();
|
||||
_client.Dispose();
|
||||
}
|
||||
|
||||
private static bool Pinned(X509Certificate certificate, byte[] expected)
|
||||
{
|
||||
X509Certificate2 full = certificate as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(certificate.GetRawCertData());
|
||||
return CryptographicOperations.FixedTimeEquals(SHA256.HashData(full.PublicKey.ExportSubjectPublicKeyInfo()), expected);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>监听方收到的一条已经认证的数据面 TCP 连接。</summary>
|
||||
public sealed class TcpConnection
|
||||
{
|
||||
public TcpConnection(Stream stream, Socket socket, DataSession session, uint streamId)
|
||||
{
|
||||
Stream = stream;
|
||||
Socket = socket;
|
||||
Session = session;
|
||||
StreamId = streamId;
|
||||
}
|
||||
|
||||
public Stream Stream { get; }
|
||||
|
||||
/// <summary>底层套接字(内核采样用)。</summary>
|
||||
public Socket Socket { get; }
|
||||
|
||||
public DataSession Session { get; }
|
||||
|
||||
public uint StreamId { get; }
|
||||
}
|
||||
|
||||
public delegate Task TcpResponder(TcpConnection connection, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// 监听方的 TCP 数据面:每个端口一个监听器。连接的第一个字节是 0x16 就先做 TLS(节点自己的证书),否则直接是明文。
|
||||
/// 随后是认证首帧(<c>"MN" | ver | session_id | stream_id | nonce | mac</c>,随机数只能用一次),通过之后交给按 <c>stream_id</c> 登记的处理函数。
|
||||
/// 首帧在限定时间内没有到达或者校验失败,直接关闭连接并计数。
|
||||
/// </summary>
|
||||
public sealed class TcpAcceptor
|
||||
{
|
||||
private static readonly TimeSpan s_authTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly IPAddress _bind;
|
||||
private readonly IReadOnlyList<int> _ports;
|
||||
private readonly X509Certificate2 _certificate;
|
||||
private readonly IDataSessions _sessions;
|
||||
private readonly DataPlaneCounters _counters;
|
||||
private readonly ConcurrentDictionary<uint, TcpResponder> _handlers = new();
|
||||
private readonly List<TcpListener> _listeners = [];
|
||||
|
||||
public TcpAcceptor(IPAddress bind, IReadOnlyList<int> ports, X509Certificate2 certificate, IDataSessions sessions, DataPlaneCounters counters)
|
||||
{
|
||||
_bind = bind;
|
||||
_ports = ports;
|
||||
_certificate = certificate;
|
||||
_sessions = sessions;
|
||||
_counters = counters;
|
||||
}
|
||||
|
||||
public IReadOnlyList<int> BoundPorts { get; private set; } = [];
|
||||
|
||||
public void Register(uint streamId, TcpResponder handler) => _handlers[streamId] = handler;
|
||||
|
||||
public void Unregister(uint streamId) => _handlers.TryRemove(streamId, out _);
|
||||
|
||||
/// <summary>开始监听(端口被占用等错误在这里抛出)。</summary>
|
||||
public void Bind()
|
||||
{
|
||||
var bound = new List<int>();
|
||||
foreach (int port in _ports)
|
||||
{
|
||||
var listener = new TcpListener(_bind, port);
|
||||
listener.Start(128);
|
||||
_listeners.Add(listener);
|
||||
bound.Add(((IPEndPoint)listener.LocalEndpoint).Port);
|
||||
}
|
||||
|
||||
BoundPorts = bound;
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(_listeners.Select(l => AcceptLoopAsync(l, ct)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (TcpListener listener in _listeners)
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync(TcpListener listener, CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
TcpClient client;
|
||||
try
|
||||
{
|
||||
client = await listener.AcceptTcpClientAsync(ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => HandleAsync(client, ct), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAsync(TcpClient client, CancellationToken ct)
|
||||
{
|
||||
using (client)
|
||||
{
|
||||
client.NoDelay = true;
|
||||
try
|
||||
{
|
||||
Stream stream = client.GetStream();
|
||||
using var authCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
authCts.CancelAfter(s_authTimeout);
|
||||
|
||||
byte[] first = new byte[1];
|
||||
if (await stream.ReadAsync(first, authCts.Token) != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stream = new PrefixedStream(stream, first[0]);
|
||||
if (first[0] == 0x16)
|
||||
{
|
||||
var ssl = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||
await ssl.AuthenticateAsServerAsync(new SslServerAuthenticationOptions { ServerCertificate = _certificate }, authCts.Token);
|
||||
stream = ssl;
|
||||
}
|
||||
|
||||
byte[] auth = new byte[TcpWire.AuthBytes];
|
||||
await stream.ReadExactlyAsync(auth, authCts.Token);
|
||||
if (!TcpWire.TryParseAuth(auth, out long sessionId, out uint streamId, out ReadOnlySpan<byte> nonce)
|
||||
|| _sessions.Find(sessionId) is not { } session
|
||||
|| !_handlers.TryGetValue(streamId, out TcpResponder? handler)
|
||||
|| !TcpWire.VerifyAuth(auth, session.Key)
|
||||
|| !session.UseNonce(nonce))
|
||||
{
|
||||
_counters.DropInvalid();
|
||||
return;
|
||||
}
|
||||
|
||||
await handler(new TcpConnection(stream, client.Client, session, streamId), ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or OperationCanceledException or ObjectDisposedException or AuthenticationException or InvalidDataException or EndOfStreamException)
|
||||
{
|
||||
// 连接被对端关闭、超时或者格式错误:直接结束这个连接
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>把已经读出的第一个字节放回流的开头(用于探测是 TLS 还是明文)。</summary>
|
||||
private sealed class PrefixedStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private byte? _prefix;
|
||||
|
||||
public PrefixedStream(Stream inner, byte prefix)
|
||||
{
|
||||
_inner = inner;
|
||||
_prefix = prefix;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => _inner.Flush();
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) => _inner.FlushAsync(cancellationToken);
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count));
|
||||
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_prefix is { } prefix)
|
||||
{
|
||||
buffer[0] = prefix;
|
||||
_prefix = null;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return _inner.Read(buffer);
|
||||
}
|
||||
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
return ValueTask.FromResult(0);
|
||||
}
|
||||
|
||||
if (_prefix is { } prefix)
|
||||
{
|
||||
buffer.Span[0] = prefix;
|
||||
_prefix = null;
|
||||
return ValueTask.FromResult(1);
|
||||
}
|
||||
|
||||
return _inner.ReadAsync(buffer, cancellationToken);
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
|
||||
|
||||
public override void Write(ReadOnlySpan<byte> buffer) => _inner.Write(buffer);
|
||||
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => _inner.WriteAsync(buffer, cancellationToken);
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_inner.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user