@decentnetwork/peer 0.1.3 → 0.1.5

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.
package/dist/peer.d.ts CHANGED
@@ -71,6 +71,28 @@ export declare class Peer {
71
71
  hasTcpRoute: boolean;
72
72
  transport: "udp" | "tcp-relay" | "both" | "none";
73
73
  lastPingRecvMs: number | null;
74
+ /** OS-assigned UDP port THIS node is listening on. Useful to confirm
75
+ * we even have a UDP socket bound. */
76
+ ourLocalUdpPort: number | null;
77
+ /** Cached UDP endpoint for the peer from a previous online state
78
+ * (persisted across restarts). Set != null means UDP holepunch
79
+ * has succeeded at some point — but null when we've only ever
80
+ * reached them via TCP-relay. Tells us whether discovery has
81
+ * ever found a usable UDP endpoint. */
82
+ friendUdpEndpoint: {
83
+ host: string;
84
+ port: number;
85
+ } | null;
86
+ /** UDP endpoints picked up by recent DHT discovery for this peer,
87
+ * whether or not the cookie exchange has succeeded yet. If this
88
+ * is empty AND friendUdpEndpoint is null AND we keep getting
89
+ * tcp-relay, discovery itself is broken — we don't even know
90
+ * where to send a cookie request. */
91
+ endpointCandidatesCount: number;
92
+ /** Last time we sent an outbound cookie request via UDP for this
93
+ * peer (null = never). Used to confirm Phase 1.2 retries are
94
+ * actually firing. */
95
+ cookieRequestSentMs: number | null;
74
96
  } | null;
75
97
  waitForFriendRequest(timeoutMs?: number): Promise<FriendRequest>;
76
98
  }
