@decentnetwork/peer 0.1.34 → 0.1.36

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 +74 -3
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -332,6 +332,14 @@ export class Peer {
332
332
  session.friendRealPublicKey ??= new Uint8Array(friendKey);
333
333
  this.#friendSessions.set(friendId, session);
334
334
  if (!session.established) {
335
+ // A fresh relay-online signal means the peer just (re)connected —
336
+ // almost always a restart. Clear any accumulated cookie backoff so
337
+ // the connection loop retries the handshake at the 8s base cadence
338
+ // instead of the 120s cap it had grown to while the peer was gone.
339
+ // Without this, a peer that restarts after a long outage sits
340
+ // "online but not established" for up to 2 minutes before the next
341
+ // handshake attempt — the exact cn/callpass post-restart wedge.
342
+ this.#cookieRetryCount.delete(friendId);
335
343
  void this.#initiateSession(friendId).catch(() => { });
336
344
  }
337
345
  });
@@ -1763,6 +1771,14 @@ export class Peer {
1763
1771
  this.#cacheFriendRemote(senderId, knownMatch.host, knownMatch.port, senderPublicKey, friendDhtPublicKey);
1764
1772
  this.#debugLog(`onion dhtpk matched known node for ${senderId} at ${knownMatch.host}:${knownMatch.port}`);
1765
1773
  }
1774
+ // A dhtpk_update is proof the friend is announcing and reachable. If our
1775
+ // session to them isn't established, clear the accumulated cookie backoff
1776
+ // so the connection loop retries the handshake at the base cadence rather
1777
+ // than the up-to-120s cap (mirrors the relay friendOnline reset; covers the
1778
+ // UDP/onion reconnect path after a restart).
1779
+ if (!this.#friendSessions.get(senderId)?.established) {
1780
+ this.#cookieRetryCount.delete(senderId);
1781
+ }
1766
1782
  this.#debugLog(`dhtpk_update friend=${senderId} noReplay=${noReplay.toString()} ` +
1767
1783
  `dhtpk=${carrierIdFromPublicKey(friendDhtPublicKey)} extraLen=${extra.length} ` +
1768
1784
  `extraNodes=${extraNodes.length} extraPreviewHex=${extraPreview}`);
@@ -3025,19 +3041,37 @@ export class Peer {
3025
3041
  const relay = await this.#ensureTurnRelay().catch(() => undefined);
3026
3042
  const relayOctets = relay ? relay.host.split(".").map((s) => parseInt(s, 10)) : undefined;
3027
3043
  const relayValid = relayOctets && relayOctets.length === 4 && !relayOctets.some((o) => Number.isNaN(o));
3028
- const payload = new Uint8Array(relayValid ? 12 : 6);
3044
+ // Third candidate (bytes 12-17): our primary private LAN address + local
3045
+ // UDP port. A same-LAN peer can punch this directly; without it the only
3046
+ // "direct" option we advertise is our srflx (public) address, which the
3047
+ // NAT must hairpin back onto the LAN — most don't, so two machines in one
3048
+ // office silently fall back to the TURN relay (~260ms for a <1ms hop).
3049
+ const lanIp = getLocalIpv4Addresses().find((ip) => isPrivateAddress(ip));
3050
+ const lanOctets = lanIp ? lanIp.split(".").map((s) => parseInt(s, 10)) : undefined;
3051
+ const lanPort = this.#udp.localPort() ?? 0;
3052
+ const lanValid = !!lanOctets && lanOctets.length === 4 && !lanOctets.some((o) => Number.isNaN(o)) && lanPort > 0;
3053
+ const size = lanValid ? 18 : relayValid ? 12 : 6;
3054
+ const payload = new Uint8Array(size);
3029
3055
  payload.set(octets, 0);
3030
3056
  payload[4] = (srflx.port >> 8) & 0xff;
3031
3057
  payload[5] = srflx.port & 0xff;
3032
- if (relayValid && relay) {
3058
+ // relay slot (bytes 6-11): filled when we have a relay, else left zero
3059
+ // (receiver skips port 0) so the host candidate can still occupy 12-17.
3060
+ if (relayValid && relay && size >= 12) {
3033
3061
  payload.set(relayOctets, 6);
3034
3062
  payload[10] = (relay.port >> 8) & 0xff;
3035
3063
  payload[11] = relay.port & 0xff;
3036
3064
  }
3065
+ if (lanValid) {
3066
+ payload.set(lanOctets, 12);
3067
+ payload[16] = (lanPort >> 8) & 0xff;
3068
+ payload[17] = lanPort & 0xff;
3069
+ }
3037
3070
  try {
3038
3071
  await this.#sendMessengerPacket(friendId, PACKET_ID_UDP_ENDPOINT, payload);
3039
3072
  this.#debugLog(`udp-endpoint offer sent to ${friendId}: direct=${srflx.host}:${srflx.port}` +
3040
- (relayValid && relay ? ` relay=${relay.host}:${relay.port}` : ""));
3073
+ (relayValid && relay ? ` relay=${relay.host}:${relay.port}` : "") +
3074
+ (lanValid ? ` lan=${lanIp}:${lanPort}` : ""));
3041
3075
  }
