@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.
Files changed (2) hide show
  1. package/dist/peer.js +56 -72
  2. 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
- const REINIT_ON_DESYNC_MS = readEnvInt("DECENT_REINIT_ON_DESYNC_MS", 2000);
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
- // Half-open breaker. Our session looks alive — but "alive" only means
1451
- // WE are receiving (possibly only over the relay). If the peer KEEPS
1452
- // proposing a new key, THEIR side can't decrypt our packets: a desynced
1453
- // half-open session (observed snoopy↔mac on one LAN as 288k "no session
1454
- // matched"). Rejecting forever deadlocks. So count sustained attempts
1455
- // and, past the threshold, accept the new key to re-converge. Bounded
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
- // We received crypto data we can't decrypt from a KNOWN friend's endpoint:
1836
- // our two sessions are desynced (different keys) and NEITHER side will
1837
- // re-handshake on its own, because each believes it is still established
1838
- // (the half-open breaker can't help no handshakes are being exchanged).
1839
- // Kick a fresh handshake so we re-converge. Rate-limited per friend so a
1840
- // flood of undecryptable packets doesn't spam cookie requests.
1841
- if (!this.#remoteIsTcp(remote) && !viaRelay) {
1842
- for (const [fid, st] of this.#friendSessions.entries()) {
1843
- const matches = (st.remote?.host === remote.address && st.remote?.port === remote.port) ||
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
- function getPhysicalLanAddresses() {
5050
- const out = [];
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
- out.push(info.address);
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
- return out;
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
- const out = [];
5074
- try {
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.63",
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",