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
+92
-5
No files matched your search
@@ -27,7 +27,15 @@ public sealed class PeerListenerOptions
|
||||
|
||||
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>
|
||||
public TimeSpan HandshakeTimeout { get; init; } = PeerTimings.Handshake;
|
||||
@@ -44,6 +52,7 @@ public sealed class PeerListener
|
||||
private readonly INodeLog _log;
|
||||
private readonly TimeProvider _time;
|
||||
private int _sessions;
|
||||
private int _pending;
|
||||
|
||||
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)
|
||||
{
|
||||
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();
|
||||
continue;
|
||||
}
|
||||
@@ -97,6 +106,7 @@ public sealed class PeerListener
|
||||
private async Task HandleAsync(TcpClient client, CancellationToken ct)
|
||||
{
|
||||
bool admitted = false;
|
||||
bool counted = false;
|
||||
string phase = "knock";
|
||||
long started = _time.GetTimestamp();
|
||||
EndPoint? remote = client.Client.RemoteEndPoint;
|
||||
@@ -113,6 +123,15 @@ public sealed class PeerListener
|
||||
}
|
||||
|
||||
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";
|
||||
_log.Info("peer knock accepted", ("remote", remote), ("phase", phase));
|
||||
var tls = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||
@@ -152,7 +171,15 @@ public sealed class PeerListener
|
||||
finally
|
||||
{
|
||||
client.Dispose();
|
||||
Interlocked.Decrement(ref _sessions);
|
||||
if (!admitted)
|
||||
{
|
||||
Interlocked.Decrement(ref _pending);
|
||||
}
|
||||
|
||||
if (counted)
|
||||
{
|
||||
Interlocked.Decrement(ref _sessions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ public sealed class LinkTests
|
||||
private readonly CancellationTokenSource _stop = new();
|
||||
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));
|
||||
_identity = NodeIdentity.FromPkcs8(Certificate.GetECDsaPrivateKey()!.ExportPkcs8PrivateKey());
|
||||
@@ -314,6 +314,7 @@ public sealed class LinkTests
|
||||
Certificate = Certificate,
|
||||
LookupPeerKey = lookup,
|
||||
Knock = new KnockGate(_identity, () => KnockPeers, TimeProvider.System),
|
||||
MaxSessions = maxSessions,
|
||||
},
|
||||
onSession ?? ((_, transport, ct) => KeepOpenAsync(transport, ct)),
|
||||
NullNodeLog.Instance, TimeProvider.System);
|
||||
@@ -462,6 +463,65 @@ public sealed class LinkTests
|
||||
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]
|
||||
public async Task PeerListener_WritesNothingToStrangers_NotEvenATlsServerHello()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user