@decentnetwork/peer 0.1.68 → 0.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/peer.js +58 -36
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -82,6 +82,18 @@ const FRIEND_PING_INTERVAL_MS = readEnvInt("DECENT_FRIEND_PING_INTERVAL_MS", 400
|
|
|
82
82
|
// slow enough not to burn CPU on idle peers. Tunable via env.
|
|
83
83
|
const FRIEND_CONNECTION_LOOP_MS = readEnvInt("DECENT_FRIEND_CONNECTION_LOOP_MS", 250);
|
|
84
84
|
const FRIEND_TIMEOUT_MS = readEnvInt("DECENT_FRIEND_TIMEOUT_MS", 32000);
|
|
85
|
+
// A session whose keys we've PROVEN (decrypted ≥1 real packet from the peer) is
|
|
86
|
+
// NOT torn down at FRIEND_TIMEOUT_MS. A transient all-transport blackout — e.g.
|
|
87
|
+
// the GFW throttling a China<->abroad path for tens of seconds — must not trigger
|
|
88
|
+
// a re-handshake: an ASYMMETRIC re-handshake (one side re-keys while the other
|
|
89
|
+
// keeps its session) is exactly what desyncs the two sessions and produces the
|
|
90
|
+
// connect/disconnect CHURN that breaks the exit. Instead we keep the proven keys
|
|
91
|
+
// and keep probing over every transport; when the path recovers, a single packet
|
|
92
|
+
// resumes the SAME session with no re-handshake. Only after this much longer hard
|
|
93
|
+
// window do we treat the path as truly dead and re-handshake. (Unproven sessions
|
|
94
|
+
// — established but never carried a packet — still tear down at FRIEND_TIMEOUT_MS
|
|
95
|
+
// so a wedged handshake self-heals.)
|
|
96
|
+
const PROVEN_SESSION_HARD_TIMEOUT_MS = readEnvInt("DECENT_PROVEN_SESSION_HARD_TIMEOUT_MS", 180_000);
|
|
85
97
|
// How long the direct UDP path may go without an inbound packet before we
|
|
86
98
|
// release a physical-LAN lock (so a peer that left the LAN can recover onto the
|
|
87
99
|
// public/relay path). 12s ≈ 3 missed 4s keepalives — long enough to never trip
|
|
@@ -1318,7 +1330,7 @@ export class Peer {
|
|
|
1318
1330
|
// poison the UDP send-back path.
|
|
1319
1331
|
const fromTcp = this.#remoteIsTcp(remote);
|
|
1320
1332
|
if (!fromTcp) {
|
|
1321
|
-
matchedSession
|
|
1333
|
+
this.#adoptRemote(matchedSession, remote.address, remote.port);
|
|
1322
1334
|
}
|
|
1323
1335
|
else {
|
|
1324
1336
|
matchedSession.hasTcpRoute = true;
|
|
@@ -1392,13 +1404,7 @@ export class Peer {
|
|
|
1392
1404
|
state.hasTcpRoute = true;
|
|
1393
1405
|
}
|
|
1394
1406
|
else {
|
|
1395
|
-
|
|
1396
|
-
// retransmitted via the public/hairpin endpoint must not knock us off
|
|
1397
|
-
// a locked physical-LAN path. Cheap string compare — no syscall.
|
|
1398
|
-
const lockedOnLan = !!state.lanRemoteHost && state.remote?.host === state.lanRemoteHost;
|
|
1399
|
-
if (!lockedOnLan || remote.address === state.lanRemoteHost) {
|
|
1400
|
-
state.remote = { host: remote.address, port: remote.port };
|
|
1401
|
-
}
|
|
1407
|
+
this.#adoptRemote(state, remote.address, remote.port);
|
|
1402
1408
|
}
|
|
1403
1409
|
state.lastPingRecvMs = Date.now();
|
|
1404
1410
|
this.#debugVerboseLog(`hs_recv duplicate friend=${friendId} remote=${remote.address}:${remote.port} (session preserved)`);
|
|
@@ -1488,13 +1494,7 @@ export class Peer {
|
|
|
1488
1494
|
// Don't set state.remote — there's no UDP endpoint for this peer.
|
|
1489
1495
|
}
|
|
1490
1496
|
else {
|
|
1491
|
-
|
|
1492
|
-
// re-handshake arriving over the public/hairpin endpoint downgrade us
|
|
1493
|
-
// off a locked physical-LAN path. Cheap string compare — no syscall.
|
|
1494
|
-
const lockedOnLan = !!state.lanRemoteHost && state.remote?.host === state.lanRemoteHost;
|
|
1495
|
-
if (!lockedOnLan || remote.address === state.lanRemoteHost) {
|
|
1496
|
-
state.remote = { host: remote.address, port: remote.port };
|
|
1497
|
-
}
|
|
1497
|
+
this.#adoptRemote(state, remote.address, remote.port);
|
|
1498
1498
|
}
|
|
1499
1499
|
if (isNewSession) {
|
|
1500
1500
|
state.sendPacketNumber = 0;
|
|
@@ -1664,22 +1664,12 @@ export class Peer {
|
|
|
1664
1664
|
state.lastRelayRecvMs = Date.now();
|
|
1665
1665
|
}
|
|
1666
1666
|
else {
|
|
1667
|
-
// Adopt the source as our send endpoint —
|
|
1668
|
-
//
|
|
1669
|
-
//
|
|
1670
|
-
// the
|
|
1671
|
-
//
|
|
1672
|
-
|
|
1673
|
-
// is made ONLY in the periodic offer/maintenance paths, which record the
|
|
1674
|
-
// locked peer LAN host in state.lanRemoteHost. Here in the per-packet hot
|
|
1675
|
-
// path we do a single string compare — NO interface syscall — so a
|
|
1676
|
-
// high-rate stream (CCTV: thousands of packets/sec) never pays for it.
|
|
1677
|
-
// (The per-packet getPhysicalLanSubnets() call this replaces was the
|
|
1678
|
-
// CCTV CPU regression.)
|
|
1679
|
-
const lockedOnLan = !!state.lanRemoteHost && state.remote?.host === state.lanRemoteHost;
|
|
1680
|
-
if (!lockedOnLan || remote.address === state.lanRemoteHost) {
|
|
1681
|
-
state.remote = { host: remote.address, port: remote.port };
|
|
1682
|
-
}
|
|
1667
|
+
// Adopt the source as our send endpoint — via #adoptRemote, which
|
|
1668
|
+
// refuses to downgrade off a locked physical-LAN path to a public /
|
|
1669
|
+
// Tailscale-exit hairpin source. Cheap string compare, NO interface
|
|
1670
|
+
// syscall (that was the CCTV CPU regression) — safe at CCTV packet
|
|
1671
|
+
// rates.
|
|
1672
|
+
this.#adoptRemote(state, remote.address, remote.port);
|
|
1683
1673
|
if (!state.lastUdpRecvMs) {
|
|
1684
1674
|
this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
|
|
1685
1675
|
}
|
|
@@ -2561,8 +2551,24 @@ export class Peer {
|
|
|
2561
2551
|
// non-established branch below, which re-initiates a fresh
|
|
2562
2552
|
// connection — turning a permanent wedge into a ~timeout self-heal.
|
|
2563
2553
|
const lastAlive = session.lastPingRecvMs ?? session.sessionEstablishedAtMs ?? now;
|
|
2564
|
-
|
|
2565
|
-
|
|
2554
|
+
const silentFor = now - lastAlive;
|
|
2555
|
+
// Proven = we've decrypted ≥1 real packet, so the keys are known-good and
|
|
2556
|
+
// a silence is a transport blackout (GFW), not a desync. Give proven
|
|
2557
|
+
// sessions a long grace so a blackout doesn't churn them via re-handshake;
|
|
2558
|
+
// unproven sessions still time out fast to self-heal a wedged handshake.
|
|
2559
|
+
const proven = session.lastPingRecvMs !== undefined;
|
|
2560
|
+
const deadline = proven ? PROVEN_SESSION_HARD_TIMEOUT_MS : FRIEND_TIMEOUT_MS;
|
|
2561
|
+
if (proven && silentFor > FRIEND_TIMEOUT_MS && silentFor <= deadline) {
|
|
2562
|
+
// Blackout grace: keep the proven session and keep probing (the
|
|
2563
|
+
// keepalive + UDP re-punch + relay-keepalive below all still run). Do
|
|
2564
|
+
// NOT delete or re-handshake — that's what desyncs and churns. The peer,
|
|
2565
|
+
// running the same logic, also keeps its half, so when the path recovers
|
|
2566
|
+
// both resume in sync. Status stays as-is; the exit's own health probe
|
|
2567
|
+
// (multi-exit router) handles failover meanwhile.
|
|
2568
|
+
this.#debugVerboseLog(`session blackout-grace ${friendId} (silent ${silentFor}ms, proven — keeping keys, not re-handshaking)`);
|
|
2569
|
+
}
|
|
2570
|
+
else if (silentFor > deadline) {
|
|
2571
|
+
this.#debugLog(`session timeout for ${friendId} (no ping in ${silentFor}ms, proven=${proven}; ` +
|
|
2566
2572
|
`lastPingRecv=${session.lastPingRecvMs ?? "never"}) — tearing down to re-handshake`);
|
|
2567
2573
|
this.#friendSessions.delete(friendId);
|
|
2568
2574
|
this.#setFriendOffline(friendId);
|
|
@@ -3146,7 +3152,7 @@ export class Peer {
|
|
|
3146
3152
|
};
|
|
3147
3153
|
this.#friendSessions.set(friendId, session);
|
|
3148
3154
|
}
|
|
3149
|
-
session
|
|
3155
|
+
this.#adoptRemote(session, host, port);
|
|
3150
3156
|
this.#rememberEndpointCandidate(session, host, port);
|
|
3151
3157
|
if (realPublicKey && !session.friendRealPublicKey) {
|
|
3152
3158
|
session.friendRealPublicKey = realPublicKey;
|
|
@@ -3155,6 +3161,22 @@ export class Peer {
|
|
|
3155
3161
|
session.friendDhtPublicKey = dhtPublicKey;
|
|
3156
3162
|
}
|
|
3157
3163
|
}
|
|
3164
|
+
/** Single choke point for setting session.remote (the direct-UDP send
|
|
3165
|
+
* endpoint). Never downgrades off a LOCKED physical-LAN path to a non-LAN
|
|
3166
|
+
* endpoint (public srflx / Tailscale-exit hairpin): the LAN lock
|
|
3167
|
+
* (session.lanRemoteHost) is decided at candidate-learn / maintenance time —
|
|
3168
|
+
* the only places allowed the getPhysicalLanSubnets() syscall — so this guard
|
|
3169
|
+
* is just a string compare and is safe to call from the per-packet hot path.
|
|
3170
|
+
* Replacing the per-packet getPhysicalLanSubnets() call here was the fix for
|
|
3171
|
+
* the CCTV CPU regression. */
|
|
3172
|
+
#adoptRemote(session, host, port) {
|
|
3173
|
+
if (session.lanRemoteHost &&
|
|
3174
|
+
session.remote?.host === session.lanRemoteHost &&
|
|
3175
|
+
host !== session.lanRemoteHost) {
|
|
3176
|
+
return; // stay locked on the physical-LAN path
|
|
3177
|
+
}
|
|
3178
|
+
session.remote = { host, port };
|
|
3179
|
+
}
|
|
3158
3180
|
#rememberEndpointCandidate(session, host, port) {
|
|
3159
3181
|
const now = Date.now();
|
|
3160
3182
|
const next = (session.endpointCandidates ?? []).filter((candidate) => !(candidate.host === host && candidate.port === port));
|
|
@@ -3499,7 +3521,7 @@ export class Peer {
|
|
|
3499
3521
|
const haveLanCandidate = (session.endpointCandidates ?? []).some((c) => isPrivateAddress(c.host) && !isCgnatAddress(c.host) &&
|
|
3500
3522
|
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(c.host, s)));
|
|
3501
3523
|
if (!haveRealUdp && !haveLanCandidate) {
|
|
3502
|
-
session
|
|
3524
|
+
this.#adoptRemote(session, host, port);
|
|
3503
3525
|
}
|
|
3504
3526
|
// Nudge a keepalive out immediately so the upgrade doesn't wait for
|
|
3505
3527
|
// the next periodic ALIVE — fans out over UDP (punched) + TCP.
|
|
@@ -3684,7 +3706,7 @@ export class Peer {
|
|
|
3684
3706
|
if (tcpAvailable)
|
|
3685
3707
|
session.hasTcpRoute = true;
|
|
3686
3708
|
if (connectCandidates.length > 0) {
|
|
3687
|
-
session.
|
|
3709
|
+
this.#adoptRemote(session, connectCandidates[0].host, connectCandidates[0].port);
|
|
3688
3710
|
const endpointKey = `${connectCandidates[0].host}:${connectCandidates[0].port}`;
|
|
3689
3711
|
if (this.#lastEndpointSelectedKey.get(friendId) !== endpointKey) {
|
|
3690
3712
|
this.#debugLog(`endpoint_selected friend=${friendId} endpoint=${endpointKey}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|