@decentnetwork/peer 0.1.123 → 0.1.126
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 +133 -18
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -109,6 +109,15 @@ const FRIEND_PING_INTERVAL_MS = readEnvInt("DECENT_FRIEND_PING_INTERVAL_MS", 400
|
|
|
109
109
|
// middle of toxcore's reactive range — fast enough to keep responsiveness,
|
|
110
110
|
// slow enough not to burn CPU on idle peers. Tunable via env.
|
|
111
111
|
const FRIEND_CONNECTION_LOOP_MS = readEnvInt("DECENT_FRIEND_CONNECTION_LOOP_MS", 250);
|
|
112
|
+
/** Minimum gap between onion DHT-PK announces to the SAME friend.
|
|
113
|
+
*
|
|
114
|
+
* One announce fans out to several nodes and costs three X25519 scalar mults
|
|
115
|
+
* per node (one per onion layer), so this budget is the difference between an
|
|
116
|
+
* idle node and a busy core. Two call sites in #doFriendConnections need it —
|
|
117
|
+
* they previously each spelled the interval out, drifted apart (25s vs the
|
|
118
|
+
* enclosing 3s re-punch cadence), and the cheap one silently set the pace for
|
|
119
|
+
* the expensive one. Shared here so they cannot diverge again. */
|
|
120
|
+
const DHT_PK_ANNOUNCE_COOLDOWN_MS = readEnvInt("DECENT_DHT_PK_ANNOUNCE_COOLDOWN_MS", 25_000);
|
|
112
121
|
const FRIEND_TIMEOUT_MS = readEnvInt("DECENT_FRIEND_TIMEOUT_MS", 32000);
|
|
113
122
|
// A session whose keys we've PROVEN (decrypted ≥1 real packet from the peer) is
|
|
114
123
|
// NOT torn down at FRIEND_TIMEOUT_MS. A transient all-transport blackout — e.g.
|
|
@@ -379,6 +388,14 @@ export class Peer {
|
|
|
379
388
|
// session entry exists yet so the connection loop does not flood DHT-PK
|
|
380
389
|
// requests when route discovery keeps failing.
|
|
381
390
|
#dhtPkSendCooldown = new Map();
|
|
391
|
+
/** Last ONION-routed endpoint lookup per friend. Separate from
|
|
392
|
+
* #dhtPkSendCooldown: they are two different expensive operations for the
|
|
393
|
+
* same friend, and sharing one timestamp would let whichever ran first
|
|
394
|
+
* suppress the other for a full budget. */
|
|
395
|
+
#onionLookupCooldown = new Map();
|
|
396
|
+
/** Consecutive failed endpoint lookups per (friend,target) — drives how wide
|
|
397
|
+
* the next fan-out goes. Cleared on success. */
|
|
398
|
+
#onionLookupMisses = new Map();
|
|
382
399
|
// Per-friend cooldown for re-asserting the TCP-relay route toward an
|
|
383
400
|
// unconnected friend (the "accepted friend that never connects" wedge —
|
|
384
401
|
// requestRoute only fired once at startup and was never retried).
|
|
@@ -1884,7 +1901,7 @@ export class Peer {
|
|
|
1884
1901
|
this.#debugLog(`cookie response: no tcp relay accepted oob send for ${senderId}`);
|
|
1885
1902
|
}
|
|
1886
1903
|
}
|
|
1887
|
-
else if (this.#isUnroutableSelfSource(remote.address, remote.port)) {
|
|
1904
|
+
else if (this.#isUnroutableSelfSource(remote.address, remote.port, true)) {
|
|
1888
1905
|
// The request arrived sourced from our own TUN address (or our own
|
|
1889
1906
|
// socket) — an overlay routing artifact. Replying there loops into
|
|
1890
1907
|
// ourselves and dies. Answer over the relay OOB path instead, which
|
|
@@ -1979,7 +1996,7 @@ export class Peer {
|
|
|
1979
1996
|
// poison the UDP send-back path.
|
|
1980
1997
|
const fromTcp = this.#remoteIsTcp(remote);
|
|
1981
1998
|
if (!fromTcp) {
|
|
1982
|
-
this.#adoptRemote(matchedSession, remote.address, remote.port);
|
|
1999
|
+
this.#adoptRemote(matchedSession, remote.address, remote.port, true);
|
|
1983
2000
|
}
|
|
1984
2001
|
else {
|
|
1985
2002
|
matchedSession.hasTcpRoute = true;
|
|
@@ -2053,7 +2070,7 @@ export class Peer {
|
|
|
2053
2070
|
state.hasTcpRoute = true;
|
|
2054
2071
|
}
|
|
2055
2072
|
else {
|
|
2056
|
-
this.#adoptRemote(state, remote.address, remote.port);
|
|
2073
|
+
this.#adoptRemote(state, remote.address, remote.port, true);
|
|
2057
2074
|
}
|
|
2058
2075
|
// CONVERGENCE RESYNC: the peer is retransmitting the SAME session pubkey
|
|
2059
2076
|
// we already hold, so OUR view of the session looks fine. But if we've
|
|
@@ -2250,7 +2267,7 @@ export class Peer {
|
|
|
2250
2267
|
// Don't set state.remote — there's no UDP endpoint for this peer.
|
|
2251
2268
|
}
|
|
2252
2269
|
else {
|
|
2253
|
-
this.#adoptRemote(state, remote.address, remote.port);
|
|
2270
|
+
this.#adoptRemote(state, remote.address, remote.port, true);
|
|
2254
2271
|
}
|
|
2255
2272
|
if (isNewSession) {
|
|
2256
2273
|
state.sendPacketNumber = 0;
|
|
@@ -2439,7 +2456,7 @@ export class Peer {
|
|
|
2439
2456
|
}
|
|
2440
2457
|
state.lastRelayRecvMs = Date.now();
|
|
2441
2458
|
}
|
|
2442
|
-
else if (this.#isUnroutableSelfSource(remote.address, remote.port)) {
|
|
2459
|
+
else if (this.#isUnroutableSelfSource(remote.address, remote.port, true)) {
|
|
2443
2460
|
// Sourced from our own TUN address / own socket (overlay routing
|
|
2444
2461
|
// artifact): the payload is real — deliver it — but this is NOT a
|
|
2445
2462
|
// usable bidirectional UDP path. Don't adopt it (replies loop into
|
|
@@ -2452,7 +2469,7 @@ export class Peer {
|
|
|
2452
2469
|
// Tailscale-exit hairpin source. Cheap string compare, NO interface
|
|
2453
2470
|
// syscall (that was the CCTV CPU regression) — safe at CCTV packet
|
|
2454
2471
|
// rates.
|
|
2455
|
-
this.#adoptRemote(state, remote.address, remote.port);
|
|
2472
|
+
this.#adoptRemote(state, remote.address, remote.port, true);
|
|
2456
2473
|
// A successfully decrypted packet arriving from a private address is
|
|
2457
2474
|
// stronger proof than an endpoint hint: it demonstrates that this
|
|
2458
2475
|
// exact host:port already reaches us. Do not require the address to be
|
|
@@ -2467,7 +2484,7 @@ export class Peer {
|
|
|
2467
2484
|
if (!state.lanRemoteHost &&
|
|
2468
2485
|
isPrivateAddress(remote.address) &&
|
|
2469
2486
|
!isCgnatAddress(remote.address) &&
|
|
2470
|
-
!this.#isUnroutableSelfSource(remote.address, remote.port)) {
|
|
2487
|
+
!this.#isUnroutableSelfSource(remote.address, remote.port, true)) {
|
|
2471
2488
|
state.lanRemoteHost = remote.address;
|
|
2472
2489
|
this.#rememberEndpointCandidate(state, remote.address, remote.port);
|
|
2473
2490
|
this.#debugLog(`lan_lock_confirmed friend=${friendId} host=${remote.address}:${remote.port} (decrypted UDP source)`);
|
|
@@ -3060,12 +3077,53 @@ export class Peer {
|
|
|
3060
3077
|
return false;
|
|
3061
3078
|
}
|
|
3062
3079
|
const targetId = carrierIdFromPublicKey(searchPublicKey);
|
|
3080
|
+
// Per-(friend, target) budget, enforced HERE rather than at the call sites.
|
|
3081
|
+
//
|
|
3082
|
+
// One run of this fans out to up to 24 nodes and each onion request costs
|
|
3083
|
+
// three X25519 scalar mults (one per layer) — up to ~72 per call, tens of
|
|
3084
|
+
// ms of pure CPU. There are four call sites and gating them one at a time
|
|
3085
|
+
// just moves the cost to whichever one was missed: after gating only the
|
|
3086
|
+
// re-punch path, this function was STILL the top crypto entry on
|
|
3087
|
+
// smtp.gfax.cn. A choke point inside the function is the only version that
|
|
3088
|
+
// cannot be bypassed by a call site added later.
|
|
3089
|
+
//
|
|
3090
|
+
// Keyed by target, not just friend, so the dhtPk lookup and the
|
|
3091
|
+
// onion-routed fallback for the same friend keep independent budgets —
|
|
3092
|
+
// throttling repeats of the SAME search, never the first try of a
|
|
3093
|
+
// different one.
|
|
3094
|
+
const budgetKey = `${friendId}:${targetId}`;
|
|
3095
|
+
const lastLookup = this.#onionLookupCooldown.get(budgetKey) ?? 0;
|
|
3096
|
+
const sinceLast = Date.now() - lastLookup;
|
|
3097
|
+
if (sinceLast < DHT_PK_ANNOUNCE_COOLDOWN_MS) {
|
|
3098
|
+
this.#debugLog(`endpoint lookup for ${friendId} skipped: ${Math.round(sinceLast / 1000)}s since last ` +
|
|
3099
|
+
`(budget ${DHT_PK_ANNOUNCE_COOLDOWN_MS / 1000}s)`);
|
|
3100
|
+
return false;
|
|
3101
|
+
}
|
|
3102
|
+
this.#onionLookupCooldown.set(budgetKey, Date.now());
|
|
3103
|
+
// Adaptive fan-out. Each node in this queue costs TWO announce round trips
|
|
3104
|
+
// (ping id, then the real query) and every onion request is three X25519
|
|
3105
|
+
// scalar mults — so a full 24-node sweep is ~144 scalar mults, and the loop
|
|
3106
|
+
// only exits early on SUCCESS. In steady state the lookups that repeat are
|
|
3107
|
+
// exactly the ones that keep failing, so the old fixed width paid the
|
|
3108
|
+
// maximum price precisely when it was least likely to pay off: measured
|
|
3109
|
+
// ~10-23 announce sends/sec on smtp.gfax.cn, ~28% of a core.
|
|
3110
|
+
//
|
|
3111
|
+
// First attempt keeps the full width — that is the one most likely to find
|
|
3112
|
+
// the peer, and discovery reliability is not what we are trading away.
|
|
3113
|
+
// Repeats narrow to a probe, and every 8th attempt goes wide again so a
|
|
3114
|
+
// peer that only just came back is still found without waiting for a
|
|
3115
|
+
// restart. Widening beats a permanently narrow sweep: the failure we must
|
|
3116
|
+
// not introduce is "never discovers", not "discovers a bit later".
|
|
3117
|
+
const misses = this.#onionLookupMisses.get(budgetKey) ?? 0;
|
|
3118
|
+
const fullWidth = Math.max(8, Math.min(24, MAX_FRIEND_ROUTE_ATTEMPTS));
|
|
3119
|
+
const width = misses === 0 || misses % 8 === 0 ? fullWidth : 4;
|
|
3063
3120
|
const queue = dedupeNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes)
|
|
3064
3121
|
.filter((n) => !this.#isNodeBlacklisted(`${n.host}:${n.port}`))
|
|
3065
3122
|
.sort((a, b) => this.#nodeScore(`${b.host}:${b.port}`) - this.#nodeScore(`${a.host}:${a.port}`))
|
|
3066
|
-
.slice(0,
|
|
3123
|
+
.slice(0, width);
|
|
3067
3124
|
const directKnown = queue.find((node) => node.pk === targetId);
|
|
3068
3125
|
if (directKnown) {
|
|
3126
|
+
this.#onionLookupMisses.delete(budgetKey);
|
|
3069
3127
|
this.#cacheFriendRemote(friendId, directKnown.host, directKnown.port);
|
|
3070
3128
|
this.#debugLog(`friend endpoint matched known node for ${friendId} at ${directKnown.host}:${directKnown.port}`);
|
|
3071
3129
|
return true;
|
|
@@ -3122,11 +3180,15 @@ export class Peer {
|
|
|
3122
3180
|
}
|
|
3123
3181
|
const exact = discovered.find((n) => n.pk === targetId);
|
|
3124
3182
|
if (exact) {
|
|
3183
|
+
this.#onionLookupMisses.delete(budgetKey);
|
|
3125
3184
|
this.#cacheFriendRemote(friendId, exact.host, exact.port);
|
|
3126
3185
|
this.#debugLog(`friend endpoint discovered for ${friendId} at ${exact.host}:${exact.port}`);
|
|
3127
3186
|
return true;
|
|
3128
3187
|
}
|
|
3129
3188
|
}
|
|
3189
|
+
// Nothing found: remember, so the next sweep for this target probes
|
|
3190
|
+
// narrowly instead of paying the full 24-node price again.
|
|
3191
|
+
this.#onionLookupMisses.set(budgetKey, misses + 1);
|
|
3130
3192
|
return false;
|
|
3131
3193
|
}
|
|
3132
3194
|
async #announceSelfBestEffort(force = false, deadlineMs = Number.POSITIVE_INFINITY) {
|
|
@@ -3547,9 +3609,29 @@ export class Peer {
|
|
|
3547
3609
|
if (friendRealPk && friendRealPk.length === 32) {
|
|
3548
3610
|
// 1. Re-announce our DHT-PK so peer learns *our* UDP endpoint
|
|
3549
3611
|
// even if their own DHT lookup against us was stale.
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3612
|
+
//
|
|
3613
|
+
// This shares #dhtPkSendCooldown with the periodic branch
|
|
3614
|
+
// below ON PURPOSE. The two operations in this `if` have very
|
|
3615
|
+
// different costs and must not share the 3s cadence: the
|
|
3616
|
+
// endpoint offer above is one UDP packet, while an onion
|
|
3617
|
+
// DHT-PK announce fans out to several nodes and does THREE
|
|
3618
|
+
// X25519 scalar mults per node (one per onion layer). At 3s
|
|
3619
|
+
// per friend that dominated the CPU of any node with a few
|
|
3620
|
+
// idle relay friends — measured on smtp.gfax.cn: 82% of wall
|
|
3621
|
+
// clock inside nacl, 83% of it under createOnionRequest0,
|
|
3622
|
+
// against 2% on a node with none. Note the trigger is NOT a
|
|
3623
|
+
// broken friend: udpConfirmed needs UDP traffic within 4s, so
|
|
3624
|
+
// a perfectly healthy but idle relay friend qualifies forever.
|
|
3625
|
+
//
|
|
3626
|
+
// Keep the offer at 3s (cheap, and it is what actually
|
|
3627
|
+
// re-punches); let the expensive announce keep its 25s budget.
|
|
3628
|
+
const lastAnnounce = this.#dhtPkSendCooldown.get(friendId) ?? 0;
|
|
3629
|
+
if (now - lastAnnounce > DHT_PK_ANNOUNCE_COOLDOWN_MS) {
|
|
3630
|
+
this.#dhtPkSendCooldown.set(friendId, now);
|
|
3631
|
+
void this.#sendOnionDhtPk(friendRealPk).catch((error) => {
|
|
3632
|
+
this.#debugLog(`UDP retry: dhtpk_send for ${friendId} failed: ${error.message}`);
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3553
3635
|
// 2. Try to discover the peer's UDP endpoint and kick a
|
|
3554
3636
|
// fresh cookie request via UDP. discoverAndCacheFriend
|
|
3555
3637
|
// Endpoint accepts EITHER the DHT-PK (preferred —
|
|
@@ -3560,6 +3642,19 @@ export class Peer {
|
|
|
3560
3642
|
// session.friendDhtPublicKey is null. Falling back
|
|
3561
3643
|
// to friendRealPk keeps the retry from being a no-op
|
|
3562
3644
|
// in exactly the case it was designed to fix.
|
|
3645
|
+
// COST NOTE: those two lookups are not the same price. With a
|
|
3646
|
+
// dhtPk it is a direct DHT query. Without one it is an
|
|
3647
|
+
// ONION-routed lookup — several nodes, three X25519 scalar
|
|
3648
|
+
// mults each — and the fallback fires precisely on
|
|
3649
|
+
// relay-only sessions, which are the long-lived ones. At the
|
|
3650
|
+
// 3s re-punch cadence that made it the single biggest CPU
|
|
3651
|
+
// consumer on a node with idle relay friends (measured on
|
|
3652
|
+
// smtp.gfax.cn: 31% of all crypto time entered here). So the
|
|
3653
|
+
// cheap direct lookup keeps the 3s cadence and only the onion
|
|
3654
|
+
// fallback is charged against the announce budget.
|
|
3655
|
+
// The per-(friend, target) budget now lives INSIDE
|
|
3656
|
+
// #discoverAndCacheFriendEndpoint, so this call site stays
|
|
3657
|
+
// simple and every other caller is covered by the same gate.
|
|
3563
3658
|
const dhtPk = session.friendDhtPublicKey;
|
|
3564
3659
|
const searchKey = dhtPk ?? friendRealPk;
|
|
3565
3660
|
if (searchKey && searchKey.length === 32) {
|
|
@@ -3593,8 +3688,7 @@ export class Peer {
|
|
|
3593
3688
|
// bootstrap nodes). Per-friend cooldown via #dhtPkSendCooldown so we
|
|
3594
3689
|
// don't flood onion data requests every 5s loop tick.
|
|
3595
3690
|
const lastDhtPkSent = this.#dhtPkSendCooldown.get(friendId) ?? 0;
|
|
3596
|
-
|
|
3597
|
-
if (now - lastDhtPkSent > dhtPkInterval) {
|
|
3691
|
+
if (now - lastDhtPkSent > DHT_PK_ANNOUNCE_COOLDOWN_MS) {
|
|
3598
3692
|
let friendRealPk = session?.friendRealPublicKey;
|
|
3599
3693
|
if (!friendRealPk && friend.address) {
|
|
3600
3694
|
try {
|
|
@@ -4610,7 +4704,10 @@ export class Peer {
|
|
|
4610
4704
|
}
|
|
4611
4705
|
// An unroutable self-source is never the friend's endpoint (see
|
|
4612
4706
|
// #adoptRemote) — don't let it poison the in-memory record either.
|
|
4613
|
-
|
|
4707
|
+
// observed=false: this path takes SPECULATIVE hints (DHT-PK extras,
|
|
4708
|
+
// sendnodes-derived knownNodes), so a self-address here is a claim, not
|
|
4709
|
+
// proof of a same-host peer.
|
|
4710
|
+
if (this.#isUnroutableSelfSource(host, port, false)) {
|
|
4614
4711
|
return;
|
|
4615
4712
|
}
|
|
4616
4713
|
// Important: do NOT persist this candidate to disk. Speculative endpoint
|
|
@@ -4661,19 +4758,37 @@ export class Peer {
|
|
|
4661
4758
|
* router and loop), or it's literally our own UDP socket. A self
|
|
4662
4759
|
* PHYSICAL address with a DIFFERENT port is routable — that's a
|
|
4663
4760
|
* same-host peer (e.g. the iOS simulator running on this Mac). */
|
|
4664
|
-
#isUnroutableSelfSource(host, port) {
|
|
4761
|
+
#isUnroutableSelfSource(host, port, observed) {
|
|
4665
4762
|
if (isOwnVirtualAddress(host))
|
|
4666
4763
|
return true;
|
|
4667
4764
|
if (!isOwnAddress(host))
|
|
4668
4765
|
return false;
|
|
4669
4766
|
const ourPort = this.#udp?.localPort();
|
|
4670
|
-
|
|
4767
|
+
if (ourPort !== undefined && port === ourPort)
|
|
4768
|
+
return true;
|
|
4769
|
+
// Our own physical IP on a DIFFERENT port is a same-host peer (the iOS
|
|
4770
|
+
// simulator on this Mac) — but only when we actually RECEIVED a packet
|
|
4771
|
+
// from there. A peer merely CLAIMING that address in its candidate list
|
|
4772
|
+
// is not evidence: private ranges are reused everywhere, so two machines
|
|
4773
|
+
// on unrelated networks routinely share one. Seen in the field between
|
|
4774
|
+
// ubuntu-aaec and air-wli, both sitting on a 172.29/20: aaec adopted its
|
|
4775
|
+
// OWN eth0 address as air-wli's endpoint and sent every IP packet to
|
|
4776
|
+
// itself — friend "online", chat fine, ping 100% loss, and not a single
|
|
4777
|
+
// drop recorded because nothing was dropped, just delivered to the wrong
|
|
4778
|
+
// machine.
|
|
4779
|
+
return !observed;
|
|
4671
4780
|
}
|
|
4672
|
-
|
|
4781
|
+
/**
|
|
4782
|
+
* @param observed true when host:port is the source of a datagram we
|
|
4783
|
+
* actually received, false when it is an address the peer advertised.
|
|
4784
|
+
* The distinction matters for self-addresses — see
|
|
4785
|
+
* {@link #isUnroutableSelfSource}.
|
|
4786
|
+
*/
|
|
4787
|
+
#adoptRemote(session, host, port, observed = false) {
|
|
4673
4788
|
// NEVER adopt an unroutable self-source as a friend's remote — replies
|
|
4674
4789
|
// would loop into our own interface and die, the peer never hears back
|
|
4675
4790
|
// and re-handshakes forever.
|
|
4676
|
-
if (this.#isUnroutableSelfSource(host, port)) {
|
|
4791
|
+
if (this.#isUnroutableSelfSource(host, port, observed)) {
|
|
4677
4792
|
return;
|
|
4678
4793
|
}
|
|
4679
4794
|
if (session.lanRemoteHost &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.126",
|
|
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",
|