@decentnetwork/peer 0.1.99 → 0.1.100

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 +72 -4
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -1893,10 +1893,33 @@ export class Peer {
1893
1893
  // breaker" churned because it accepted new keys with NO such evidence).
1894
1894
  const peerReKeyed = state.undecryptableRecvMs !== undefined &&
1895
1895
  state.undecryptableRecvMs > (state.lastPingRecvMs ?? 0);
1896
- if (sinceEstablished > 1000 && currentSessionAlive && !peerReKeyed) {
1897
- this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ${sinceEstablished}ms after establish)`);
1896
+ const now = Date.now();
1897
+ const sameRehsKey = state.rehsSessionPublicKey !== undefined &&
1898
+ bytesEqual(state.rehsSessionPublicKey, hs.sessionPublicKey);
1899
+ const rehsWindowExpired = state.rehsLastMs === undefined ||
1900
+ now - state.rehsLastMs > REHS_WINDOW_MS;
1901
+ if (!sameRehsKey || rehsWindowExpired) {
1902
+ state.rehsSessionPublicKey = hs.sessionPublicKey.slice();
1903
+ state.rehsCount = 1;
1904
+ state.rehsFirstMs = now;
1905
+ }
1906
+ else {
1907
+ state.rehsCount = (state.rehsCount ?? 0) + 1;
1908
+ }
1909
+ state.rehsLastMs = now;
1910
+ const rehsElapsed = now - (state.rehsFirstMs ?? now);
1911
+ const persistentRehs = (state.rehsCount ?? 0) >= REHS_ACCEPT_COUNT &&
1912
+ rehsElapsed >= REHS_ACCEPT_MS &&
1913
+ rehsElapsed <= REHS_WINDOW_MS;
1914
+ if (sinceEstablished > 1000 && currentSessionAlive && !peerReKeyed && !persistentRehs) {
1915
+ this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ` +
1916
+ `${sinceEstablished}ms after establish, attempts=${state.rehsCount ?? 0})`);
1898
1917
  return;
1899
1918
  }