package/dist/peer.js CHANGED
@@ -772,6 +772,7 @@ export class Peer {
772
772
  const s = this.#friendSessions.get(pubkey);
773
773
  if (!s)
774
774
  return null;
775
+ const friend = this.#friends.get(pubkey);
775
776
  // session.remote can be a synthetic `tcp:<dhtpk>:0` placeholder
776
777
  // when the TCP relay path reports an endpoint with no real UDP
777
778
  // address; treat those as no-UDP for reporting.
@@ -785,12 +786,22 @@ export class Peer {
785
786
  : s.hasTcpRoute
786
787
  ? "tcp-relay"
787
788
  : "none";
789
+ const friendUdp = friend?.remoteHost &&
790
+ friend.remotePort &&
791
+ !friend.remoteHost.startsWith("tcp:") &&
792
+ friend.remotePort !== 0
793
+ ? { host: friend.remoteHost, port: friend.remotePort }
794
+ : null;
788
795
  return {
789
796
  established: s.established === true,
790
797
  udpRemote: realUdp,
791
798
  hasTcpRoute: s.hasTcpRoute === true,
792
799
  transport,
793
800
  lastPingRecvMs: s.lastPingRecvMs ?? null,
801
+ ourLocalUdpPort: this.#udp?.localPort() ?? null,
802
+ friendUdpEndpoint: friendUdp,
803
+ endpointCandidatesCount: s.endpointCandidates?.length ?? 0,
804
+ cookieRequestSentMs: s.cookieRequestSentMs ?? null,
794
805
  };
795
806
  }
796
807
  waitForFriendRequest(timeoutMs = 30000) {
@@ -2777,10 +2788,73 @@ export class Peer {
2777
2788
  // residential ISPs where self-announce doesn't propagate).
2778
2789
  // Mirrors toxcore's send_dht_pk_packet which calls
2779
2790
  // tcp_copy_connected_relays to fill extras.
2780
- let extras = new Uint8Array(0);
2791
+ // Also pack OUR OWN UDP endpoint(s) as extras (family 0x02 =
2792
+ // UDP_FAMILY_IPV4). Without this, peers receiving our DHT-PK push
2793
+ // learn our TCP relays but have NO IDEA where to send UDP cookie
2794
+ // requests — so they never even try UDP holepunch and every
2795
+ // session stays on tcp-relay (~500ms+ RTT) instead of the
2796
+ // ~80ms UDP path. Observed in the wild: even same-LAN peers
2797
+ // (snoopy and mac-dev on the same /24) stuck on tcp-relay
2798
+ // forever because neither side ever announces a UDP endpoint
2799
+ // to the other.
2800
+ //
2801
+ // For peers behind NAT, the LAN address we advertise is
2802
+ // reachable by same-LAN peers and harmless for WAN peers
2803
+ // (they fall through to tcp-relay as before). For peers with
2804
+ // a public IP (e.g. a Vultr VPS), the LAN address IS the
2805
+ // reachable one; same-format works. STUN-style reflexive
2806
+ // discovery is the next layer but isn't required to fix the
2807
+ // LAN case immediately.
2808
+ const packed = [];
2809
+ const ourUdpPort = this.#udp?.localPort();
2810
+ if (ourUdpPort && ourUdpPort > 0) {
2811
+ const ourDhtPk = this.#keyPair?.publicKey;
2812
+ if (ourDhtPk && ourDhtPk.length === 32) {
2813
+ // Enumerate non-loopback IPv4 addresses on this host.
2814
+ // We use os.networkInterfaces() rather than guessing — this
2815
+ // covers a multi-homed peer with several reachable IPs.
2816
+ try {
2817
+ const ifaces = (await import("os")).networkInterfaces();
2818
+ const seenIps = new Set();
2819
+ for (const iface of Object.values(ifaces)) {
2820
+ if (!iface)
2821
+ continue;
2822
+ for (const addr of iface) {
2823
+ if (addr.family !== "IPv4")
2824
+ continue;
2825
+ if (addr.internal)
2826
+ continue;
2827
+ if (addr.address === "0.0.0.0")
2828
+ continue;
2829
+ if (seenIps.has(addr.address))
2830
+ continue;
2831
+ seenIps.add(addr.address);
2832
+ const parts = addr.address
2833
+ .split(".")
2834
+ .map((p) => Number.parseInt(p, 10));
2835
+ if (parts.length !== 4 || parts.some((n) => !(n >= 0 && n <= 255)))
2836
+ continue;
2837
+ const entry = new Uint8Array(1 + 4 + 2 + 32);
2838
+ entry[0] = 0x02; // UDP_FAMILY_IPV4
2839
+ entry[1] = parts[0];
2840
+ entry[2] = parts[1];
2841
+ entry[3] = parts[2];
2842
+ entry[4] = parts[3];
2843
+ entry[5] = (ourUdpPort >> 8) & 0xff;
2844
+ entry[6] = ourUdpPort & 0xff;
2845
+ entry.set(ourDhtPk, 7);
2846
+ packed.push(entry);
2847
+ }
2848
+ }
2849
+ }
2850
+ catch {
2851
+ // os import or interface enumeration failed; skip UDP extras
2852
+ // and let TCP relay carry the session as before.
2853
+ }
2854
+ }
2855
+ }
2781
2856
  if (this.#tcpRelays) {
2782
2857
  const relays = this.#tcpRelays.connectedRelays(3); // MAX_SHARED_RELAYS
2783
- const packed = [];
2784
2858
  for (const r of relays) {
2785
2859
  // Only IPv4 TCP entries supported here; matches what we parse.
2786
2860
  // Format per packed-nodes spec: family(1) + ipv4(4) + port(2 BE) + pk(32)
@@ -2798,10 +2872,8 @@ export class Peer {
2798
2872
  entry.set(r.serverPublicKey, 7);
2799
2873
  packed.push(entry);
2800
2874
  }
2801
- if (packed.length > 0) {
2802
- extras = concatBytes(packed);
2803
- }
2804
2875
  }
2876
+ const extras = packed.length > 0 ? concatBytes(packed) : new Uint8Array(0);
2805
2877
  const innerPayload = concatBytes([
2806
2878
  noReplayBytes,
2807
2879
  this.#keyPair.publicKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",