@decentnetwork/peer 0.1.62 → 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.
- package/dist/peer.js +51 -18
- 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
|
|
@@ -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
|
|
1836
|
-
//
|
|
1837
|
-
// re-
|
|
1838
|
-
//
|
|
1839
|
-
//
|
|
1840
|
-
//
|
|
1841
|
-
|
|
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
|
-
|
|
1844
|
-
(st.endpointCandidates ?? []).some((c) => c.host === remote.address && c.port === remote.port)
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
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`);
|
|
@@ -3476,7 +3502,14 @@ export class Peer {
|
|
|
3476
3502
|
// upgrading the path to direct UDP in both directions with no new
|
|
3477
3503
|
// handshake. If UDP never works, the periodic offer re-punches.
|
|
3478
3504
|
const haveRealUdp = session.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
|
|
3479
|
-
|
|
3505
|
+
// NEVER point the session at the public srflx when we know a same-LAN
|
|
3506
|
+
// candidate for this peer: the physical LAN is strictly better, and letting
|
|
3507
|
+
// the srflx win here caused a LAN↔public FLAP (the LAN block above set
|
|
3508
|
+
// remote=LAN, then this overwrote it with the Tailscale-exit public IP),
|
|
3509
|
+
// so the direct LAN path never stabilised and udpRecv never confirmed.
|
|
3510
|
+
const haveLanCandidate = (session.endpointCandidates ?? []).some((c) => isPrivateAddress(c.host) && !isCgnatAddress(c.host) &&
|
|
3511
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(c.host, s)));
|
|
3512
|
+
if (!haveRealUdp && !haveLanCandidate) {
|
|
3480
3513
|
session.remote = { host, port };
|
|
3481
3514
|
}
|
|
3482
3515
|
// Nudge a keepalive out immediately so the upgrade doesn't wait for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
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",
|