@decentnetwork/peer 0.1.113 → 0.1.115

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 +92 -17
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -340,6 +340,14 @@ export class Peer {
340
340
  * When a DHT packet later arrives from one of these sources, its sender pk
341
341
  * is that friend's CURRENT DHT key — see #refreshFriendDhtKeyFromDht. */
342
342
  #lanProbeTargets = new Map();
343
+ /** Exclusive turn-taking for OWN-HOST probe targets. Own-host targets
344
+ * (our IPs + loopback at native ports) are shared by every stranded
345
+ * friend, so concurrent probes overwrite each other's #lanProbeTargets
346
+ * mapping and the reply gets credited to whoever probed last (observed
347
+ * live: the simulator's key landed on an unrelated friend). One friend
348
+ * holds the slot at a time; per-friend candidate probes are unaffected. */
349
+ #ownHostProbeFriendId;
350
+ #ownHostProbeUntilMs = 0;
343
351
  #dhtMaintenanceTimer;
344
352
  // Per-friend last DHT-PK send time, keyed by friendId, used even when no
345
353
  // session entry exists yet so the connection loop does not flood DHT-PK
@@ -1800,6 +1808,20 @@ export class Peer {
1800
1808
  if (!fromTcp) {
1801
1809
  this.#cacheFriendRemote(senderId, remote.address, remote.port, request.senderRealPublicKey, request.senderDhtPublicKey);
1802
1810
  }
1811
+ else if (request.senderDhtPublicKey && request.senderDhtPublicKey.length === 32) {
1812
+ // TCP arrival: skip the endpoint, but STILL learn the sender's DHT
1813
+ // key. Natives rotate it every app restart and the relay routes
1814
+ // CRYPTO_DATA by it — a relay-only native (its UDP unreachable to
1815
+ // us) otherwise kept its stale key here, every routed send went to
1816
+ // a key the relay didn't know, and the peer — never receiving our
1817
+ // data — re-keyed every 8s forever (the iPhone churn loop).
1818
+ const session = this.#friendSessions.get(senderId);
1819
+ if (session && !(session.friendDhtPublicKey && Buffer.from(session.friendDhtPublicKey).equals(Buffer.from(request.senderDhtPublicKey)))) {
1820
+ this.#debugLog(`dht_key_from_tcp_cookie friend=${senderId} new=${carrierIdFromPublicKey(request.senderDhtPublicKey)}`);
1821
+ session.friendDhtPublicKey = request.senderDhtPublicKey;
1822
+ }
1823
+ this.#learnFriendDhtKey(senderId, request.senderDhtPublicKey);
1824
+ }
1803
1825
  const response = createCookieResponse({
1804
1826
  request,
1805
1827
  receiverDhtSecretKey: this.#keyPair.secretKey,
@@ -2063,8 +2085,18 @@ export class Peer {
2063
2085
  else {
2064
2086
  if (!state.friendRealPublicKey)
2065
2087
  state.friendRealPublicKey = hs.senderRealPublicKey;
2066
- if (!state.friendDhtPublicKey)
2088
+ // ALWAYS take the handshake's DHT key, not only when empty: the
2089
+ // handshake authenticates the sender's real key, and natives rotate
2090
+ // their DHT key every restart. Keeping a stale key here sent every
2091
+ // relay-routed CRYPTO_DATA packet to a key the relay didn't know —
2092
+ // silent drop, peer re-keys every 8s (iPhone churn loop).
2093
+ if (hs.senderDhtPublicKey && hs.senderDhtPublicKey.length === 32 &&
2094
+ !(state.friendDhtPublicKey && Buffer.from(state.friendDhtPublicKey).equals(Buffer.from(hs.senderDhtPublicKey)))) {
2095
+ if (state.friendDhtPublicKey) {
2096
+ this.#debugLog(`dht_key_from_hs friend=${friendId} new=${carrierIdFromPublicKey(hs.senderDhtPublicKey)}`);
2097
+ }
2067
2098
  state.friendDhtPublicKey = hs.senderDhtPublicKey;
2099
+ }
2068
2100
  }
2069
2101
  if (hs.senderDhtPublicKey && hs.senderDhtPublicKey.length === 32) {
2070
2102
  this.#learnFriendDhtKey(friendId, hs.senderDhtPublicKey);
@@ -5188,6 +5220,7 @@ export class Peer {
5188
5220
  };
5189
5221
  const tried = new Set();
5190
5222
  // (b) known candidates — refresh a rotated DHT key at a live endpoint.
5223
+ // Per-friend targets: no mapping ambiguity, always allowed.
5191
5224
  for (const candidate of connectCandidates) {
5192
5225
  const key = `${candidate.host}:${candidate.port}`;
5193
5226
  if (tried.has(key))
@@ -5196,17 +5229,25 @@ export class Peer {
5196
5229
  await probe(candidate.host, candidate.port, false); // cookie already sent above
5197
5230
  }
5198
5231
  // (a)+(b) own-host targets — find a same-host peer after an IP change.
5199
- for (const host of [...getLocalIpv4Addresses(), "127.0.0.1"]) {
5200
- if (isOwnVirtualAddress(host))
5201
- continue; // never probe into the overlay
5202
- for (const port of LAN_SWEEP_PORTS) {
5203
- if (port === ourLocalPort)
5204
- continue; // our own socket
5205
- const key = `${host}:${port}`;
5206
- if (tried.has(key))
5207
- continue;
5208
- tried.add(key);
5209
- await probe(host, port, true);
5232
+ // SHARED targets: only the current slot holder probes them, so the
5233
+ // reply maps to exactly one friend. Slot rotates by expiry; the
5234
+ // cookie backoff staggers contenders so every stranded friend
5235
+ // eventually gets a turn.
5236
+ if (this.#ownHostProbeFriendId === friendId || probeNow >= this.#ownHostProbeUntilMs) {
5237
+ this.#ownHostProbeFriendId = friendId;
5238
+ this.#ownHostProbeUntilMs = probeNow + 10_000;
5239
+ for (const host of [...getLocalIpv4Addresses(), "127.0.0.1"]) {
5240
+ if (isOwnVirtualAddress(host))
5241
+ continue; // never probe into the overlay
5242
+ for (const port of LAN_SWEEP_PORTS) {
5243
+ if (port === ourLocalPort)
5244
+ continue; // our own socket
5245
+ const key = `${host}:${port}`;
5246
+ if (tried.has(key))
5247
+ continue;
5248
+ tried.add(key);
5249
+ await probe(host, port, true);
5250
+ }
5210
5251
  }
5211
5252
  }
5212
5253
  }
@@ -5237,9 +5278,18 @@ export class Peer {
5237
5278
  }
5238
5279
  catch (error) {
5239
5280
  this.#debugLog(`cookie request send failed for ${friendId}: ${error.message}`);
5281
+ // A failed send is still a failed ATTEMPT: count it so (a) backoff
5282
+ // grows instead of hammering a dead address at base cadence forever,
5283
+ // and (b) the rescue probes (gated on retryCount >= 1) still fire for
5284
+ // a friend whose sole candidate errors at the syscall level — before
5285
+ // this, such a friend never probed and could never be rescued.
5286
+ this.#cookieRetryCount.set(friendId, (this.#cookieRetryCount.get(friendId) ?? 0) + 1);
5240
5287
  session.pendingEcho = undefined;
5241
5288
  session.pendingCookiePeerDhtPublicKey = undefined;
5242
- session.cookieRequestSentMs = undefined;
5289
+ // Keep cookieRequestSentMs (set at attempt start): it is the loop's
5290
+ // `lastAttempt`. Clearing it made lastAttempt 0, so the cooldown gate
5291
+ // always passed and a friend whose sends error retried every 250ms
5292
+ // tick forever — the "cookie request send failed" log flood.
5243
5293
  return false;
5244
5294
  }
5245
5295
  }
@@ -5486,11 +5536,36 @@ export class Peer {
5486
5536
  }
5487
5537
  if (!friendId)
5488
5538
  return;
5489
- // The sender identifies as an existing DIFFERENT friend (JS peers use
5490
- // their stable real key as DHT key)don't cross-wire.
5539
+ // If ANY established session already uses this DHT key, the key's owner
5540
+ // is known and it is not `friendId`a shared own-host target (the
5541
+ // simulator) answers EVERY prober's probe, and without this check each
5542
+ // stranded friend in turn "adopted" the sim's key during its slot,
5543
+ // endlessly poisoning its own routing.
5544
+ for (const [fid, s] of this.#friendSessions) {
5545
+ if (fid === friendId || !s.established || !s.friendDhtPublicKey)
5546
+ continue;
5547
+ if (Buffer.from(s.friendDhtPublicKey).equals(Buffer.from(senderDhtPk)))
5548
+ return;
5549
+ }
5550
+ // The sender pk currently maps to a DIFFERENT friend. If that friend's
5551
+ // session is ESTABLISHED, the key genuinely belongs to them — don't
5552
+ // cross-wire. If it is NOT established, the mapping itself may be a
5553
+ // mis-credited earlier probe reply (shared own-host targets), and
5554
+ // refusing here would permanently lock the true owner out — steal it
5555
+ // and scrub the other friend's stale copy so routing stops confusing
5556
+ // the two. A wrong steal self-corrects: the real-key handshake cannot
5557
+ // complete against the wrong node.
5491
5558
  const byDht = this.#friendByDhtPk(senderId);
5492
- if (byDht && byDht.friendId !== friendId)
5493
- return;
5559
+ if (byDht && byDht.friendId !== friendId) {
5560
+ const otherSession = this.#friendSessions.get(byDht.friendId);
5561
+ if (otherSession?.established)
5562
+ return;
5563
+ this.#debugLog(`dht_key_steal ${senderId} from=${byDht.friendId} to=${friendId} (other unestablished)`);
5564
+ if (otherSession?.friendDhtPublicKey && Buffer.from(otherSession.friendDhtPublicKey).equals(Buffer.from(senderDhtPk))) {
5565
+ otherSession.friendDhtPublicKey = undefined;
5566
+ }
5567
+ this.#friendDhtKeys.delete(byDht.friendId);
5568
+ }
5494
5569
  if (senderId === friendId)
5495
5570
  return; // already keyed by real pk — nothing to refresh
5496
5571
  const session = this.#friendSessions.get(friendId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
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",