@decentnetwork/peer 0.1.64 → 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 +46 -88
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -1456,30 +1456,13 @@ export class Peer {
1456
1456
  const lastAlive = state.lastPingRecvMs ?? state.sessionEstablishedAtMs;
1457
1457
  const currentSessionAlive = Date.now() - lastAlive < FRIEND_TIMEOUT_MS;
1458
1458
  if (sinceEstablished > 1000 && currentSessionAlive) {
1459
- // Half-open breaker. Our session looks alive — but "alive" only means
1460
- // WE are receiving (possibly only over the relay). If the peer KEEPS
1461
- // proposing a new key, THEIR side can't decrypt our packets: a desynced
1462
- // half-open session (observed snoopy↔mac on one LAN as 288k "no session
1463
- // matched"). Rejecting forever deadlocks. So count sustained attempts
1464
- // and, past the threshold, accept the new key to re-converge. Bounded
1465
- // to sustained attempts so it never disturbs a healthy session.
1466
- const now = Date.now();
1467
- if (state.rehsFirstMs === undefined || now - (state.rehsLastMs ?? 0) > REHS_WINDOW_MS) {
1468
- state.rehsFirstMs = now;
1469
- state.rehsCount = 0;
1470
- }
1471
- state.rehsLastMs = now;
1472
- state.rehsCount = (state.rehsCount ?? 0) + 1;
1473
- const stuck = state.rehsCount >= REHS_ACCEPT_COUNT && now - state.rehsFirstMs >= REHS_ACCEPT_MS;
1474
- if (!stuck) {
1475
- this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live session, attempt ${state.rehsCount}, ${sinceEstablished}ms after establish)`);
1476
- return;
1477
- }
1478
- 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;
1479
1465
  }
1480
- // Converging on a fresh session — clear the breaker counter.
1481
- state.rehsCount = 0;
1482
- state.rehsFirstMs = undefined;
1483
1466
  this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (current session wedged/dead, ` +
1484
1467
  `lastPingRecv=${state.lastPingRecvMs ? Date.now() - state.lastPingRecvMs + "ms ago" : "never"})`);
1485
1468
  }
@@ -1841,43 +1824,15 @@ export class Peer {
1841
1824
  this.#debugVerboseLog(`unknown messenger packet kind ${kind} from ${friendId}`);
1842
1825
  return;
1843
1826
  }
1844
- // We received crypto data we can't decrypt from a KNOWN friend: our two
1845
- // sessions are desynced (different keys) and both are "established", so
1846
- // neither re-handshakes (#initiateSession no-ops on an established session,
1847
- // and the lower-pubkey side defers entirely). DELETE the stale session so
1848
- // the connection loop re-establishes cleanly via the single-initiator path
1849
- // (higher pubkey initiates a fresh cookie chain, lower responds, both
1850
- // derive the SAME key). This covers BOTH transports the desync shows up
1851
- // over the TCP relay (remote = tcp:<friendId>:0) as well as direct UDP.
1852
- let desyncedFid;
1853
- if (this.#remoteIsTcp(remote)) {
1854
- const m = /^tcp:([A-Za-z0-9]+)/.exec(remote.address);
1855
- if (m && this.#friendSessions.has(m[1]))
1856
- desyncedFid = m[1];
1857
- }
1858
- else if (!viaRelay) {
1859
- for (const [fid, st] of this.#friendSessions.entries()) {
1860
- if ((st.remote?.host === remote.address && st.remote?.port === remote.port) ||
1861
- (st.endpointCandidates ?? []).some((c) => c.host === remote.address && c.port === remote.port)) {
1862
- desyncedFid = fid;
1863
- break;
1864
- }
1865
- }
1866
- }
1867
- if (desyncedFid) {
1868
- const st = this.#friendSessions.get(desyncedFid);
1869
- // Only a GENUINELY stuck session (no successful decrypt for a while) —
1870
- // never nuke a healthy one on a stray/late undecryptable packet — and
1871
- // rate-limited via a map that survives the delete.
1872
- const working = st.lastPingRecvMs !== undefined && Date.now() - st.lastPingRecvMs < REINIT_STUCK_MS;
1873
- const lastDel = this.#lastDesyncDeleteMs.get(desyncedFid) ?? 0;
1874
- if (!working && Date.now() - lastDel > REINIT_ON_DESYNC_MS) {
1875
- this.#lastDesyncDeleteMs.set(desyncedFid, Date.now());
1876
- this.#debugLog(`no-session-matched from ${remote.address} (friend ${desyncedFid}) — desynced, deleting session for clean re-handshake`);
1877
- this.#friendSessions.delete(desyncedFid);
1878
- void this.#initiateSession(desyncedFid).catch(() => undefined);
1879
- }
1880
- }
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.
1881
1836
  this.#debugVerboseLog(`crypto data received from ${remote.address}:${remote.port} but no session matched`);
1882
1837
  return;
1883
1838
  }
@@ -5072,50 +5027,53 @@ function isCgnatAddress(host) {
5072
5027
  }
5073
5028
  /** Local IPv4 addresses on PHYSICAL interfaces only (excludes overlay/virtual
5074
5029
  * interfaces by name and the CGNAT range). Used for LAN-direct host candidates. */
5075
- function getPhysicalLanAddresses() {
5076
- 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 = [];
5077
5045
  try {
5078
5046
  for (const [name, list] of Object.entries(networkInterfaces())) {
5079
5047
  if (!list || VIRTUAL_IFACE_RE.test(name))
5080
5048
  continue;
5081
5049
  for (const info of list) {
5082
- if (info.family !== "IPv4" || info.internal)
5083
- continue;
5084
- if (isCgnatAddress(info.address))
5050
+ if (info.family !== "IPv4" || info.internal || isCgnatAddress(info.address))
5085
5051
  continue;
5086
- 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
+ }
5087
5058
  }
5088
5059
  }
5089
5060
  }
5090
5061
  catch {
5091
5062
  // best-effort
5092
5063
  }
5093
- return out;
5064
+ _lanAddrsCache = addrs;
5065
+ _lanSubnetsCache = subnets;
5066
+ }
5067
+ function getPhysicalLanAddresses() {
5068
+ refreshLanIfaceCache();
5069
+ return _lanAddrsCache;
5094
5070
  }
5095
5071
  /** Subnets of PHYSICAL interfaces only — for matching a peer's host candidate
5096
5072
  * to OUR real LAN (so we never match a peer's Tailscale/TUN address against our
5097
- * own overlay subnet). */
5073
+ * own overlay subnet). Cached (see refreshLanIfaceCache). */
5098
5074
  function getPhysicalLanSubnets() {
5099
- const out = [];
5100
- try {
5101
- for (const [name, list] of Object.entries(networkInterfaces())) {
5102
- if (!list || VIRTUAL_IFACE_RE.test(name))
5103
- continue;
5104
- for (const info of list) {
5105
- if (info.family !== "IPv4" || info.internal || isCgnatAddress(info.address))
5106
- continue;
5107
- const addr = ipv4ToInt(info.address);
5108
- const mask = netmaskToInt(info.netmask);
5109
- if (addr === undefined || mask === undefined || mask === 0)
5110
- continue;
5111
- out.push({ networkBits: (addr & mask) >>> 0, maskBits: mask });
5112
- }
5113
- }
5114
- }
5115
- catch {
5116
- // best-effort
5117
- }
5118
- return out;
5075
+ refreshLanIfaceCache();
5076
+ return _lanSubnetsCache;
5119
5077
  }
5120
5078
  function isPrivateAddress(host) {
5121
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.64",
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",