@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 +23 -0
- package/dist/peer.js +43 -20
- 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.
|
|
@@ -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
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|