@decentnetwork/peer 0.1.124 → 0.1.127

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 +50 -20
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -393,8 +393,9 @@ export class Peer {
393
393
  * same friend, and sharing one timestamp would let whichever ran first
394
394
  * suppress the other for a full budget. */
395
395
  #onionLookupCooldown = new Map();
396
- /** Consecutive failed endpoint lookups per (friend,target) drives how wide
397
- * the next fan-out goes. Cleared on success. */
396
+ /** Consecutive failed endpoint lookups per (friend,target). Kept for
397
+ * diagnostics — it no longer narrows the sweep (see the width comment in
398
+ * #discoverAndCacheFriendEndpoint for why that was reverted). */
398
399
  #onionLookupMisses = new Map();
399
400
  // Per-friend cooldown for re-asserting the TCP-relay route toward an
400
401
  // unconnected friend (the "accepted friend that never connects" wedge —
@@ -1901,7 +1902,7 @@ export class Peer {
1901
1902
  this.#debugLog(`cookie response: no tcp relay accepted oob send for ${senderId}`);
1902
1903
  }
1903
1904
  }
1904
- else if (this.#isUnroutableSelfSource(remote.address, remote.port)) {
1905
+ else if (this.#isUnroutableSelfSource(remote.address, remote.port, true)) {
1905
1906
  // The request arrived sourced from our own TUN address (or our own
1906
1907
  // socket) — an overlay routing artifact. Replying there loops into
1907
1908
  // ourselves and dies. Answer over the relay OOB path instead, which
@@ -1996,7 +1997,7 @@ export class Peer {
1996
1997
  // poison the UDP send-back path.
1997
1998
  const fromTcp = this.#remoteIsTcp(remote);
1998
1999
  if (!fromTcp) {
1999
- this.#adoptRemote(matchedSession, remote.address, remote.port);
2000
+ this.#adoptRemote(matchedSession, remote.address, remote.port, true);
2000
2001
  }
2001
2002
  else {
2002
2003
  matchedSession.hasTcpRoute = true;
@@ -2070,7 +2071,7 @@ export class Peer {
2070
2071
  state.hasTcpRoute = true;
2071
2072
  }
2072
2073
  else {
2073
- this.#adoptRemote(state, remote.address, remote.port);
2074
+ this.#adoptRemote(state, remote.address, remote.port, true);
2074
2075
  }
2075
2076
  // CONVERGENCE RESYNC: the peer is retransmitting the SAME session pubkey
2076
2077
  // we already hold, so OUR view of the session looks fine. But if we've
@@ -2267,7 +2268,7 @@ export class Peer {
2267
2268
  // Don't set state.remote — there's no UDP endpoint for this peer.
2268
2269
  }
2269
2270
  else {
2270
- this.#adoptRemote(state, remote.address, remote.port);
2271
+ this.#adoptRemote(state, remote.address, remote.port, true);
2271
2272
  }
2272
2273
  if (isNewSession) {
2273
2274
  state.sendPacketNumber = 0;
@@ -2456,7 +2457,7 @@ export class Peer {
2456
2457
  }
2457
2458
  state.lastRelayRecvMs = Date.now();
2458
2459
  }
2459
- else if (this.#isUnroutableSelfSource(remote.address, remote.port)) {
2460
+ else if (this.#isUnroutableSelfSource(remote.address, remote.port, true)) {
2460
2461
  // Sourced from our own TUN address / own socket (overlay routing
2461
2462
  // artifact): the payload is real — deliver it — but this is NOT a
2462
2463
  // usable bidirectional UDP path. Don't adopt it (replies loop into
@@ -2469,7 +2470,7 @@ export class Peer {
2469
2470
  // Tailscale-exit hairpin source. Cheap string compare, NO interface
2470
2471
  // syscall (that was the CCTV CPU regression) — safe at CCTV packet
2471
2472
  // rates.
2472
- this.#adoptRemote(state, remote.address, remote.port);
2473
+ this.#adoptRemote(state, remote.address, remote.port, true);
2473
2474
  // A successfully decrypted packet arriving from a private address is
2474
2475
  // stronger proof than an endpoint hint: it demonstrates that this
2475
2476
  // exact host:port already reaches us. Do not require the address to be
@@ -2484,7 +2485,7 @@ export class Peer {
2484
2485
  if (!state.lanRemoteHost &&
2485
2486
  isPrivateAddress(remote.address) &&
2486
2487
  !isCgnatAddress(remote.address) &&
2487
- !this.#isUnroutableSelfSource(remote.address, remote.port)) {
2488
+ !this.#isUnroutableSelfSource(remote.address, remote.port, true)) {
2488
2489
  state.lanRemoteHost = remote.address;
2489
2490
  this.#rememberEndpointCandidate(state, remote.address, remote.port);
2490
2491
  this.#debugLog(`lan_lock_confirmed friend=${friendId} host=${remote.address}:${remote.port} (decrypted UDP source)`);
@@ -3114,9 +3115,18 @@ export class Peer {
3114
3115
  // peer that only just came back is still found without waiting for a
3115
3116
  // restart. Widening beats a permanently narrow sweep: the failure we must
3116
3117
  // 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;
3118
+ // Width stays FULL. An earlier cut narrowed repeats to 4 nodes and only
3119
+ // went wide every 8th attempt — with the 25s budget that put 200s between
3120
+ // full sweeps, and iOS peers (which come online in short windows and are
3121
+ // the hardest to find at the best of times) stopped being seen at all.
3122
+ // Reported from the field the same day: weili.beagles.eth went offline and
3123
+ // stayed offline.
3124
+ //
3125
+ // Cost here is width x frequency, and the frequency gate above already
3126
+ // took 3s -> 25s, an 8x cut on its own. Narrowing width bought another 6x
3127
+ // and paid for it with the one property this code exists to provide.
3128
+ // Keep the cheap half, drop the expensive one.
3129
+ const width = Math.max(8, Math.min(24, MAX_FRIEND_ROUTE_ATTEMPTS));
3120
3130
  const queue = dedupeNodes(this.#knownNodes.length > 0 ? this.#knownNodes : this.#opts.bootstrapNodes)
3121
3131
  .filter((n) => !this.#isNodeBlacklisted(`${n.host}:${n.port}`))
3122
3132
  .sort((a, b) => this.#nodeScore(`${b.host}:${b.port}`) - this.#nodeScore(`${a.host}:${a.port}`))
@@ -3186,9 +3196,8 @@ export class Peer {
3186
3196
  return true;
3187
3197
  }
3188
3198
  }
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);
3199
+ // Nothing found: count it, for diagnostics only.
3200
+ this.#onionLookupMisses.set(budgetKey, (this.#onionLookupMisses.get(budgetKey) ?? 0) + 1);
3192
3201
  return false;
3193
3202
  }
3194
3203
  async #announceSelfBestEffort(force = false, deadlineMs = Number.POSITIVE_INFINITY) {
@@ -4704,7 +4713,10 @@ export class Peer {
4704
4713
  }
4705
4714
  // An unroutable self-source is never the friend's endpoint (see
4706
4715
  // #adoptRemote) — don't let it poison the in-memory record either.
4707
- if (this.#isUnroutableSelfSource(host, port)) {
4716
+ // observed=false: this path takes SPECULATIVE hints (DHT-PK extras,
4717
+ // sendnodes-derived knownNodes), so a self-address here is a claim, not
4718
+ // proof of a same-host peer.
4719
+ if (this.#isUnroutableSelfSource(host, port, false)) {
4708
4720
  return;
4709
4721
  }
4710
4722
  // Important: do NOT persist this candidate to disk. Speculative endpoint
@@ -4755,19 +4767,37 @@ export class Peer {
4755
4767
  * router and loop), or it's literally our own UDP socket. A self
4756
4768
  * PHYSICAL address with a DIFFERENT port is routable — that's a
4757
4769
  * same-host peer (e.g. the iOS simulator running on this Mac). */
4758
- #isUnroutableSelfSource(host, port) {
4770
+ #isUnroutableSelfSource(host, port, observed) {
4759
4771
  if (isOwnVirtualAddress(host))
4760
4772
  return true;
4761
4773
  if (!isOwnAddress(host))
4762
4774
  return false;
4763
4775
  const ourPort = this.#udp?.localPort();
4764
- return ourPort !== undefined && port === ourPort;
4776
+ if (ourPort !== undefined && port === ourPort)
4777
+ return true;
4778
+ // Our own physical IP on a DIFFERENT port is a same-host peer (the iOS
4779
+ // simulator on this Mac) — but only when we actually RECEIVED a packet
4780
+ // from there. A peer merely CLAIMING that address in its candidate list
4781
+ // is not evidence: private ranges are reused everywhere, so two machines
4782
+ // on unrelated networks routinely share one. Seen in the field between
4783
+ // ubuntu-aaec and air-wli, both sitting on a 172.29/20: aaec adopted its
4784
+ // OWN eth0 address as air-wli's endpoint and sent every IP packet to
4785
+ // itself — friend "online", chat fine, ping 100% loss, and not a single
4786
+ // drop recorded because nothing was dropped, just delivered to the wrong
4787
+ // machine.
4788
+ return !observed;
4765
4789
  }
4766
- #adoptRemote(session, host, port) {
4790
+ /**
4791
+ * @param observed true when host:port is the source of a datagram we
4792
+ * actually received, false when it is an address the peer advertised.
4793
+ * The distinction matters for self-addresses — see
4794
+ * {@link #isUnroutableSelfSource}.
4795
+ */
4796
+ #adoptRemote(session, host, port, observed = false) {
4767
4797
  // NEVER adopt an unroutable self-source as a friend's remote — replies
4768
4798
  // would loop into our own interface and die, the peer never hears back
4769
4799
  // and re-handshakes forever.
4770
- if (this.#isUnroutableSelfSource(host, port)) {
4800
+ if (this.#isUnroutableSelfSource(host, port, observed)) {
4771
4801
  return;
4772
4802
  }
4773
4803
  if (session.lanRemoteHost &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.124",
3
+ "version": "0.1.127",
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",