@decentnetwork/peer 0.1.91 → 0.1.93

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 +63 -5
  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.
@@ -1725,7 +1745,45 @@ export class Peer {
1725
1745
  else {
1726
1746
  this.#adoptRemote(state, remote.address, remote.port);
1727
1747
  }
1728
- state.lastPingRecvMs = Date.now();
1748
+ // CONVERGENCE RESYNC: the peer is retransmitting the SAME session pubkey
1749
+ // we already hold, so OUR view of the session looks fine. But if we've
1750
+ // been receiving crypto data from this peer we CANNOT decrypt, the peer
1751
+ // is holding a STALE copy of OUR session pubkey (it re-keyed, or we did,
1752
+ // and our reply handshake never reached it) — it encrypts to a key we no
1753
+ // longer have, so every data packet is "no session matched" and it keeps
1754
+ // retransmitting its handshake forever. Silently preserving deadlocks the
1755
+ // session PERMANENTLY (observed callpass↔mac: 11h, 20k undecryptable, IP
1756
+ // + messages both dead while presence shows "online"). Re-send our reply
1757
+ // handshake so the peer re-derives its shared key against our CURRENT
1758
+ // session pubkey. Rate-limited (peer retransmits ~1/s); a healthy session
1759
+ // never sets undecryptableRecvMs, so this can NEVER churn a working one.
1760
+ const now = Date.now();
1761
+ const peerSendingUndecryptable = state.undecryptableRecvMs !== undefined &&
1762
+ now - state.undecryptableRecvMs < FRIEND_TIMEOUT_MS;
1763
+ const resyncDue = now - (state.lastResyncHandshakeMs ?? 0) > 2000;
1764
+ if (peerSendingUndecryptable && resyncDue) {
1765
+ state.lastResyncHandshakeMs = now;
1766
+ state.lastPingRecvMs = now;
1767
+ const reply = createCryptoHandshake({
1768
+ recipientCookie: hs.embeddedCookie,
1769
+ baseNonce: state.ourBaseNonce,
1770
+ sessionPublicKey: state.ourSessionKeyPair.publicKey,
1771
+ senderRealSecretKey: this.#keyPair.secretKey,
1772
+ senderRealPublicKey: this.#keyPair.publicKey,
1773
+ senderDhtPublicKey: this.#keyPair.publicKey,
1774
+ receiverRealPublicKey: hs.senderRealPublicKey,
1775
+ receiverDhtPublicKey: hs.senderDhtPublicKey,
1776
+ localCookieSymmetricKey: this.#cookieSymmetricKey
1777
+ });
1778
+ const sendResync = () => this.#remoteIsTcp(remote)
1779
+ ? Promise.resolve(this.#tcpRelays?.sendOobToFriend(hs.senderDhtPublicKey, reply) ?? 0)
1780
+ : this.#sendPacket(reply, { host: remote.address, port: remote.port });
1781
+ void sendResync()
1782
+ .then(() => this.#debugLog(`hs_recv duplicate but peer sent undecryptable data — re-sent our handshake to resync friend=${friendId}`))
1783
+ .catch((error) => this.#debugLog(`resync handshake failed for ${friendId}: ${error.message}`));
1784
+ return;
1785
+ }
1786
+ state.lastPingRecvMs = now;
1729
1787
  this.#debugVerboseLog(`hs_recv duplicate friend=${friendId} remote=${remote.address}:${remote.port} (session preserved)`);
1730
1788
  return;
1731
1789
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.91",
3
+ "version": "0.1.93",
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",