@decentnetwork/peer 0.1.122 → 0.1.123

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
@@ -307,4 +307,27 @@ export declare class Peer {
307
307
  name: string;
308
308
  description: string;
309
309
  };
310
+ /**
311
+ * Tell a friend our public UDP endpoint over the (already-working,
312
+ * possibly tcp-relay) messenger channel, so they can hole-punch to us.
313
+ * This is the out-of-band substitute for the broken DHT/onion-announce
314
+ * endpoint discovery. Both peers do this symmetrically; on receipt each
315
+ * feeds the other's endpoint into endpointCandidates and punches.
316
+ */
317
+ /**
318
+ * Lazily allocate our node-level TURN relay on its own dedicated UDP
319
+ * socket. Returns our public relay address (on the TURN server) which we
320
+ * advertise to peers so they can reach us via the relay even when direct
321
+ * UDP can't be punched. Relayed data is injected into #onDatagram tagged
322
+ * `viaRelay` so it updates the relay freshness, not the direct endpoint.
323
+ */
324
+ /**
325
+ * Test-only seam for scripts/turn-socket-leak-selftest.mjs: drive one TURN
326
+ * allocation attempt directly instead of waiting out the 120s-per-friend
327
+ * relay keepalive that reaches this path in production. Not public API.
328
+ */
329
+ __testForceTurnAllocation(): Promise<{
330
+ host: string;
331
+ port: number;
332
+ } | undefined>;
310
333
  }
package/dist/peer.js CHANGED
@@ -23,14 +23,35 @@ import { UdpTransport } from "./transport/udp.js";
23
23
  import { bytesToHex, concatBytes, randomBytes } from "./utils/bytes.js";
24
24
  import { buildBindingRequest, decodeStun, decodeXorMappedAddress, findAttr, STUN_ATTR_XOR_MAPPED_ADDRESS, STUN_BINDING_SUCCESS } from "./stun.js";
25
25
  import { TurnClient } from "./turn.js";
26
- import { createSocket as createDgramSocket } from "dgram";
26
+ import { createBoundUdp4Socket, closeDgramSocket } from "./transport/dgram-lifecycle.js";
27
27
  // Dedicated TURN relay servers — fixed public relays used as a stable
28
28
  // fallback path when direct UDP hole-punch fails or flaps (symmetric NAT,
29
29
  // NAT remaps, lossy direct path). A relay endpoint never NAT-flaps, so the
30
30
  // path stays put even when the peers' NATs churn. Static long-term creds.
31
- const TURN_RELAY_SERVERS = [
32
- { host: "tokyo.fi.chat", port: 3478, username: "allcom", password: "allcompass" }
33
- ];
31
+ // DECENT_TURN_SERVERS overrides the list ("host:port,host:port"). Used by the
32
+ // socket-leak selftest to point allocation at an unroutable address so every
33
+ // attempt fails deterministically; also lets ops repoint TURN without a build.
34
+ const TURN_RELAY_SERVERS = (() => {
35
+ const raw = process.env.DECENT_TURN_SERVERS?.trim();
36
+ if (!raw) {
37
+ return [{ host: "tokyo.fi.chat", port: 3478, username: "allcom", password: "allcompass" }];
38
+ }
39
+ return raw
40
+ .split(",")
41
+ .map((entry) => entry.trim())
42
+ .filter(Boolean)
43
+ .map((entry) => {
44
+ const idx = entry.lastIndexOf(":");
45
+ const host = idx === -1 ? entry : entry.slice(0, idx);
46
+ const port = idx === -1 ? 3478 : Number.parseInt(entry.slice(idx + 1), 10);
47
+ return {
48
+ host,
49
+ port: Number.isFinite(port) && port > 0 ? port : 3478,
50
+ username: "allcom",
51
+ password: "allcompass"
52
+ };
53
+ });
54
+ })();
34
55
  const ANNOUNCE_WAIT_TIMEOUT_MS = readEnvInt("DECENT_ANNOUNCE_WAIT_TIMEOUT_MS", 4000);
35
56
  // 24 (was 12): with distance-ordered walking a lookup needs headroom to
36
57
  // recurse past a first wave of dead close-in-XOR-space entries.
