@decentnetwork/peer 0.1.63 → 0.1.64

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 +43 -17
  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
@@ -1832,24 +1841,41 @@ export class Peer {
1832
1841
  this.#debugVerboseLog(`unknown messenger packet kind ${kind} from ${friendId}`);
1833
1842
  return;
1834
1843
  }
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) {
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) {
1842
1859
  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);
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;
1851
1864
  }
1852
- break;
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);
1853
1879
  }
1854
1880
  }
1855
1881
  this.#debugVerboseLog(`crypto data received from ${remote.address}:${remote.port} but no session matched`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
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",