@decentnetwork/peer 0.1.63 → 0.1.65
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 +56 -72
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -92,7 +92,13 @@ const REHS_ACCEPT_MS = readEnvInt("DECENT_REHS_ACCEPT_MS", 2500);
|
|
|
92
92
|
const REHS_WINDOW_MS = readEnvInt("DECENT_REHS_WINDOW_MS", 8000);
|
|
93
93
|
// Re-handshake when we keep receiving undecryptable crypto data from a known
|
|
94
94
|
// friend's endpoint (desynced sessions), rate-limited per friend.
|
|
95
|
-
|
|
95
|
+
// Conservative: deleting a session forces a re-handshake which ROTATES our
|
|
96
|
+
// ephemeral key, so doing it too eagerly turns a transient blip into a
|
|
97
|
+
// rotate→desync→delete thrash. Only nuke a session that's been UNABLE to
|
|
98
|
+
// decrypt anything for a long time (genuinely dead, well past any
|
|
99
|
+
// establishment-transition window), and at most once per long interval.
|
|
100
|
+
const REINIT_ON_DESYNC_MS = readEnvInt("DECENT_REINIT_ON_DESYNC_MS", 12000);
|
|
101
|
+
const REINIT_STUCK_MS = readEnvInt("DECENT_REINIT_STUCK_MS", 20000);
|
|
96
102
|
// How long a relay path stays "confirmed" (carrying bulk data on its own, no
|
|
97
103
|
// tcp-relay fan-out) after the last packet received over it. The relay
|
|
98
104
|
// keepalive refreshes this every ~4s on a healthy path, so a generous window
|
|
@@ -267,6 +273,9 @@ export class Peer {
|
|
|
267
273
|
// #initiateSession every few seconds for every friend; without this
|
|
268
274
|
// dedupe, lower-pubkey side floods the log with "deferring to peer".
|
|
269
275
|
#initiateSkipLogged = new Set();
|
|
276
|
+
// Per-friend last time we deleted a desynced session (rate-limit; survives the
|
|
277
|
+
// session deletion, unlike a field on the session object itself).
|
|
278
|
+
#lastDesyncDeleteMs = new Map();
|
|
270
279
|
// Cached server-reflexive (public) UDP endpoint of our #udp socket,
|
|
271
280
|
// learned via STUN. Cone-NAT mappings are stable for the socket
|
|
272
281
|
// lifetime, so we only re-probe every ~60s. Used to tell friends where
|
|
@@ -1447,30 +1456,13 @@ export class Peer {
|
|
|
1447
1456
|
const lastAlive = state.lastPingRecvMs ?? state.sessionEstablishedAtMs;
|
|
1448
1457
|
const currentSessionAlive = Date.now() - lastAlive < FRIEND_TIMEOUT_MS;
|
|
1449
1458
|
if (sinceEstablished > 1000 && currentSessionAlive) {
|
|
1450
|
-
//
|
|
1451
|
-
//
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
// to sustained attempts so it never disturbs a healthy session.
|
|
1457
|
-
const now = Date.now();
|
|
1458
|
-
if (state.rehsFirstMs === undefined || now - (state.rehsLastMs ?? 0) > REHS_WINDOW_MS) {
|
|
1459
|
-
state.rehsFirstMs = now;
|
|
1460
|
-
state.rehsCount = 0;
|
|
1461
|
-
}
|
|
1462
|
-
state.rehsLastMs = now;
|
|
1463
|
-
state.rehsCount = (state.rehsCount ?? 0) + 1;
|
|
1464
|
-
const stuck = state.rehsCount >= REHS_ACCEPT_COUNT && now - state.rehsFirstMs >= REHS_ACCEPT_MS;
|
|
1465
|
-
if (!stuck) {
|
|
1466
|
-
this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live session, attempt ${state.rehsCount}, ${sinceEstablished}ms after establish)`);
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (half-open breaker: ${state.rehsCount} new-key attempts over ${now - state.rehsFirstMs}ms — peer can't decrypt our session)`);
|
|
1459
|
+
// Reject a new-key handshake while our session is alive — toxcore does
|
|
1460
|
+
// the same. (The "half-open breaker" that accepted these after N tries
|
|
1461
|
+
// was reverted: accepting a new key ROTATES the session, which churned
|
|
1462
|
+
// healthy sessions; the real fix was the TURN-crash fix, not this.)
|
|
1463
|
+
this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ${sinceEstablished}ms after establish)`);
|
|
1464
|
+
return;
|
|
1470
1465
|
}
|
|
1471
|
-
// Converging on a fresh session — clear the breaker counter.
|
|
1472
|
-
state.rehsCount = 0;
|
|
1473
|
-
state.rehsFirstMs = undefined;
|
|
1474
1466
|
this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (current session wedged/dead, ` +
|
|
1475
1467
|
`lastPingRecv=${state.lastPingRecvMs ? Date.now() - state.lastPingRecvMs + "ms ago" : "never"})`);
|
|
1476
1468
|
}
|
|
@@ -1832,26 +1824,15 @@ export class Peer {
|
|
|
1832
1824
|
this.#debugVerboseLog(`unknown messenger packet kind ${kind} from ${friendId}`);
|
|
1833
1825
|
return;
|
|
1834
1826
|
}
|
|
1835
|
-
//
|
|
1836
|
-
//
|
|
1837
|
-
//
|
|
1838
|
-
// (
|
|
1839
|
-
//
|
|
1840
|
-
//
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
(st.endpointCandidates ?? []).some((c) => c.host === remote.address && c.port === remote.port);
|
|
1845
|
-
if (!matches)
|
|
1846
|
-
continue;
|
|
1847
|
-
if (Date.now() - (st.lastReinitMs ?? 0) > REINIT_ON_DESYNC_MS) {
|
|
1848
|
-
st.lastReinitMs = Date.now();
|
|
1849
|
-
this.#debugLog(`no-session-matched from ${remote.address}:${remote.port} (friend ${fid}) — desynced, re-initiating handshake`);
|
|
1850
|
-
void this.#initiateSession(fid).catch(() => undefined);
|
|
1851
|
-
}
|
|
1852
|
-
break;
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1827
|
+
// Reverted: we used to DELETE a "desynced" session here to force a clean
|
|
1828
|
+
// re-handshake. It caused more harm than good — deleting a session
|
|
1829
|
+
// interrupts the data plane, and a transient/late undecryptable packet
|
|
1830
|
+
// (common over the relay) triggered a delete→re-key→re-desync thrash that
|
|
1831
|
+
// churned MANY friends' sessions (observed: CCTV jitter + slow file
|
|
1832
|
+
// transfer to 4090, not just snoopy). The real instability was the daemon
|
|
1833
|
+
// CRASH (TURN DNS); with that fixed, sessions stay converged on their own.
|
|
1834
|
+
// Just drop the undecryptable packet — net_crypto's normal re-handshake
|
|
1835
|
+
// path recovers a genuinely dead session.
|
|
1855
1836
|
this.#debugVerboseLog(`crypto data received from ${remote.address}:${remote.port} but no session matched`);
|
|
1856
1837
|
return;
|
|
1857
1838
|
}
|
|
@@ -5046,50 +5027,53 @@ function isCgnatAddress(host) {
|
|
|
5046
5027
|
}
|
|
5047
5028
|
/** Local IPv4 addresses on PHYSICAL interfaces only (excludes overlay/virtual
|
|
5048
5029
|
* interfaces by name and the CGNAT range). Used for LAN-direct host candidates. */
|
|
5049
|
-
|
|
5050
|
-
|
|
5030
|
+
// CACHED interface enumeration. getPhysicalLanSubnets/Addresses are called on
|
|
5031
|
+
// the receive hot path (the same-LAN downgrade guard runs per packet); calling
|
|
5032
|
+
// networkInterfaces() — a syscall — for every CRYPTO_DATA packet pegged the CPU
|
|
5033
|
+
// on a high-rate stream (CCTV jitter/instability). Cache the result for 5s; LAN
|
|
5034
|
+
// interfaces almost never change, so this is safe and ~free.
|
|
5035
|
+
let _lanIfaceCacheMs = 0;
|
|
5036
|
+
let _lanAddrsCache = [];
|
|
5037
|
+
let _lanSubnetsCache = [];
|
|
5038
|
+
function refreshLanIfaceCache() {
|
|
5039
|
+
const now = Date.now();
|
|
5040
|
+
if (_lanIfaceCacheMs !== 0 && now - _lanIfaceCacheMs < 5000)
|
|
5041
|
+
return;
|
|
5042
|
+
_lanIfaceCacheMs = now;
|
|
5043
|
+
const addrs = [];
|
|
5044
|
+
const subnets = [];
|
|
5051
5045
|
try {
|
|
5052
5046
|
for (const [name, list] of Object.entries(networkInterfaces())) {
|
|
5053
5047
|
if (!list || VIRTUAL_IFACE_RE.test(name))
|
|
5054
5048
|
continue;
|
|
5055
5049
|
for (const info of list) {
|
|
5056
|
-
if (info.family !== "IPv4" || info.internal)
|
|
5057
|
-
continue;
|
|
5058
|
-
if (isCgnatAddress(info.address))
|
|
5050
|
+
if (info.family !== "IPv4" || info.internal || isCgnatAddress(info.address))
|
|
5059
5051
|
continue;
|
|
5060
|
-
|
|
5052
|
+
addrs.push(info.address);
|
|
5053
|
+
const addr = ipv4ToInt(info.address);
|
|
5054
|
+
const mask = netmaskToInt(info.netmask);
|
|
5055
|
+
if (addr !== undefined && mask !== undefined && mask !== 0) {
|
|
5056
|
+
subnets.push({ networkBits: (addr & mask) >>> 0, maskBits: mask });
|
|
5057
|
+
}
|
|
5061
5058
|
}
|
|
5062
5059
|
}
|
|
5063
5060
|
}
|
|
5064
5061
|
catch {
|
|
5065
5062
|
// best-effort
|
|
5066
5063
|
}
|
|
5067
|
-
|
|
5064
|
+
_lanAddrsCache = addrs;
|
|
5065
|
+
_lanSubnetsCache = subnets;
|
|
5066
|
+
}
|
|
5067
|
+
function getPhysicalLanAddresses() {
|
|
5068
|
+
refreshLanIfaceCache();
|
|
5069
|
+
return _lanAddrsCache;
|
|
5068
5070
|
}
|
|
5069
5071
|
/** Subnets of PHYSICAL interfaces only — for matching a peer's host candidate
|
|
5070
5072
|
* to OUR real LAN (so we never match a peer's Tailscale/TUN address against our
|
|
5071
|
-
* own overlay subnet). */
|
|
5073
|
+
* own overlay subnet). Cached (see refreshLanIfaceCache). */
|
|
5072
5074
|
function getPhysicalLanSubnets() {
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
for (const [name, list] of Object.entries(networkInterfaces())) {
|
|
5076
|
-
if (!list || VIRTUAL_IFACE_RE.test(name))
|
|
5077
|
-
continue;
|
|
5078
|
-
for (const info of list) {
|
|
5079
|
-
if (info.family !== "IPv4" || info.internal || isCgnatAddress(info.address))
|
|
5080
|
-
continue;
|
|
5081
|
-
const addr = ipv4ToInt(info.address);
|
|
5082
|
-
const mask = netmaskToInt(info.netmask);
|
|
5083
|
-
if (addr === undefined || mask === undefined || mask === 0)
|
|
5084
|
-
continue;
|
|
5085
|
-
out.push({ networkBits: (addr & mask) >>> 0, maskBits: mask });
|
|
5086
|
-
}
|
|
5087
|
-
}
|
|
5088
|
-
}
|
|
5089
|
-
catch {
|
|
5090
|
-
// best-effort
|
|
5091
|
-
}
|
|
5092
|
-
return out;
|
|
5075
|
+
refreshLanIfaceCache();
|
|
5076
|
+
return _lanSubnetsCache;
|
|
5093
5077
|
}
|
|
5094
5078
|
function isPrivateAddress(host) {
|
|
5095
5079
|
// RFC1918 IPv4: 10/8, 172.16/12, 192.168/16; loopback 127/8;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.65",
|
|
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",
|