fleet: count only admitted peers against the session limit so silent connections cannot lock the roster out
This commit is contained in:
1 parent
fee236fe56
commit
4a9608ead4
2 files changed
+91
-4
No files matched your search
@@ -27,7 +27,15 @@ public sealed class PeerListenerOptions
|
|||||||
|
|
||||||
public int MaxFrameBytes { get; init; } = 8 << 20;
|
public int MaxFrameBytes { get; init; } = 8 << 20;
|
||||||
|
|
||||||
public int MaxSessions { get; init; } = 8;
|
/// <summary>
|
||||||
|
/// 同时在线的会话数上限,只数已经通过敲门的对端。每个拨号方每个地址族一个会话,所以要容得下「名册里的节点数 × 2」。
|
||||||
|
/// 之前这个上限连还没敲门的连接一起数:节点多起来之后,已建立的会话把名额占满,后到的拨号方(哪怕在名册里)一连上就被断开,
|
||||||
|
/// 而且一直连不上,重试只会得到同样的结果。
|
||||||
|
/// </summary>
|
||||||
|
public int MaxSessions { get; init; } = 64;
|
||||||
|
|
||||||
|
/// <summary>还没通过敲门的连接数上限(陌生人、扫描器、还在握手的):敲门有读超时,占着名额的时间有限,但它们不能挤占已通过敲门的会话。</summary>
|
||||||
|
public int MaxPending { get; init; } = 64;
|
||||||
|
|
||||||
/// <summary>敲门、TLS、挑战应答整个握手的上限。线路差的时候(RTT 好几秒、还丢包)握手本来就要很久,10 秒做不完就会一直连不上;敲门已经挡住了不认识的连接,放宽不会放大被占连接的风险。</summary>
|
/// <summary>敲门、TLS、挑战应答整个握手的上限。线路差的时候(RTT 好几秒、还丢包)握手本来就要很久,10 秒做不完就会一直连不上;敲门已经挡住了不认识的连接,放宽不会放大被占连接的风险。</summary>
|
||||||
public TimeSpan HandshakeTimeout { get; init; } = PeerTimings.Handshake;
|
public TimeSpan HandshakeTimeout { get; init; } = PeerTimings.Handshake;
|
||||||
@@ -44,6 +52,7 @@ public sealed class PeerListener
|
|||||||
private readonly INodeLog _log;
|
private readonly INodeLog _log;
|
||||||
private readonly TimeProvider _time;
|
private readonly TimeProvider _time;
|
||||||
private int _sessions;
|
private int _sessions;
|
||||||
|
private int _pending;
|
||||||
|
|
||||||
public PeerListener(PeerListenerOptions options, Func<PeerSessionInfo, IRpcTransport, CancellationToken, Task> onSession, INodeLog log, TimeProvider time)
|
public PeerListener(PeerListenerOptions options, Func<PeerSessionInfo, IRpcTransport, CancellationToken, Task> onSession, INodeLog log, TimeProvider time)
|
||||||
{
|
{
|
||||||
@@ -74,9 +83,9 @@ public sealed class PeerListener
|
|||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
TcpClient client = await listener.AcceptTcpClientAsync(ct);
|
TcpClient client = await listener.AcceptTcpClientAsync(ct);
|
||||||
if (Interlocked.Increment(ref _sessions) > _options.MaxSessions)
|
if (Interlocked.Increment(ref _pending) > _options.MaxPending)
|
||||||
{
|
{
|
||||||
Interlocked.Decrement(ref _sessions);
|
Interlocked.Decrement(ref _pending);
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -97,6 +106,7 @@ public sealed class PeerListener
|
|||||||
private async Task HandleAsync(TcpClient client, CancellationToken ct)
|
private async Task HandleAsync(TcpClient client, CancellationToken ct)
|
||||||
{
|
{
|
||||||
bool admitted = false;
|
bool admitted = false;
|
||||||
|
bool counted = false;
|
||||||
string phase = "knock";
|
string phase = "knock";
|
||||||
long started = _time.GetTimestamp();
|
long started = _time.GetTimestamp();
|
||||||
EndPoint? remote = client.Client.RemoteEndPoint;
|
EndPoint? remote = client.Client.RemoteEndPoint;
|
||||||
@@ -113,6 +123,15 @@ public sealed class PeerListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
admitted = true;
|
admitted = true;
|
||||||
|
Interlocked.Decrement(ref _pending);
|
||||||
|
if (Interlocked.Increment(ref _sessions) > _options.MaxSessions)
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _sessions);
|
||||||
|
_log.Warn("peer session limit reached", ("remote", remote), ("limit", _options.MaxSessions));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
counted = true;
|
||||||
phase = "tls";
|
phase = "tls";
|
||||||
_log.Info("peer knock accepted", ("remote", remote), ("phase", phase));
|
_log.Info("peer knock accepted", ("remote", remote), ("phase", phase));
|
||||||
var tls = new SslStream(stream, leaveInnerStreamOpen: false);
|
var tls = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||||
@@ -152,9 +171,17 @@ public sealed class PeerListener
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
|
if (!admitted)
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counted)
|
||||||
|
{
|
||||||
Interlocked.Decrement(ref _sessions);
|
Interlocked.Decrement(ref _sessions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<PeerSessionInfo?> HandshakeAsync(TlsFrameTransport transport, EndPoint? remote, CancellationToken ct)
|
private async Task<PeerSessionInfo?> HandshakeAsync(TlsFrameTransport transport, EndPoint? remote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ public sealed class LinkTests
|
|||||||
private readonly CancellationTokenSource _stop = new();
|
private readonly CancellationTokenSource _stop = new();
|
||||||
private readonly NodeIdentity _identity;
|
private readonly NodeIdentity _identity;
|
||||||
|
|
||||||
public PeerFixture(Func<string, byte[]?> lookup, Func<PeerSessionInfo, IRpcTransport, CancellationToken, Task>? onSession = null)
|
public PeerFixture(Func<string, byte[]?> lookup, Func<PeerSessionInfo, IRpcTransport, CancellationToken, Task>? onSession = null, int maxSessions = 64)
|
||||||
{
|
{
|
||||||
Certificate = NodeCertificate.LoadPfx(NodeCertificate.CreateSelfSignedPfx("agent", TimeSpan.FromDays(30), TimeProvider.System));
|
Certificate = NodeCertificate.LoadPfx(NodeCertificate.CreateSelfSignedPfx("agent", TimeSpan.FromDays(30), TimeProvider.System));
|
||||||
_identity = NodeIdentity.FromPkcs8(Certificate.GetECDsaPrivateKey()!.ExportPkcs8PrivateKey());
|
_identity = NodeIdentity.FromPkcs8(Certificate.GetECDsaPrivateKey()!.ExportPkcs8PrivateKey());
|
||||||
@@ -314,6 +314,7 @@ public sealed class LinkTests
|
|||||||
Certificate = Certificate,
|
Certificate = Certificate,
|
||||||
LookupPeerKey = lookup,
|
LookupPeerKey = lookup,
|
||||||
Knock = new KnockGate(_identity, () => KnockPeers, TimeProvider.System),
|
Knock = new KnockGate(_identity, () => KnockPeers, TimeProvider.System),
|
||||||
|
MaxSessions = maxSessions,
|
||||||
},
|
},
|
||||||
onSession ?? ((_, transport, ct) => KeepOpenAsync(transport, ct)),
|
onSession ?? ((_, transport, ct) => KeepOpenAsync(transport, ct)),
|
||||||
NullNodeLog.Instance, TimeProvider.System);
|
NullNodeLog.Instance, TimeProvider.System);
|
||||||
@@ -462,6 +463,65 @@ public sealed class LinkTests
|
|||||||
await ok.DisposeAsync();
|
await ok.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>名额只数通过敲门的会话:一堆还没敲门的连接占着,名册里的拨号方照样连得上(之前它们共用一个上限,节点多了之后后到的拨号方被永远拒绝)。</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task PeerListener_SessionLimitCountsOnlyAdmittedPeers_SoSilentConnectionsCannotLockOutTheRoster()
|
||||||
|
{
|
||||||
|
using NodeIdentity first = NodeIdentity.Generate();
|
||||||
|
using NodeIdentity second = NodeIdentity.Generate();
|
||||||
|
using NodeIdentity third = NodeIdentity.Generate();
|
||||||
|
await using var agent = new PeerFixture(id => id == "dlr_1" ? first.PublicKeySpki : id == "dlr_2" ? second.PublicKeySpki : third.PublicKeySpki, maxSessions: 2);
|
||||||
|
agent.KnockPeers.AddRange([first.PublicKeySpki, second.PublicKeySpki, third.PublicKeySpki]);
|
||||||
|
await agent.Listener.Started.Task;
|
||||||
|
|
||||||
|
// 20 个什么都不说的连接:占着待敲门的名额,但不占会话名额
|
||||||
|
var silent = new List<TcpClient>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
var client = new TcpClient();
|
||||||
|
await client.ConnectAsync(IPAddress.Loopback, agent.Listener.Port);
|
||||||
|
silent.Add(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
(_, TlsFrameTransport a) = await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, first, "dlr_1", null, 1 << 20, TimeProvider.System, default);
|
||||||
|
(_, TlsFrameTransport b) = await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, second, "dlr_2", null, 1 << 20, TimeProvider.System, default);
|
||||||
|
|
||||||
|
// 会话名额(2)满了:第三个在名册里的拨号方被拒绝,一个会话结束之后又能连上
|
||||||
|
await Assert.ThrowsAnyAsync<Exception>(async () =>
|
||||||
|
{
|
||||||
|
(_, TlsFrameTransport refused) = await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, third, "dlr_3", null, 1 << 20, TimeProvider.System, default);
|
||||||
|
await refused.DisposeAsync();
|
||||||
|
});
|
||||||
|
await a.DisposeAsync();
|
||||||
|
bool reconnected = false;
|
||||||
|
for (int attempt = 0; attempt < 50 && !reconnected; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(_, TlsFrameTransport again) = await PeerConnector.ConnectAsync("127.0.0.1", agent.Listener.Port, agent.PublicKey, third, "dlr_3", null, 1 << 20, TimeProvider.System, default);
|
||||||
|
await again.DisposeAsync();
|
||||||
|
reconnected = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or System.Security.Authentication.AuthenticationException or InvalidOperationException)
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(reconnected);
|
||||||
|
await b.DisposeAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (TcpClient client in silent)
|
||||||
|
{
|
||||||
|
client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PeerListener_WritesNothingToStrangers_NotEvenATlsServerHello()
|
public async Task PeerListener_WritesNothingToStrangers_NotEvenATlsServerHello()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user