@decentnetwork/peer 0.1.56 → 0.1.58
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.d.ts +3 -0
- package/dist/peer.js +67 -9
- package/package.json +1 -1
package/dist/peer.d.ts
CHANGED
|
@@ -194,6 +194,9 @@ export declare class Peer {
|
|
|
194
194
|
* tcp-relay, discovery itself is broken — we don't even know
|
|
195
195
|
* where to send a cookie request. */
|
|
196
196
|
endpointCandidatesCount: number;
|
|
197
|
+
/** The actual candidate endpoints (host:port) — lets diag see whether a
|
|
198
|
+
* same-LAN host candidate (e.g. 10.0.0.x) was received but not adopted. */
|
|
199
|
+
endpointCandidates: string[];
|
|
197
200
|
/** Last time we sent an outbound cookie request via UDP for this
|
|
198
201
|
* peer (null = never). Used to confirm Phase 1.2 retries are
|
|
199
202
|
* actually firing. */
|
package/dist/peer.js
CHANGED
|
@@ -82,6 +82,14 @@ const FRIEND_PING_INTERVAL_MS = readEnvInt("DECENT_FRIEND_PING_INTERVAL_MS", 400
|
|
|
82
82
|
// slow enough not to burn CPU on idle peers. Tunable via env.
|
|
83
83
|
const FRIEND_CONNECTION_LOOP_MS = readEnvInt("DECENT_FRIEND_CONNECTION_LOOP_MS", 250);
|
|
84
84
|
const FRIEND_TIMEOUT_MS = readEnvInt("DECENT_FRIEND_TIMEOUT_MS", 32000);
|
|
85
|
+
// Half-open breaker: if the peer proposes a NEW session key this many times over
|
|
86
|
+
// this long while our session looks "alive", accept it (the peer can't decrypt
|
|
87
|
+
// our current session — a desynced half-open session, observed as a flood of
|
|
88
|
+
// "no session matched"). Each handshake carries a fresh cookie so these are
|
|
89
|
+
// genuine attempts, not replays. Bounded so it never fires on a healthy session.
|
|
90
|
+
const REHS_ACCEPT_COUNT = readEnvInt("DECENT_REHS_ACCEPT_COUNT", 4);
|
|
91
|
+
const REHS_ACCEPT_MS = readEnvInt("DECENT_REHS_ACCEPT_MS", 2500);
|
|
92
|
+
const REHS_WINDOW_MS = readEnvInt("DECENT_REHS_WINDOW_MS", 8000);
|
|
85
93
|
// How long a relay path stays "confirmed" (carrying bulk data on its own, no
|
|
86
94
|
// tcp-relay fan-out) after the last packet received over it. The relay
|
|
87
95
|
// keepalive refreshes this every ~4s on a healthy path, so a generous window
|
|
@@ -1062,6 +1070,7 @@ export class Peer {
|
|
|
1062
1070
|
ourLocalUdpPort: this.#udp?.localPort() ?? null,
|
|
1063
1071
|
friendUdpEndpoint: friendUdp,
|
|
1064
1072
|
endpointCandidatesCount: s.endpointCandidates?.length ?? 0,
|
|
1073
|
+
endpointCandidates: (s.endpointCandidates ?? []).map((c) => `${c.host}:${c.port}`),
|
|
1065
1074
|
cookieRequestSentMs: s.cookieRequestSentMs ?? null,
|
|
1066
1075
|
};
|
|
1067
1076
|
}
|
|
@@ -1435,12 +1444,30 @@ export class Peer {
|
|
|
1435
1444
|
const lastAlive = state.lastPingRecvMs ?? state.sessionEstablishedAtMs;
|
|
1436
1445
|
const currentSessionAlive = Date.now() - lastAlive < FRIEND_TIMEOUT_MS;
|
|
1437
1446
|
if (sinceEstablished > 1000 && currentSessionAlive) {
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
//
|
|
1441
|
-
|
|
1442
|
-
|
|
1447
|
+
// Half-open breaker. Our session looks alive — but "alive" only means
|
|
1448
|
+
// WE are receiving (possibly only over the relay). If the peer KEEPS
|
|
1449
|
+
// proposing a new key, THEIR side can't decrypt our packets: a desynced
|
|
1450
|
+
// half-open session (observed snoopy↔mac on one LAN as 288k "no session
|
|
1451
|
+
// matched"). Rejecting forever deadlocks. So count sustained attempts
|
|
1452
|
+
// and, past the threshold, accept the new key to re-converge. Bounded
|
|
1453
|
+
// to sustained attempts so it never disturbs a healthy session.
|
|
1454
|
+
const now = Date.now();
|
|
1455
|
+
if (state.rehsFirstMs === undefined || now - (state.rehsLastMs ?? 0) > REHS_WINDOW_MS) {
|
|
1456
|
+
state.rehsFirstMs = now;
|
|
1457
|
+
state.rehsCount = 0;
|
|
1458
|
+
}
|
|
1459
|
+
state.rehsLastMs = now;
|
|
1460
|
+
state.rehsCount = (state.rehsCount ?? 0) + 1;
|
|
1461
|
+
const stuck = state.rehsCount >= REHS_ACCEPT_COUNT && now - state.rehsFirstMs >= REHS_ACCEPT_MS;
|
|
1462
|
+
if (!stuck) {
|
|
1463
|
+
this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live session, attempt ${state.rehsCount}, ${sinceEstablished}ms after establish)`);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
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)`);
|
|
1443
1467
|
}
|
|
1468
|
+
// Converging on a fresh session — clear the breaker counter.
|
|
1469
|
+
state.rehsCount = 0;
|
|
1470
|
+
state.rehsFirstMs = undefined;
|
|
1444
1471
|
this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (current session wedged/dead, ` +
|
|
1445
1472
|
`lastPingRecv=${state.lastPingRecvMs ? Date.now() - state.lastPingRecvMs + "ms ago" : "never"})`);
|
|
1446
1473
|
}
|
|
@@ -1628,7 +1655,19 @@ export class Peer {
|
|
|
1628
1655
|
state.lastRelayRecvMs = Date.now();
|
|
1629
1656
|
}
|
|
1630
1657
|
else {
|
|
1631
|
-
|
|
1658
|
+
// Adopt the source as our send endpoint — but NEVER downgrade from a
|
|
1659
|
+
// same-LAN (physical) path to a non-LAN source. A packet that loops in
|
|
1660
|
+
// via the peer's public / Tailscale-exit endpoint must not knock us off
|
|
1661
|
+
// the direct LAN path, which is strictly better. (This is the other
|
|
1662
|
+
// half of the "use the physical LAN, not Tailscale" rule.)
|
|
1663
|
+
const cur = state.remote?.host;
|
|
1664
|
+
const onSameLanAlready = !!cur && !cur.startsWith("tcp:") && isPrivateAddress(cur) && !isCgnatAddress(cur) &&
|
|
1665
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(cur, s));
|
|
1666
|
+
const incomingSameLan = isPrivateAddress(remote.address) && !isCgnatAddress(remote.address) &&
|
|
1667
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(remote.address, s));
|
|
1668
|
+
if (incomingSameLan || !onSameLanAlready) {
|
|
1669
|
+
state.remote = { host: remote.address, port: remote.port };
|
|
1670
|
+
}
|
|
1632
1671
|
if (!state.lastUdpRecvMs) {
|
|
1633
1672
|
this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
|
|
1634
1673
|
}
|
|
@@ -3353,8 +3392,19 @@ export class Peer {
|
|
|
3353
3392
|
if (++ln >= 6)
|
|
3354
3393
|
clearInterval(lanTimer);
|
|
3355
3394
|
}, 120);
|
|
3356
|
-
|
|
3357
|
-
|
|
3395
|
+
// The physical LAN is strictly better than ANY public path — direct, no
|
|
3396
|
+
// NAT, no relay, and crucially no Tailscale-exit hairpin (a peer whose
|
|
3397
|
+
// internet egresses via Tailscale advertises its exit's public IP as
|
|
3398
|
+
// its srflx, so the "direct" public path actually loops out through
|
|
3399
|
+
// Tailscale and back). So adopt the LAN candidate as session.remote even
|
|
3400
|
+
// when we already have a public "real UDP" remote — UNLESS we're already
|
|
3401
|
+
// on a same-LAN address (don't flap between two LAN candidates). Without
|
|
3402
|
+
// this the session stuck to the public/Tailscale endpoint and never sent
|
|
3403
|
+
// over the LAN (observed: session.remote stayed public, data on relay).
|
|
3404
|
+
const cur = session.remote?.host;
|
|
3405
|
+
const onSameLanAlready = !!cur && !cur.startsWith("tcp:") && isPrivateAddress(cur) && !isCgnatAddress(cur) &&
|
|
3406
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(cur, s));
|
|
3407
|
+
if (!onSameLanAlready) {
|
|
3358
3408
|
session.remote = { host: lanHost, port: lanPort };
|
|
3359
3409
|
}
|
|
3360
3410
|
if (session.established) {
|
|
@@ -4920,7 +4970,15 @@ function isInIpv4Subnet(host, subnet) {
|
|
|
4920
4970
|
// path onto the overlay or loops into our own mesh, then silently falls back to
|
|
4921
4971
|
// the relay — the snoopy/lili "lastUdpRecv=never -> bufferbloat" path. (lili
|
|
4922
4972
|
// runs Tailscale; that's exactly this.)
|
|
4923
|
-
|
|
4973
|
+
// Names of interfaces that are NOT a real physical LAN — overlays (tailscale,
|
|
4974
|
+
// zerotier, wireguard, the agentnet TUN), and especially container/VM bridges.
|
|
4975
|
+
// Docker creates `docker0` AND per-network `br-<hash>` bridges (172.x); libvirt
|
|
4976
|
+
// `virbr0`; k8s `cni0`/`flannel`/`kube`. These were leaking their 172.x address
|
|
4977
|
+
// into the LAN host-candidate offer, so a peer on a real 10.x LAN advertised a
|
|
4978
|
+
// Docker-bridge IP its LAN partner could never match → LAN-direct never engaged
|
|
4979
|
+
// (observed: two boxes on one 10.0.0.0/24 stuck hairpinning through the public
|
|
4980
|
+
// internet). `^bridge` did NOT cover `br-…`.
|
|
4981
|
+
const VIRTUAL_IFACE_RE = /^(utun|tun|tap|wg|tailscale|zt|ham|agentnet|awdl|llw|gif|stf|bridge|br-|virbr|cni|flannel|weave|kube|vnet|vnic|vmnet|veth|docker)/i;
|
|
4924
4982
|
/** True for RFC 6598 carrier-grade-NAT space 100.64.0.0/10 (Tailscale's range). */
|
|
4925
4983
|
function isCgnatAddress(host) {
|
|
4926
4984
|
const o = host.split(".");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
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",
|