1919
+ if (persistentRehs) {
1920
+ this.#debugLog(`hs_recv accepting persistent re-handshake friend=${friendId} ` +
1921
+ `(attempts=${state.rehsCount}, elapsed=${rehsElapsed}ms)`);
1922
+ }
1900
1923
  if (peerReKeyed) {
1901
1924
  this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (peer re-keyed: undecryptable data since last good packet)`);
1902
1925
  }
@@ -1908,6 +1931,10 @@ export class Peer {
1908
1931
  state.sessionSharedKey = nacl.box.before(hs.sessionPublicKey, state.ourSessionKeyPair.secretKey);
1909
1932
  state.established = true;
1910
1933
  state.sessionEstablishedAtMs = Date.now();
1934
+ state.rehsCount = undefined;
1935
+ state.rehsFirstMs = undefined;
1936
+ state.rehsLastMs = undefined;
1937
+ state.rehsSessionPublicKey = undefined;
1911
1938
  // Fresh session up — reset the higher-pubkey defer window for the
1912
1939
  // next outage (see #initiateSession).
1913
1940
  this.#initiateDeferSinceMs.delete(friendId);
@@ -1936,7 +1963,13 @@ export class Peer {
1936
1963
  state.sendPacketNumber ??= 0;
1937
1964
  state.receiveBufferStart ??= 0;
1938
1965
  }
1939
- state.lastPingRecvMs = Date.now();
1966
+ // A handshake authenticates identities and proposes keys, but it does not
1967
+ // prove that CRYPTO_DATA encrypted under those keys works in both
1968
+ // directions. Keep a fresh session "unproven" until the first packet is
1969
+ // actually decrypted; otherwise a half-open session receives the 180s
1970
+ // proven-session blackout grace instead of the 32s recovery deadline.
1971
+ if (isNewSession)
1972
+ state.lastPingRecvMs = undefined;
1940
1973
  state.pendingEcho = undefined;
1941
1974
  state.pendingCookiePeerDhtPublicKey = undefined;
1942
1975
  this.#debugLog(`hs_recv friend=${friendId} initiated=${weInitiated ? 1 : 0} remote=${remote.address}:${remote.port}`);
@@ -2084,6 +2117,13 @@ export class Peer {
2084
2117
  // stays unset → udpFresh false → bulk data (files) never ride the LAN
2085
2118
  // even when LAN UDP is flowing both ways (the snoopy slow-file bug).
2086
2119
  state.lastPingRecvMs = Date.now();
2120
+ // Any successfully decrypted packet proves the current session is
2121
+ // healthy. Forget rejected new-key proposals so late/replayed
2122
+ // handshakes can never accumulate across healthy traffic.
2123
+ state.rehsCount = undefined;
2124
+ state.rehsFirstMs = undefined;
2125
+ state.rehsLastMs = undefined;
2126
+ state.rehsSessionPublicKey = undefined;
2087
2127
  if (this.#remoteIsTcp(remote)) {
2088
2128
  state.hasTcpRoute = true;
2089
2129
  }
@@ -2932,6 +2972,18 @@ export class Peer {
2932
2972
  // sessions a long grace so a blackout doesn't churn them via re-handshake;
2933
2973
  // unproven sessions still time out fast to self-heal a wedged handshake.
2934
2974
  const proven = session.lastPingRecvMs !== undefined;
2975
+ // A proven session normally gets a long blackout grace: if the network
2976
+ // disappears, keeping the known-good keys avoids unnecessary churn.
2977
+ // A restarted peer is different. Once our last good decrypt goes quiet
2978
+ // while fresh packets from that same friend keep failing authentication,
2979
+ // the transport is alive but the keys are not. Waiting the full proven
2980
+ // timeout turns every peer restart into a multi-minute wedge. Require a
2981
+ // sustained good-packet silence plus recent desync evidence so a single
2982
+ // late relay duplicate can never tear down a healthy session.
2983
+ const recentUndecryptable = session.undecryptableRecvMs !== undefined &&
2984
+ session.undecryptableRecvMs > lastAlive &&
2985
+ now - session.undecryptableRecvMs <= REINIT_STUCK_MS;
2986
+ const rekeyStuck = recentUndecryptable && silentFor > REINIT_ON_DESYNC_MS;
2935
2987
  // A relay-only session (synthetic tcp: remote, no UDP path) whose
2936
2988
  // relay route is gone has NO transport at all — nothing can arrive
2937
2989
  // and nothing we send leaves. The proven-session blackout grace
@@ -2942,7 +2994,23 @@ export class Peer {
2942
2994
  const relayOnly = !session.remote || session.remote.host.startsWith("tcp:");
2943
2995
  const pathless = relayOnly && !session.hasTcpRoute;
2944
2996
  const deadline = proven && !pathless ? PROVEN_SESSION_HARD_TIMEOUT_MS : FRIEND_TIMEOUT_MS;
2945
- if (proven && !pathless && silentFor > FRIEND_TIMEOUT_MS && silentFor <= deadline) {
2997
+ if (rekeyStuck) {
2998
+ this.#debugLog(`session re-key desync for ${friendId} (no good decrypt in ${silentFor}ms; ` +
2999
+ `undecryptable packets still arriving) — tearing down to re-handshake`);
3000
+ this.#friendSessions.delete(friendId);
3001
+ this.#setFriendOffline(friendId);
3002
+ try {
3003
+ const pk = base58ToBytes(friend.pubkey);
3004
+ if (pk.length === 32)
3005
+ this.#tcpRelays?.requestRoute(pk);
3006
+ const dhtPk = this.#friendDhtKeys.get(friendId);
3007
+ if (dhtPk && dhtPk.length === 32)
3008
+ this.#tcpRelays?.requestRoute(dhtPk);
3009
+ }
3010
+ catch { /* skip malformed */ }
3011
+ continue;
3012
+ }
3013
+ else if (proven && !pathless && silentFor > FRIEND_TIMEOUT_MS && silentFor <= deadline) {
2946
3014
  // Blackout grace: keep the proven session and keep probing (the
2947
3015
  // keepalive + UDP re-punch + relay-keepalive below all still run). Do
2948
3016
  // NOT delete or re-handshake — that's what desyncs and churns. The peer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
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",