@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 +23 -0
- package/dist/peer.js +47 -21
- package/dist/transport/dgram-lifecycle.d.ts +14 -0
- package/dist/transport/dgram-lifecycle.js +64 -0
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|