3042
3076
  catch {
3043
3077
  // best-effort — the retry loop will try again
@@ -3088,6 +3122,43 @@ export class Peer {
3088
3122
  }
3089
3123
  }
3090
3124
  }
3125
+ // LAN host candidate (bytes 12-17): the peer's private LAN address. Only
3126
+ // safe to try when it falls in OUR subnet — same physical LAN — otherwise
3127
+ // a peer's 192.168.1.x could collide with an unrelated host on our own
3128
+ // network. When it matches it's the fastest possible path (direct, no NAT,
3129
+ // no relay), so punch it and add it as a same-LAN endpoint candidate, which
3130
+ // #collectSessionEndpointCandidates prioritises for the session path. We
3131
+ // prefer it as session.remote (when we have no confirmed real UDP yet) so
3132
+ // the very next keepalive upgrades the path off the relay.
3133
+ if (payload.length >= 18) {
3134
+ const lanHost = `${payload[12]}.${payload[13]}.${payload[14]}.${payload[15]}`;
3135
+ const lanPort = ((payload[16] << 8) | payload[17]) >>> 0;
3136
+ const sameLan = lanPort !== 0 &&
3137
+ isPrivateAddress(lanHost) &&
3138
+ getLocalIpv4Subnets().some((s) => isInIpv4Subnet(lanHost, s)) &&
3139
+ !(getLocalIpv4Addresses().includes(lanHost) && this.#udp.localPort() === lanPort);
3140
+ if (sameLan) {
3141
+ this.#rememberEndpointCandidate(session, lanHost, lanPort);
3142
+ this.#debugLog(`udp-endpoint LAN candidate from ${friendId}: ${lanHost}:${lanPort} (same subnet) — punching`);
3143
+ const lanPunch = Uint8Array.of(0xf2);
3144
+ let ln = 0;
3145
+ const lanTimer = setInterval(() => {
3146
+ this.#udp.sendDirectSync(Buffer.from(lanPunch), lanHost, lanPort);
3147
+ if (++ln >= 6)
3148
+ clearInterval(lanTimer);
3149
+ }, 120);
3150
+ const haveRealUdp = session.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
3151
+ if (!haveRealUdp) {
3152
+ session.remote = { host: lanHost, port: lanPort };
3153
+ }
3154
+ if (session.established) {
3155
+ void this.#sendMessengerPacket(friendId, PACKET_ID_ALIVE, new Uint8Array()).catch(() => undefined);
3156
+ }
3157
+ else {
3158
+ void this.#initiateSession(friendId).catch(() => undefined);
3159
+ }
3160
+ }
3161
+ }
3091
3162
  // Don't punch toward ourselves.
3092
3163
  if (getLocalIpv4Addresses().includes(host) && this.#udp.localPort() === port)
3093
3164
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
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",