@@ -736,13 +757,8 @@ export class Peer {
736
757
  this.#udp.off("datagram", this.#onDatagram);
737
758
  await this.#udp.stop();
738
759
  // Release the TURN allocation + its dedicated socket.
739
- try {
740
- this.#turnClient?.close();
741
- this.#turnSocket?.close();
742
- }
743
- catch {
744
- // best-effort
745
- }
760
+ this.#turnClient?.close();
761
+ await closeDgramSocket(this.#turnSocket);
746
762
  this.#turnClient = undefined;
747
763
  this.#turnSocket = undefined;
748
764
  this.#ourRelayAddr = undefined;
@@ -4765,6 +4781,14 @@ export class Peer {
4765
4781
  * UDP can't be punched. Relayed data is injected into #onDatagram tagged
4766
4782
  * `viaRelay` so it updates the relay freshness, not the direct endpoint.
4767
4783
  */
4784
+ /**
4785
+ * Test-only seam for scripts/turn-socket-leak-selftest.mjs: drive one TURN
4786
+ * allocation attempt directly instead of waiting out the 120s-per-friend
4787
+ * relay keepalive that reaches this path in production. Not public API.
4788
+ */
4789
+ async __testForceTurnAllocation() {
4790
+ return this.#ensureTurnRelay().catch(() => undefined);
4791
+ }
4768
4792
  async #ensureTurnRelay() {
4769
4793
  if (this.#ourRelayAddr)
4770
4794
  return this.#ourRelayAddr;
@@ -4772,15 +4796,10 @@ export class Peer {
4772
4796
  return this.#turnAllocating;
4773
4797
  this.#turnAllocating = (async () => {
4774
4798
  for (const srv of TURN_RELAY_SERVERS) {
4799
+ let sock;
4800
+ let client;
4775
4801
  try {
4776
- const sock = createDgramSocket("udp4");
4777
- await new Promise((resolve, reject) => {
4778
- sock.once("error", reject);
4779
- sock.bind(0, () => {
4780
- sock.off("error", reject);
4781
- resolve();
4782
- });
4783
- });
4802
+ sock = await createBoundUdp4Socket();
4784
4803
  // CRITICAL: a persistent 'error' listener. A TURN send to an
4785
4804
  // unresolvable host (DNS NXDOMAIN — e.g. tokyo.fi.chat on a box whose
4786
4805
  // resolver doesn't know it) emits an 'error' on this dgram socket;
@@ -4789,7 +4808,7 @@ export class Peer {
4789
4808
  // "getaddrinfo ENOTFOUND tokyo.fi.chat", which churned every session).
4790
4809
  // Swallow it — the relay simply won't allocate/relay over this server.
4791
4810
  sock.on("error", (e) => this.#debugLog(`turn socket error (${srv.host}): ${e.message}`));
4792
- const client = new TurnClient({
4811
+ client = new TurnClient({
4793
4812
  sock,
4794
4813
  creds: { host: srv.host, port: srv.port, realm: "", username: srv.username, password: srv.password }
4795
4814
  });
@@ -4805,12 +4824,16 @@ export class Peer {
4805
4824
  });
4806
4825
  this.#turnSocket = sock;
4807
4826
  this.#turnClient = client;
4827
+ sock = undefined;
4828
+ client = undefined;
4808
4829
  this.#ourRelayAddr = { host: alloc.relayedAddress.address, port: alloc.relayedAddress.port };
4809
4830
  this.#debugLog(`turn relay allocated on ${srv.host}: ${this.#ourRelayAddr.host}:${this.#ourRelayAddr.port}`);
4810
4831
  return this.#ourRelayAddr;
4811
4832
  }
4812
4833
  catch (error) {
4813
4834
  this.#debugLog(`turn relay allocate failed on ${srv.host}: ${error.message}`);
4835
+ client?.close();
4836
+ await closeDgramSocket(sock);
4814
4837
  }
4815
4838
  }
4816
4839
  return undefined;
@@ -0,0 +1,14 @@
1
+ import { type Socket as DgramSocket } from "dgram";
2
+ export type DgramSocketFactory = () => DgramSocket;
3
+ export declare function createBoundUdp4Socket(createSocket?: DgramSocketFactory): Promise<DgramSocket>;
4
+ /**
5
+ * Close a dgram socket and wait for the fd to actually be released.
6
+ *
7
+ * Never rejects and never hangs: `close()` throws ERR_SOCKET_DGRAM_NOT_RUNNING
8
+ * on an already-closed socket, and a socket caught mid-teardown may never run
9
+ * the close callback at all. Both must settle, because callers await this on
10
+ * the TURN failure path — a hang there would leave `#turnAllocating` pending
11
+ * forever and wedge every later caller awaiting that same promise, and would
12
+ * also block `Peer.stop()`.
13
+ */
14
+ export declare function closeDgramSocket(sock: DgramSocket | undefined, timeoutMs?: number): Promise<void>;
@@ -0,0 +1,64 @@
1
+ import { createSocket as createDgramSocket } from "dgram";
2
+ export async function createBoundUdp4Socket(createSocket = () => createDgramSocket("udp4")) {
3
+ const sock = createSocket();
4
+ try {
5
+ await new Promise((resolve, reject) => {
6
+ const cleanup = () => {
7
+ sock.off("error", onError);
8
+ };
9
+ const onError = (error) => {
10
+ cleanup();
11
+ reject(error);
12
+ };
13
+ sock.once("error", onError);
14
+ sock.bind(0, () => {
15
+ cleanup();
16
+ resolve();
17
+ });
18
+ });
19
+ return sock;
20
+ }
21
+ catch (error) {
22
+ await closeDgramSocket(sock);
23
+ throw error;
24
+ }
25
+ }
26
+ /**
27
+ * Close a dgram socket and wait for the fd to actually be released.
28
+ *
29
+ * Never rejects and never hangs: `close()` throws ERR_SOCKET_DGRAM_NOT_RUNNING
30
+ * on an already-closed socket, and a socket caught mid-teardown may never run
31
+ * the close callback at all. Both must settle, because callers await this on
32
+ * the TURN failure path — a hang there would leave `#turnAllocating` pending
33
+ * forever and wedge every later caller awaiting that same promise, and would
34
+ * also block `Peer.stop()`.
35
+ */
36
+ export async function closeDgramSocket(sock, timeoutMs = 2000) {
37
+ if (!sock)
38
+ return;
39
+ await new Promise((resolve) => {
40
+ let done = false;
41
+ let timer;
42
+ const finish = () => {
43
+ if (done)
44
+ return;
45
+ done = true;
46
+ if (timer)
47
+ clearTimeout(timer);
48
+ sock.off("close", finish);
49
+ resolve();
50
+ };
51
+ // Belt and braces: the callback, the 'close' event, or the timer —
52
+ // whichever comes first releases the caller.
53
+ timer = setTimeout(finish, timeoutMs);
54
+ // Don't keep the event loop alive just to observe a close.
55
+ timer.unref?.();
56
+ try {
57
+ sock.once("close", finish);
58
+ sock.close(finish);
59
+ }
60
+ catch {
61
+ finish();
62
+ }
63
+ });
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.122",
3
+ "version": "0.1.123",
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",