@decentnetwork/peer 0.1.34 → 0.1.35

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 +58 -3
  2. package/package.json +1 -1
package/dist/peer.js CHANGED
@@ -3025,19 +3025,37 @@ export class Peer {
3025
3025
  const relay = await this.#ensureTurnRelay().catch(() => undefined);
3026
3026
  const relayOctets = relay ? relay.host.split(".").map((s) => parseInt(s, 10)) : undefined;
3027
3027
  const relayValid = relayOctets && relayOctets.length === 4 && !relayOctets.some((o) => Number.isNaN(o));
3028
- const payload = new Uint8Array(relayValid ? 12 : 6);
3028
+ // Third candidate (bytes 12-17): our primary private LAN address + local
3029
+ // UDP port. A same-LAN peer can punch this directly; without it the only
3030
+ // "direct" option we advertise is our srflx (public) address, which the
3031
+ // NAT must hairpin back onto the LAN — most don't, so two machines in one
3032
+ // office silently fall back to the TURN relay (~260ms for a <1ms hop).
3033
+ const lanIp = getLocalIpv4Addresses().find((ip) => isPrivateAddress(ip));
3034
+ const lanOctets = lanIp ? lanIp.split(".").map((s) => parseInt(s, 10)) : undefined;
3035
+ const lanPort = this.#udp.localPort() ?? 0;
3036
+ const lanValid = !!lanOctets && lanOctets.length === 4 && !lanOctets.some((o) => Number.isNaN(o)) && lanPort > 0;
3037
+ const size = lanValid ? 18 : relayValid ? 12 : 6;
3038
+ const payload = new Uint8Array(size);
3029
3039
  payload.set(octets, 0);
3030
3040
  payload[4] = (srflx.port >> 8) & 0xff;
3031
3041
  payload[5] = srflx.port & 0xff;
3032
- if (relayValid && relay) {
3042
+ // relay slot (bytes 6-11): filled when we have a relay, else left zero
3043
+ // (receiver skips port 0) so the host candidate can still occupy 12-17.
3044
+ if (relayValid && relay && size >= 12) {
3033
3045
  payload.set(relayOctets, 6);
3034
3046
  payload[10] = (relay.port >> 8) & 0xff;
3035
3047
  payload[11] = relay.port & 0xff;
3036
3048
  }
3049
+ if (lanValid) {
3050
+ payload.set(lanOctets, 12);
3051
+ payload[16] = (lanPort >> 8) & 0xff;
3052
+ payload[17] = lanPort & 0xff;
3053
+ }
3037
3054
  try {
3038
3055
  await this.#sendMessengerPacket(friendId, PACKET_ID_UDP_ENDPOINT, payload);
3039
3056
  this.#debugLog(`udp-endpoint offer sent to ${friendId}: direct=${srflx.host}:${srflx.port}` +
3040
- (relayValid && relay ? ` relay=${relay.host}:${relay.port}` : ""));
3057
+ (relayValid && relay ? ` relay=${relay.host}:${relay.port}` : "") +
3058
+ (lanValid ? ` lan=${lanIp}:${lanPort}` : ""));
3041
3059
  }
3042
3060
  catch {
3043
3061
  // best-effort — the retry loop will try again
@@ -3088,6 +3106,43 @@ export class Peer {
3088
3106
  }
3089
3107
  }
3090
3108
  }
3109
+ // LAN host candidate (bytes 12-17): the peer's private LAN address. Only
3110
+ // safe to try when it falls in OUR subnet — same physical LAN — otherwise
3111
+ // a peer's 192.168.1.x could collide with an unrelated host on our own
3112
+ // network. When it matches it's the fastest possible path (direct, no NAT,
3113
+ // no relay), so punch it and add it as a same-LAN endpoint candidate, which
3114
+ // #collectSessionEndpointCandidates prioritises for the session path. We
3115
+ // prefer it as session.remote (when we have no confirmed real UDP yet) so
3116
+ // the very next keepalive upgrades the path off the relay.
3117
+ if (payload.length >= 18) {
3118
+ const lanHost = `${payload[12]}.${payload[13]}.${payload[14]}.${payload[15]}`;
3119
+ const lanPort = ((payload[16] << 8) | payload[17]) >>> 0;
3120
+ const sameLan = lanPort !== 0 &&
3121
+ isPrivateAddress(lanHost) &&
3122
+ getLocalIpv4Subnets().some((s) => isInIpv4Subnet(lanHost, s)) &&
3123
+ !(getLocalIpv4Addresses().includes(lanHost) && this.#udp.localPort() === lanPort);
3124
+ if (sameLan) {
3125
+ this.#rememberEndpointCandidate(session, lanHost, lanPort);
3126
+ this.#debugLog(`udp-endpoint LAN candidate from ${friendId}: ${lanHost}:${lanPort} (same subnet) — punching`);
3127
+ const lanPunch = Uint8Array.of(0xf2);
3128
+ let ln = 0;
3129
+ const lanTimer = setInterval(() => {
3130
+ this.#udp.sendDirectSync(Buffer.from(lanPunch), lanHost, lanPort);
3131
+ if (++ln >= 6)
3132
+ clearInterval(lanTimer);
3133
+ }, 120);
3134
+ const haveRealUdp = session.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
3135
+ if (!haveRealUdp) {
3136
+ session.remote = { host: lanHost, port: lanPort };
3137
+ }
3138
+ if (session.established) {
3139
+ void this.#sendMessengerPacket(friendId, PACKET_ID_ALIVE, new Uint8Array()).catch(() => undefined);
3140
+ }
3141
+ else {
3142
+ void this.#initiateSession(friendId).catch(() => undefined);
3143
+ }
3144
+ }
3145
+ }
3091
3146
  // Don't punch toward ourselves.
3092
3147
  if (getLocalIpv4Addresses().includes(host) && this.#udp.localPort() === port)
3093
3148
  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.35",
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",