diff --git a/backend/agent/src/MoShiro.Agent/Peer/PeerHub.cs b/backend/agent/src/MoShiro.Agent/Peer/PeerHub.cs
index 69812ac..67769a9 100644
--- a/backend/agent/src/MoShiro.Agent/Peer/PeerHub.cs
+++ b/backend/agent/src/MoShiro.Agent/Peer/PeerHub.cs
@@ -78,8 +78,9 @@ public sealed class PeerHub
{
private const int DedupeWindow = 1024;
- /// 超过这么久没有收到 Probe 的任何帧就认为会话已死(Probe 每 30 秒至少发一次时钟同步)。
- private static readonly TimeSpan IdleLimit = TimeSpan.FromSeconds(75);
+ /// 超过这么久没有收到 Probe 的任何帧就认为会话已死(Probe 每 30 秒至少发一次时钟同步)。
+ /// 要比线路可能的中断长得多:差的线路上几十秒甚至一两分钟的黑洞时有发生(TCP RTT 能到 30 秒),75 秒就关会话会把一次中断变成几分钟的数据空白(要重新握手,而握手在差线路上也慢)。
+ private static readonly TimeSpan IdleLimit = TimeSpan.FromSeconds(180);
private readonly RpcRouter _router;
private readonly SessionRegistry _sessions;
diff --git a/backend/fleet/src/Fleet.Link/Peer.cs b/backend/fleet/src/Fleet.Link/Peer.cs
index ba94a2b..7a20f7a 100644
--- a/backend/fleet/src/Fleet.Link/Peer.cs
+++ b/backend/fleet/src/Fleet.Link/Peer.cs
@@ -29,7 +29,8 @@ public sealed class PeerListenerOptions
public int MaxSessions { get; init; } = 8;
- public TimeSpan HandshakeTimeout { get; init; } = TimeSpan.FromSeconds(10);
+ /// 敲门、TLS、挑战应答整个握手的上限。线路差的时候(RTT 好几秒、还丢包)握手本来就要很久,10 秒做不完就会一直连不上;敲门已经挡住了不认识的连接,放宽不会放大被占连接的风险。
+ public TimeSpan HandshakeTimeout { get; init; } = PeerTimings.Handshake;
}
///
@@ -209,7 +210,8 @@ public sealed class PeerListener
}
/// 发起点对点会话(Probe 一侧):按 SPKI 指纹固定对端证书,用身份私钥签名,派生同样的数据面凭据。
-/// 点对点会话的套接字:开启 TCP keepalive(空闲 15 秒后每 5 秒探测,连续 3 次无响应就断开,约 30 秒),主机丢失或路径被切断时连接不会一直挂着。
+/// 点对点会话的套接字:开启 TCP keepalive(空闲 30 秒后每 10 秒探测,连续 6 次无响应才断开,约 90 秒),主机真的没了的时候连接不会一直挂着;
+/// 但线路短暂中断(几十秒的黑洞)不能把会话杀掉——线路中断本来就是要测的东西,会话得扛过去,恢复之后接着测。
internal static class PeerSockets
{
public static void EnableKeepAlive(Socket socket)
@@ -217,9 +219,9 @@ internal static class PeerSockets
try
{
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
- socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 15);
- socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 5);
- socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 3);
+ socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 30);
+ socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 10);
+ socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 6);
}
catch (Exception ex) when (ex is SocketException or ArgumentException or PlatformNotSupportedException)
{
@@ -228,6 +230,13 @@ internal static class PeerSockets
}
}
+/// 点对点会话的时间常数,Probe 和 Agent 两侧共用。
+public static class PeerTimings
+{
+ /// 建立会话(连接、敲门、TLS、挑战应答)的总时限。
+ public static readonly TimeSpan Handshake = TimeSpan.FromSeconds(30);
+}
+
public static class PeerConnector
{
public static async Task<(PeerSessionInfo Info, TlsFrameTransport Transport)> ConnectAsync(
@@ -240,7 +249,7 @@ public static class PeerConnector
PeerSockets.EnableKeepAlive(client.Client);
try
{
- using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10), time);
+ using var timeout = new CancellationTokenSource(PeerTimings.Handshake, time);
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeout.Token);
await client.ConnectAsync(host, port, linked.Token);
Stream stream = client.GetStream();
diff --git a/backend/probe/src/MoShiro.Probe/Link/AgentConnection.cs b/backend/probe/src/MoShiro.Probe/Link/AgentConnection.cs
index dcc3c74..43ede3e 100644
--- a/backend/probe/src/MoShiro.Probe/Link/AgentConnection.cs
+++ b/backend/probe/src/MoShiro.Probe/Link/AgentConnection.cs
@@ -47,7 +47,8 @@ public sealed class AgentConnection
public async Task RunAsync(CancellationToken ct)
{
- var backoff = new Backoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(20));
+ // 重连的间隔别拉太长:每次尝试本来就要等好几秒(握手上限 30 秒),线路一恢复要尽快接上;带抖动,不会和别的节点撞在一起
+ var backoff = new Backoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(8));
while (!ct.IsCancellationRequested)
{
try