@decentnetwork/peer 0.1.92 → 0.1.94

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 +42 -8
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -593,10 +593,8 @@ export class Peer {
593
593
  };
594
594
  }
595
595
  async stop() {
596
- if (this.#tcpRelays) {
597
- await this.#tcpRelays.stop().catch(() => { });
598
- this.#tcpRelays = undefined;
599
- }
596
+ // Stop background producers first so no keepalive/profile packet can race
597
+ // behind the shutdown notice and make the remote session look live again.
600
598
  if (this.#expressPollTimer) {
601
599
  clearInterval(this.#expressPollTimer);
602
600
  this.#expressPollTimer = undefined;
@@ -613,6 +611,28 @@ export class Peer {
613
611
  clearInterval(this.#friendConnectionTimer);
614
612
  this.#friendConnectionTimer = undefined;
615
613
  }
614
+ // Gracefully retire every established net_crypto session before closing
615
+ // UDP/TCP. The identity key survives an AgentNet restart but the ephemeral
616
+ // session key does not. Without PACKET_ID_KILL, a service node keeps the old
617
+ // established session and rejects the restarted daemon's new-key handshake
618
+ // until its liveness timeout expires (or the service node is restarted).
619
+ // Carrier/toxcore peers already handle KILL by deleting that one session, so
620
+ // this is wire-compatible with older holders and does not disturb any other
621
+ // friend served by an exit node.
622
+ const establishedFriends = [...this.#friendSessions.entries()]
623
+ .filter(([, session]) => session.established)
624
+ .map(([friendId]) => friendId);
625
+ if (establishedFriends.length > 0) {
626
+ await Promise.allSettled(establishedFriends.map((friendId) => this.#sendMessengerPacket(friendId, PACKET_ID_KILL, new Uint8Array())));
627
+ // TcpRelayClient.close() destroys its socket. Give queued relay writes a
628
+ // brief chance to reach the relay before tearing transports down. UDP and
629
+ // TURN sends have already been handed to their sockets at this point.
630
+ await sleep(250);
631
+ }
632
+ if (this.#tcpRelays) {
633
+ await this.#tcpRelays.stop().catch(() => { });
634
+ this.#tcpRelays = undefined;
635
+ }
616
636
  this.#udp.off("datagram", this.#onDatagram);
617
637
  await this.#udp.stop();
618
638
  // Release the TURN allocation + its dedicated socket.
@@ -1740,10 +1760,19 @@ export class Peer {
1740
1760
  const now = Date.now();
1741
1761
  const peerSendingUndecryptable = state.undecryptableRecvMs !== undefined &&
1742
1762
  now - state.undecryptableRecvMs < FRIEND_TIMEOUT_MS;
1763
+ // A duplicate handshake can also be the first packet from a freshly
1764
+ // restarted peer whose KILL notice was lost during shutdown. The
1765
+ // holder still has the peer's old accepted session shell, while the
1766
+ // restarted process needs our handshake reply before it can derive the
1767
+ // shared key. Re-send our CURRENT handshake (same key, so no local
1768
+ // reset) even if we also initiated a cookie chain: after a blackout
1769
+ // both sides commonly initiate at once, so `weInitiated` cannot tell a
1770
+ // request from a reply here. The 2s rate limit bounds duplicate traffic;
1771
+ // the immediate reply cannot bounce because it arrives inside the
1772
+ // limit. This is also the normal lost-reply recovery path.
1743
1773
  const resyncDue = now - (state.lastResyncHandshakeMs ?? 0) > 2000;
1744
- if (peerSendingUndecryptable && resyncDue) {
1774
+ if (resyncDue) {
1745
1775
  state.lastResyncHandshakeMs = now;
1746
- state.lastPingRecvMs = now;
1747
1776
  const reply = createCryptoHandshake({
1748
1777
  recipientCookie: hs.embeddedCookie,
1749
1778
  baseNonce: state.ourBaseNonce,
@@ -1759,11 +1788,16 @@ export class Peer {
1759
1788
  ? Promise.resolve(this.#tcpRelays?.sendOobToFriend(hs.senderDhtPublicKey, reply) ?? 0)
1760
1789
  : this.#sendPacket(reply, { host: remote.address, port: remote.port });
1761
1790
  void sendResync()
1762
- .then(() => this.#debugLog(`hs_recv duplicate but peer sent undecryptable data — re-sent our handshake to resync friend=${friendId}`))
1791
+ .then(() => this.#debugLog(`hs_recv duplicate — re-sent our handshake to resync friend=${friendId} ` +
1792
+ `reason=${peerSendingUndecryptable ? "undecryptable-data" : "duplicate-handshake"}`))
1763
1793
  .catch((error) => this.#debugLog(`resync handshake failed for ${friendId}: ${error.message}`));
1764
1794
  return;
1765
1795
  }
1766
- state.lastPingRecvMs = now;
1796
+ // A handshake proves reachability, not that this net_crypto session can
1797
+ // decrypt data. Refreshing lastPingRecvMs here kept a stale session
1798
+ // immortal: the restarted peer retransmitted handshakes every second,
1799
+ // and each retransmit postponed the 32s dead-session breaker forever.
1800
+ // Only successfully decrypted CRYPTO_DATA may refresh liveness.
1767
1801
  this.#debugVerboseLog(`hs_recv duplicate friend=${friendId} remote=${remote.address}:${remote.port} (session preserved)`);
1768
1802
  return;
1769
1803
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.92",
3
+ "version": "0.1.94",
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",