@decentnetwork/peer 0.1.121 → 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.
@@ -60,7 +81,10 @@ const DHT_MAINTENANCE_INTERVAL_MS = readEnvInt("DECENT_DHT_MAINTENANCE_INTERVAL_
60
81
  // distance-sort it feeds) without limit — which starved CCTV forwarding over time.
61
82
  const MAX_KNOWN_NODES = readEnvInt("DECENT_MAX_KNOWN_NODES", 400);
62
83
  const MAX_SELF_ANNOUNCE_TARGETS = readEnvInt("DECENT_SELF_ANNOUNCE_TARGETS", 16);
63
- const SELF_ANNOUNCE_ATTEMPTS = readEnvInt("DECENT_SELF_ANNOUNCE_ATTEMPTS", 3);
84
+ // 1 (was 3): a dead node cost attempts×ANNOUNCE_WAIT = 12s — one wave of
85
+ // dead nodes ate the entire per-cycle deadline, so the walk never got past
86
+ // wave 1 and stored on ~1 node per DAY. The 12s cycle itself is the retry.
87
+ const SELF_ANNOUNCE_ATTEMPTS = readEnvInt("DECENT_SELF_ANNOUNCE_ATTEMPTS", 1);
64
88
  // Self-announce batch size — number of step1 / step2 requests to fan out
65
89
  // at once. Toxcore's onion_client.c::do_announce keeps up to
66
90
  // MAX_ONION_CLIENTS_ANNOUNCE (12) in flight. Configurable via env.
@@ -733,13 +757,8 @@ export class Peer {
733
757
  this.#udp.off("datagram", this.#onDatagram);
734
758
  await this.#udp.stop();
735
759
  // Release the TURN allocation + its dedicated socket.
736
- try {
737
- this.#turnClient?.close();
738
- this.#turnSocket?.close();
739
- }
740
- catch {
741
- // best-effort
742
- }
760
+ this.#turnClient?.close();
761
+ await closeDgramSocket(this.#turnSocket);
743
762
  this.#turnClient = undefined;
744
763
  this.#turnSocket = undefined;
745
764
  this.#ourRelayAddr = undefined;
@@ -4762,6 +4781,14 @@ export class Peer {
4762
4781
  * UDP can't be punched. Relayed data is injected into #onDatagram tagged
4763
4782
  * `viaRelay` so it updates the relay freshness, not the direct endpoint.
4764
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
+ }
4765
4792
  async #ensureTurnRelay() {
4766
4793
  if (this.#ourRelayAddr)
4767
4794
  return this.#ourRelayAddr;
@@ -4769,15 +4796,10 @@ export class Peer {
4769
4796
  return this.#turnAllocating;
4770
4797
  this.#turnAllocating = (async () => {
4771
4798
  for (const srv of TURN_RELAY_SERVERS) {
4799
+ let sock;
4800
+ let client;
4772
4801
  try {
4773
- const sock = createDgramSocket("udp4");
4774
- await new Promise((resolve, reject) => {
4775
- sock.once("error", reject);
4776
- sock.bind(0, () => {
4777
- sock.off("error", reject);
4778
- resolve();
4779
- });
4780
- });
4802
+ sock = await createBoundUdp4Socket();
4781
4803
  // CRITICAL: a persistent 'error' listener. A TURN send to an
4782
4804
  // unresolvable host (DNS NXDOMAIN — e.g. tokyo.fi.chat on a box whose
4783
4805
  // resolver doesn't know it) emits an 'error' on this dgram socket;
@@ -4786,7 +4808,7 @@ export class Peer {
4786
4808
  // "getaddrinfo ENOTFOUND tokyo.fi.chat", which churned every session).
4787
4809
  // Swallow it — the relay simply won't allocate/relay over this server.
4788
4810
  sock.on("error", (e) => this.#debugLog(`turn socket error (${srv.host}): ${e.message}`));
4789
- const client = new TurnClient({
4811
+ client = new TurnClient({
4790
4812
  sock,
4791
4813
  creds: { host: srv.host, port: srv.port, realm: "", username: srv.username, password: srv.password }
4792
4814
  });
@@ -4802,12 +4824,16 @@ export class Peer {
4802
4824
  });
4803
4825
  this.#turnSocket = sock;
4804
4826
  this.#turnClient = client;
4827
+ sock = undefined;
4828
+ client = undefined;
4805
4829
  this.#ourRelayAddr = { host: alloc.relayedAddress.address, port: alloc.relayedAddress.port };
4806
4830
  this.#debugLog(`turn relay allocated on ${srv.host}: ${this.#ourRelayAddr.host}:${this.#ourRelayAddr.port}`);
4807
4831
  return this.#ourRelayAddr;
4808
4832
  }
4809
4833
  catch (error) {
4810
4834
  this.#debugLog(`turn relay allocate failed on ${srv.host}: ${error.message}`);
4835
+ client?.close();
4836
+ await closeDgramSocket(sock);
4811
4837
  }
4812
4838
  }
4813
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.121",
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",