@decentnetwork/peer 0.1.67 → 0.1.69
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.d.ts +4 -0
- package/dist/peer.js +54 -22
- package/package.json +1 -1
package/dist/peer.d.ts
CHANGED
|
@@ -201,6 +201,10 @@ export declare class Peer {
|
|
|
201
201
|
* peer (null = never). Used to confirm Phase 1.2 retries are
|
|
202
202
|
* actually firing. */
|
|
203
203
|
cookieRequestSentMs: number | null;
|
|
204
|
+
/** The peer's physical-LAN host we've locked onto (set by the periodic
|
|
205
|
+
* offer/maintenance LAN-adoption code). When set and equal to udpRemote's
|
|
206
|
+
* host, the per-packet receive path refuses a public/hairpin downgrade. */
|
|
207
|
+
lanRemoteHost: string | null;
|
|
204
208
|
} | null;
|
|
205
209
|
waitForFriendRequest(timeoutMs?: number): Promise<FriendRequest>;
|
|
206
210
|
/**
|
package/dist/peer.js
CHANGED
|
@@ -82,6 +82,11 @@ 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
|
+
// How long the direct UDP path may go without an inbound packet before we
|
|
86
|
+
// release a physical-LAN lock (so a peer that left the LAN can recover onto the
|
|
87
|
+
// public/relay path). 12s ≈ 3 missed 4s keepalives — long enough to never trip
|
|
88
|
+
// on a healthy same-LAN path, short enough to recover quickly.
|
|
89
|
+
const LAN_LOCK_STALE_MS = readEnvInt("DECENT_LAN_LOCK_STALE_MS", 12000);
|
|
85
90
|
// Half-open breaker: if the peer proposes a NEW session key this many times over
|
|
86
91
|
// this long while our session looks "alive", accept it (the peer can't decrypt
|
|
87
92
|
// our current session — a desynced half-open session, observed as a flood of
|
|
@@ -1084,6 +1089,7 @@ export class Peer {
|
|
|
1084
1089
|
endpointCandidatesCount: s.endpointCandidates?.length ?? 0,
|
|
1085
1090
|
endpointCandidates: (s.endpointCandidates ?? []).map((c) => `${c.host}:${c.port}`),
|
|
1086
1091
|
cookieRequestSentMs: s.cookieRequestSentMs ?? null,
|
|
1092
|
+
lanRemoteHost: s.lanRemoteHost ?? null,
|
|
1087
1093
|
};
|
|
1088
1094
|
}
|
|
1089
1095
|
waitForFriendRequest(timeoutMs = 30000) {
|
|
@@ -1312,7 +1318,7 @@ export class Peer {
|
|
|
1312
1318
|
// poison the UDP send-back path.
|
|
1313
1319
|
const fromTcp = this.#remoteIsTcp(remote);
|
|
1314
1320
|
if (!fromTcp) {
|
|
1315
|
-
matchedSession
|
|
1321
|
+
this.#adoptRemote(matchedSession, remote.address, remote.port);
|
|
1316
1322
|
}
|
|
1317
1323
|
else {
|
|
1318
1324
|
matchedSession.hasTcpRoute = true;
|
|
@@ -1386,7 +1392,7 @@ export class Peer {
|
|
|
1386
1392
|
state.hasTcpRoute = true;
|
|
1387
1393
|
}
|
|
1388
1394
|
else {
|
|
1389
|
-
state
|
|
1395
|
+
this.#adoptRemote(state, remote.address, remote.port);
|
|
1390
1396
|
}
|
|
1391
1397
|
state.lastPingRecvMs = Date.now();
|
|
1392
1398
|
this.#debugVerboseLog(`hs_recv duplicate friend=${friendId} remote=${remote.address}:${remote.port} (session preserved)`);
|
|
@@ -1476,7 +1482,7 @@ export class Peer {
|
|
|
1476
1482
|
// Don't set state.remote — there's no UDP endpoint for this peer.
|
|
1477
1483
|
}
|
|
1478
1484
|
else {
|
|
1479
|
-
state
|
|
1485
|
+
this.#adoptRemote(state, remote.address, remote.port);
|
|
1480
1486
|
}
|
|
1481
1487
|
if (isNewSession) {
|
|
1482
1488
|
state.sendPacketNumber = 0;
|
|
@@ -1646,22 +1652,12 @@ export class Peer {
|
|
|
1646
1652
|
state.lastRelayRecvMs = Date.now();
|
|
1647
1653
|
}
|
|
1648
1654
|
else {
|
|
1649
|
-
// Adopt the source as our send endpoint —
|
|
1650
|
-
//
|
|
1651
|
-
//
|
|
1652
|
-
// the
|
|
1653
|
-
//
|
|
1654
|
-
|
|
1655
|
-
// is made ONLY in the periodic offer/maintenance paths, which record the
|
|
1656
|
-
// locked peer LAN host in state.lanRemoteHost. Here in the per-packet hot
|
|
1657
|
-
// path we do a single string compare — NO interface syscall — so a
|
|
1658
|
-
// high-rate stream (CCTV: thousands of packets/sec) never pays for it.
|
|
1659
|
-
// (The per-packet getPhysicalLanSubnets() call this replaces was the
|
|
1660
|
-
// CCTV CPU regression.)
|
|
1661
|
-
const lockedOnLan = !!state.lanRemoteHost && state.remote?.host === state.lanRemoteHost;
|
|
1662
|
-
if (!lockedOnLan || remote.address === state.lanRemoteHost) {
|
|
1663
|
-
state.remote = { host: remote.address, port: remote.port };
|
|
1664
|
-
}
|
|
1655
|
+
// Adopt the source as our send endpoint — via #adoptRemote, which
|
|
1656
|
+
// refuses to downgrade off a locked physical-LAN path to a public /
|
|
1657
|
+
// Tailscale-exit hairpin source. Cheap string compare, NO interface
|
|
1658
|
+
// syscall (that was the CCTV CPU regression) — safe at CCTV packet
|
|
1659
|
+
// rates.
|
|
1660
|
+
this.#adoptRemote(state, remote.address, remote.port);
|
|
1665
1661
|
if (!state.lastUdpRecvMs) {
|
|
1666
1662
|
this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
|
|
1667
1663
|
}
|
|
@@ -3128,7 +3124,7 @@ export class Peer {
|
|
|
3128
3124
|
};
|
|
3129
3125
|
this.#friendSessions.set(friendId, session);
|
|
3130
3126
|
}
|
|
3131
|
-
session
|
|
3127
|
+
this.#adoptRemote(session, host, port);
|
|
3132
3128
|
this.#rememberEndpointCandidate(session, host, port);
|
|
3133
3129
|
if (realPublicKey && !session.friendRealPublicKey) {
|
|
3134
3130
|
session.friendRealPublicKey = realPublicKey;
|
|
@@ -3137,6 +3133,22 @@ export class Peer {
|
|
|
3137
3133
|
session.friendDhtPublicKey = dhtPublicKey;
|
|
3138
3134
|
}
|
|
3139
3135
|
}
|
|
3136
|
+
/** Single choke point for setting session.remote (the direct-UDP send
|
|
3137
|
+
* endpoint). Never downgrades off a LOCKED physical-LAN path to a non-LAN
|
|
3138
|
+
* endpoint (public srflx / Tailscale-exit hairpin): the LAN lock
|
|
3139
|
+
* (session.lanRemoteHost) is decided at candidate-learn / maintenance time —
|
|
3140
|
+
* the only places allowed the getPhysicalLanSubnets() syscall — so this guard
|
|
3141
|
+
* is just a string compare and is safe to call from the per-packet hot path.
|
|
3142
|
+
* Replacing the per-packet getPhysicalLanSubnets() call here was the fix for
|
|
3143
|
+
* the CCTV CPU regression. */
|
|
3144
|
+
#adoptRemote(session, host, port) {
|
|
3145
|
+
if (session.lanRemoteHost &&
|
|
3146
|
+
session.remote?.host === session.lanRemoteHost &&
|
|
3147
|
+
host !== session.lanRemoteHost) {
|
|
3148
|
+
return; // stay locked on the physical-LAN path
|
|
3149
|
+
}
|
|
3150
|
+
session.remote = { host, port };
|
|
3151
|
+
}
|
|
3140
3152
|
#rememberEndpointCandidate(session, host, port) {
|
|
3141
3153
|
const now = Date.now();
|
|
3142
3154
|
const next = (session.endpointCandidates ?? []).filter((candidate) => !(candidate.host === host && candidate.port === port));
|
|
@@ -3144,6 +3156,15 @@ export class Peer {
|
|
|
3144
3156
|
session.endpointCandidates = next
|
|
3145
3157
|
.sort((a, b) => b.updatedMs - a.updatedMs)
|
|
3146
3158
|
.slice(0, 12);
|
|
3159
|
+
// The moment we learn the peer's physical-LAN host candidate, record it as
|
|
3160
|
+
// the LAN lock so the per-packet/handshake hot paths refuse a public/hairpin
|
|
3161
|
+
// downgrade (cheap string compare there, no syscall). This runs at
|
|
3162
|
+
// candidate-learn time (offer/discovery), not per packet, so the cached
|
|
3163
|
+
// getPhysicalLanSubnets() call is fine here.
|
|
3164
|
+
if (isPrivateAddress(host) && !isCgnatAddress(host) &&
|
|
3165
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(host, s))) {
|
|
3166
|
+
session.lanRemoteHost = host;
|
|
3167
|
+
}
|
|
3147
3168
|
}
|
|
3148
3169
|
/**
|
|
3149
3170
|
* Learn our own server-reflexive (public) UDP endpoint by sending a
|
|
@@ -3472,7 +3493,7 @@ export class Peer {
|
|
|
3472
3493
|
const haveLanCandidate = (session.endpointCandidates ?? []).some((c) => isPrivateAddress(c.host) && !isCgnatAddress(c.host) &&
|
|
3473
3494
|
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(c.host, s)));
|
|
3474
3495
|
if (!haveRealUdp && !haveLanCandidate) {
|
|
3475
|
-
session
|
|
3496
|
+
this.#adoptRemote(session, host, port);
|
|
3476
3497
|
}
|
|
3477
3498
|
// Nudge a keepalive out immediately so the upgrade doesn't wait for
|
|
3478
3499
|
// the next periodic ALIVE — fans out over UDP (punched) + TCP.
|
|
@@ -3657,7 +3678,7 @@ export class Peer {
|
|
|
3657
3678
|
if (tcpAvailable)
|
|
3658
3679
|
session.hasTcpRoute = true;
|
|
3659
3680
|
if (connectCandidates.length > 0) {
|
|
3660
|
-
session.
|
|
3681
|
+
this.#adoptRemote(session, connectCandidates[0].host, connectCandidates[0].port);
|
|
3661
3682
|
const endpointKey = `${connectCandidates[0].host}:${connectCandidates[0].port}`;
|
|
3662
3683
|
if (this.#lastEndpointSelectedKey.get(friendId) !== endpointKey) {
|
|
3663
3684
|
this.#debugLog(`endpoint_selected friend=${friendId} endpoint=${endpointKey}`);
|
|
@@ -4061,6 +4082,17 @@ export class Peer {
|
|
|
4061
4082
|
for (const [friendId, session] of this.#friendSessions.entries()) {
|
|
4062
4083
|
if (!session.established)
|
|
4063
4084
|
continue;
|
|
4085
|
+
// Release a stale LAN lock. If we're locked onto a LAN host but the
|
|
4086
|
+
// direct UDP path has gone quiet (peer left the LAN / changed network),
|
|
4087
|
+
// the lock would otherwise block recovery onto the public/relay path.
|
|
4088
|
+
// Clearing it lets the hot-path guard adopt a working endpoint again.
|
|
4089
|
+
if (session.lanRemoteHost &&
|
|
4090
|
+
session.remote?.host === session.lanRemoteHost &&
|
|
4091
|
+
session.lastUdpRecvMs !== undefined &&
|
|
4092
|
+
Date.now() - session.lastUdpRecvMs > LAN_LOCK_STALE_MS) {
|
|
4093
|
+
this.#debugLog(`lan_lock_released friend=${friendId} host=${session.lanRemoteHost} (no direct UDP recv for >${LAN_LOCK_STALE_MS}ms)`);
|
|
4094
|
+
session.lanRemoteHost = undefined;
|
|
4095
|
+
}
|
|
4064
4096
|
const r = session.remote;
|
|
4065
4097
|
const remoteIsSameLan = !!r && isPrivateAddress(r.host) && !isCgnatAddress(r.host) &&
|
|
4066
4098
|
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(r.host, s));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
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",
|