[CI] [SKIP TESTS] Add detailed Probe-Agent connection diagnostics
This commit is contained in:
1 parent
0fd38b2367
commit
a7ebf61551
16 files changed
+376
-51
No files matched your search
@@ -0,0 +1,8 @@
|
||||
namespace Fleet.Core;
|
||||
|
||||
/// <summary>Adds stable diagnostic context to every event in one operation.</summary>
|
||||
public sealed class ContextNodeLog(INodeLog inner, params (string Key, object? Value)[] context) : INodeLog
|
||||
{
|
||||
public void Write(NodeLogLevel level, string message, params (string Key, object? Value)[] fields) =>
|
||||
inner.Write(level, message, [.. context, .. fields]);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Fleet.Hosting;
|
||||
public sealed class BufferedNodeLog : INodeLog, INodeLogBatchSource
|
||||
{
|
||||
private const int Capacity = 256;
|
||||
private const int MaxFields = 12;
|
||||
private const int MaxFields = 20;
|
||||
private const int MaxMessageLength = 512;
|
||||
private const int MaxFieldValueLength = 256;
|
||||
private const int MaxBatchBytes = 8 << 10;
|
||||
|
||||
@@ -96,6 +96,10 @@ public sealed class PeerListener
|
||||
|
||||
private async Task HandleAsync(TcpClient client, CancellationToken ct)
|
||||
{
|
||||
bool admitted = false;
|
||||
string phase = "knock";
|
||||
long started = _time.GetTimestamp();
|
||||
EndPoint? remote = client.Client.RemoteEndPoint;
|
||||
try
|
||||
{
|
||||
client.NoDelay = true;
|
||||
@@ -108,19 +112,27 @@ public sealed class PeerListener
|
||||
return;
|
||||
}
|
||||
|
||||
admitted = true;
|
||||
phase = "tls";
|
||||
_log.Info("peer knock accepted", ("remote", remote), ("phase", phase));
|
||||
var tls = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||
await tls.AuthenticateAsServerAsync(
|
||||
new SslServerAuthenticationOptions { ServerCertificate = _options.Certificate, ClientCertificateRequired = false, EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13 },
|
||||
linked.Token);
|
||||
phase = "peer_auth";
|
||||
_log.Info("peer TLS established", ("remote", remote), ("phase", phase), ("tls", tls.SslProtocol));
|
||||
var transport = new TlsFrameTransport(tls, _options.MaxFrameBytes);
|
||||
PeerSessionInfo? info = await HandshakeAsync(transport, client.Client.RemoteEndPoint, linked.Token);
|
||||
if (info is null)
|
||||
{
|
||||
_log.Warn("peer authentication rejected", ("remote", remote), ("phase", phase));
|
||||
await transport.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_log.Info("peer session", ("probe", info.PeerId), ("result", "established"));
|
||||
phase = "control_session";
|
||||
_log.Info("peer session", ("probe", info.PeerId), ("result", "established"), ("remote", remote),
|
||||
("elapsed_ms", (long)_time.GetElapsedTime(started).TotalMilliseconds));
|
||||
await using (transport)
|
||||
{
|
||||
await _onSession(info, transport, ct);
|
||||
@@ -128,7 +140,14 @@ public sealed class PeerListener
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or System.Security.Authentication.AuthenticationException or OperationCanceledException or SocketException or ObjectDisposedException)
|
||||
{
|
||||
// 握手失败或连接断开:这个会话结束,监听继续
|
||||
// Invalid knocks stay silent. Only peers that passed admission get diagnostics.
|
||||
if (admitted && !ct.IsCancellationRequested)
|
||||
{
|
||||
_log.Warn("peer control connection failed", ("remote", remote), ("phase", phase),
|
||||
("elapsed_ms", (long)_time.GetElapsedTime(started).TotalMilliseconds),
|
||||
("exception", ex.GetType().Name), ("err", ex.Message),
|
||||
("socket_error", ex is SocketException socket ? socket.SocketErrorCode : null));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -239,25 +258,37 @@ public static class PeerTimings
|
||||
|
||||
public static class PeerConnector
|
||||
{
|
||||
public static Task<(PeerSessionInfo Info, TlsFrameTransport Transport)> ConnectAsync(
|
||||
string host, int port, byte[] agentPublicKey, NodeIdentity identity, string peerId, string? version, int maxFrameBytes, TimeProvider time, CancellationToken ct) =>
|
||||
ConnectAsync(host, port, agentPublicKey, identity, peerId, version, maxFrameBytes, time, NullNodeLog.Instance, ct);
|
||||
|
||||
public static async Task<(PeerSessionInfo Info, TlsFrameTransport Transport)> ConnectAsync(
|
||||
string host, int port, byte[] agentPublicKey, NodeIdentity identity, string peerId, string? version, int maxFrameBytes, TimeProvider time, CancellationToken ct)
|
||||
string host, int port, byte[] agentPublicKey, NodeIdentity identity, string peerId, string? version, int maxFrameBytes, TimeProvider time, INodeLog log, CancellationToken ct)
|
||||
{
|
||||
// 证书按公钥的指纹固定;敲门密钥由双方身份密钥派生
|
||||
byte[] expectedSpkiSha256 = NodeIdentity.Fingerprint(agentPublicKey);
|
||||
TcpClient client = IPAddress.TryParse(host, out IPAddress? address) ? new TcpClient(address.AddressFamily) : new TcpClient();
|
||||
client.NoDelay = true;
|
||||
PeerSockets.EnableKeepAlive(client.Client);
|
||||
string phase = "tcp_connect";
|
||||
long started = time.GetTimestamp();
|
||||
try
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(PeerTimings.Handshake, time);
|
||||
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeout.Token);
|
||||
await client.ConnectAsync(host, port, linked.Token);
|
||||
log.Info("peer TCP connected", ("remote", client.Client.RemoteEndPoint), ("local", client.Client.LocalEndPoint),
|
||||
("phase", phase), ("elapsed_ms", (long)time.GetElapsedTime(started).TotalMilliseconds));
|
||||
phase = "knock_send";
|
||||
Stream stream = client.GetStream();
|
||||
await stream.WriteAsync(Knock.Build(Knock.DeriveKey(identity, agentPublicKey), time.GetUtcNow().ToUnixTimeMilliseconds()), linked.Token);
|
||||
phase = "tls";
|
||||
var tls = new SslStream(
|
||||
stream, leaveInnerStreamOpen: false,
|
||||
(_, certificate, _, _) => certificate is not null && SpkiMatches(certificate, expectedSpkiSha256));
|
||||
await tls.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { TargetHost = host }, linked.Token);
|
||||
log.Info("peer TLS established", ("phase", phase), ("tls", tls.SslProtocol), ("elapsed_ms", (long)time.GetElapsedTime(started).TotalMilliseconds));
|
||||
phase = "peer_auth";
|
||||
var transport = new TlsFrameTransport(tls, maxFrameBytes);
|
||||
|
||||
RpcFrame? first = await transport.ReceiveAsync(linked.Token);
|
||||
@@ -288,10 +319,18 @@ public static class PeerConnector
|
||||
throw new InvalidOperationException("session id mismatch: the key agreement did not produce the same credentials");
|
||||
}
|
||||
|
||||
log.Info("peer authentication established", ("phase", phase), ("elapsed_ms", (long)time.GetElapsedTime(started).TotalMilliseconds));
|
||||
return (new PeerSessionInfo(peerId, credentials, client.Client.RemoteEndPoint), transport);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!ct.IsCancellationRequested)
|
||||
{
|
||||
log.Warn("peer control handshake failed", ("phase", phase), ("remote", host), ("port", port),
|
||||
("elapsed_ms", (long)time.GetElapsedTime(started).TotalMilliseconds), ("timeout_ms", PeerTimings.Handshake.TotalMilliseconds),
|
||||
("exception", ex.GetType().Name), ("err", ex.Message),
|
||||
("socket_error", ex is SocketException socket ? socket.SocketErrorCode : null));
|
||||
}
|
||||
client.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -364,13 +364,18 @@ public sealed class LinkTests
|
||||
agent.KnockPeers.Add(probe.PublicKeySpki);
|
||||
await agent.Listener.Started.Task;
|
||||
|
||||
(PeerSessionInfo client, TlsFrameTransport transport) = await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, probe, "prb_1", null, 1 << 20, TimeProvider.System, default);
|
||||
var logs = new Fleet.Hosting.BufferedNodeLog(NullNodeLog.Instance, TimeProvider.System);
|
||||
var scoped = new ContextNodeLog(logs, ("attempt_id", "test-attempt"));
|
||||
(PeerSessionInfo client, TlsFrameTransport transport) = await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, probe, "prb_1", null, 1 << 20, TimeProvider.System, scoped, default);
|
||||
await using (transport)
|
||||
{
|
||||
PeerSessionInfo server = await seen.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("prb_1", server.PeerId);
|
||||
Assert.Equal(client.Credentials.Key, server.Credentials.Key);
|
||||
Assert.Equal(client.Credentials.SessionId, server.Credentials.SessionId);
|
||||
NodeLogBatch batch = logs.TakeBatch(32);
|
||||
Assert.Equal(new[] { "tcp_connect", "tls", "peer_auth" }, batch.Records.Select(x => x.Fields["phase"]));
|
||||
Assert.All(batch.Records, entry => Assert.Equal("test-attempt", entry.Fields["attempt_id"]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,8 +388,12 @@ public sealed class LinkTests
|
||||
agent.KnockPeers.AddRange([probe.PublicKeySpki, impostor.PublicKeySpki]); // 敲门通过之后,名册外的 ID 和伪造的签名仍然被拒绝
|
||||
await agent.Listener.Started.Task;
|
||||
|
||||
var logs = new Fleet.Hosting.BufferedNodeLog(NullNodeLog.Instance, TimeProvider.System);
|
||||
InvalidOperationException unknown = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, probe, "prb_unknown", null, 1 << 20, TimeProvider.System, default));
|
||||
await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, probe, "prb_unknown", null, 1 << 20, TimeProvider.System, logs, default));
|
||||
NodeLogRecord failure = Assert.Single(logs.TakeBatch(32).Records, entry => entry.Message == "peer control handshake failed");
|
||||
Assert.Equal("peer_auth", failure.Fields["phase"]);
|
||||
Assert.Equal("InvalidOperationException", failure.Fields["exception"]);
|
||||
InvalidOperationException forged = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, impostor, "prb_1", null, 1 << 20, TimeProvider.System, default));
|
||||
Assert.Equal(unknown.Message, forged.Message);
|
||||
|
||||
Reference in new issue
Block a user