@decentnetwork/peer 0.1.18 → 0.1.19
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.js +267 -42
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -17,6 +17,15 @@ import { base58ToBytes } from "./utils/base58.js";
|
|
|
17
17
|
import { UdpTransport } from "./transport/udp.js";
|
|
18
18
|
import { bytesToHex, concatBytes, randomBytes } from "./utils/bytes.js";
|
|
19
19
|
import { buildBindingRequest, decodeStun, decodeXorMappedAddress, findAttr, STUN_ATTR_XOR_MAPPED_ADDRESS, STUN_BINDING_SUCCESS } from "./stun.js";
|
|
20
|
+
import { TurnClient } from "./turn.js";
|
|
21
|
+
import { createSocket as createDgramSocket } from "dgram";
|
|
22
|
+
// Dedicated TURN relay servers — fixed public relays used as a stable
|
|
23
|
+
// fallback path when direct UDP hole-punch fails or flaps (symmetric NAT,
|
|
24
|
+
// NAT remaps, lossy direct path). A relay endpoint never NAT-flaps, so the
|
|
25
|
+
// path stays put even when the peers' NATs churn. Static long-term creds.
|
|
26
|
+
const TURN_RELAY_SERVERS = [
|
|
27
|
+
{ host: "tokyo.fi.chat", port: 3478, username: "allcom", password: "allcompass" }
|
|
28
|
+
];
|
|
20
29
|
const ANNOUNCE_WAIT_TIMEOUT_MS = readEnvInt("DECENT_ANNOUNCE_WAIT_TIMEOUT_MS", 4000);
|
|
21
30
|
const MAX_FRIEND_ROUTE_ATTEMPTS = readEnvInt("DECENT_FRIEND_ROUTE_MAX_ATTEMPTS", 12);
|
|
22
31
|
// How many in-flight announce step1 requests to fan out at once. Toxcore
|
|
@@ -128,6 +137,16 @@ export class Peer {
|
|
|
128
137
|
#events = new EventEmitter();
|
|
129
138
|
#keyPair;
|
|
130
139
|
#udp = new UdpTransport();
|
|
140
|
+
// TURN relay (stable fallback path). One node-level allocation on its own
|
|
141
|
+
// dedicated UDP socket — kept separate from #udp so net_crypto/DHT and the
|
|
142
|
+
// TURN control protocol never share a socket (no demux). Inbound relayed
|
|
143
|
+
// data is injected into #onDatagram as if it arrived directly from the
|
|
144
|
+
// peer's relay address; outbound to a peer's relay address goes through
|
|
145
|
+
// #turnClient.sendTo. Lazily allocated on first need.
|
|
146
|
+
#turnClient;
|
|
147
|
+
#turnSocket;
|
|
148
|
+
#ourRelayAddr;
|
|
149
|
+
#turnAllocating;
|
|
131
150
|
#bootstrap;
|
|
132
151
|
#dht = new LegacyDhtClient();
|
|
133
152
|
#knownNodes;
|
|
@@ -398,6 +417,17 @@ export class Peer {
|
|
|
398
417
|
}
|
|
399
418
|
this.#udp.off("datagram", this.#onDatagram);
|
|
400
419
|
await this.#udp.stop();
|
|
420
|
+
// Release the TURN allocation + its dedicated socket.
|
|
421
|
+
try {
|
|
422
|
+
this.#turnClient?.close();
|
|
423
|
+
this.#turnSocket?.close();
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// best-effort
|
|
427
|
+
}
|
|
428
|
+
this.#turnClient = undefined;
|
|
429
|
+
this.#turnSocket = undefined;
|
|
430
|
+
this.#ourRelayAddr = undefined;
|
|
401
431
|
this.#started = false;
|
|
402
432
|
}
|
|
403
433
|
pubkey() {
|
|
@@ -893,7 +923,7 @@ export class Peer {
|
|
|
893
923
|
#remoteIsTcp(remote) {
|
|
894
924
|
return remote.address.startsWith("tcp:");
|
|
895
925
|
}
|
|
896
|
-
#onDatagram = ({ data, remote }) => {
|
|
926
|
+
#onDatagram = ({ data, remote, viaRelay }) => {
|
|
897
927
|
if (!this.#keyPair) {
|
|
898
928
|
return;
|
|
899
929
|
}
|
|
@@ -1191,13 +1221,26 @@ export class Peer {
|
|
|
1191
1221
|
isNewSession &&
|
|
1192
1222
|
state.sessionEstablishedAtMs !== undefined) {
|
|
1193
1223
|
const sinceEstablished = Date.now() - state.sessionEstablishedAtMs;
|
|
1194
|
-
if
|
|
1224
|
+
// Only reject the fresh handshake if our CURRENT session is
|
|
1225
|
+
// actually alive — i.e. carrying data. A wedged "established"
|
|
1226
|
+
// session that's receiving nothing must NOT block a re-handshake,
|
|
1227
|
+
// or a peer that dropped + restarted can never reconnect: it keeps
|
|
1228
|
+
// sending fresh handshakes and we keep rejecting them against a
|
|
1229
|
+
// corpse. (This is the all-exits-outage bug: mac held a dead
|
|
1230
|
+
// "established" session for cn and ignored every reconnect
|
|
1231
|
+
// attempt.) If we haven't received a packet within FRIEND_TIMEOUT_MS
|
|
1232
|
+
// the current session is dead — fall through and accept the new one.
|
|
1233
|
+
const lastAlive = state.lastPingRecvMs ?? state.sessionEstablishedAtMs;
|
|
1234
|
+
const currentSessionAlive = Date.now() - lastAlive < FRIEND_TIMEOUT_MS;
|
|
1235
|
+
if (sinceEstablished > 1000 && currentSessionAlive) {
|
|
1195
1236
|
// Verbose only — peers retransmit handshakes aggressively, so
|
|
1196
1237
|
// this line fires dozens of times per minute on a stuck pair
|
|
1197
1238
|
// and drowns out signal. Visible with DECENT_DEBUG_VERBOSE=1.
|
|
1198
|
-
this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on established session, ${sinceEstablished}ms after establish)`);
|
|
1239
|
+
this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ${sinceEstablished}ms after establish)`);
|
|
1199
1240
|
return;
|
|
1200
1241
|
}
|
|
1242
|
+
this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (current session wedged/dead, ` +
|
|
1243
|
+
`lastPingRecv=${state.lastPingRecvMs ? Date.now() - state.lastPingRecvMs + "ms ago" : "never"})`);
|
|
1201
1244
|
}
|
|
1202
1245
|
state.peerSessionPublicKey = hs.sessionPublicKey;
|
|
1203
1246
|
state.peerBaseNonce = hs.baseNonce.slice();
|
|
@@ -1329,6 +1372,15 @@ export class Peer {
|
|
|
1329
1372
|
if (this.#remoteIsTcp(remote)) {
|
|
1330
1373
|
state.hasTcpRoute = true;
|
|
1331
1374
|
}
|
|
1375
|
+
else if (viaRelay) {
|
|
1376
|
+
// Arrived over the TURN relay. Track relay freshness separately;
|
|
1377
|
+
// do NOT set state.remote (that's the direct endpoint). The relay
|
|
1378
|
+
// address it came from is already in state.relayRemote.
|
|
1379
|
+
if (!state.lastRelayRecvMs) {
|
|
1380
|
+
this.#debugLog(`relay_confirmed friend=${friendId} via=${remote.address}:${remote.port} (TURN relay path live)`);
|
|
1381
|
+
}
|
|
1382
|
+
state.lastRelayRecvMs = Date.now();
|
|
1383
|
+
}
|
|
1332
1384
|
else {
|
|
1333
1385
|
state.remote = { host: remote.address, port: remote.port };
|
|
1334
1386
|
if (!state.lastUdpRecvMs) {
|
|
@@ -2122,10 +2174,33 @@ export class Peer {
|
|
|
2122
2174
|
const session = this.#friendSessions.get(friendId);
|
|
2123
2175
|
// Established session: keepalive + timeout monitoring
|
|
2124
2176
|
if (session?.established) {
|
|
2125
|
-
|
|
2126
|
-
|
|
2177
|
+
// Liveness: time out a session that hasn't received ANY packet
|
|
2178
|
+
// within FRIEND_TIMEOUT_MS — measured from the last received ping
|
|
2179
|
+
// OR, if we've never received one, from when the session was
|
|
2180
|
+
// established. The previous `lastPingRecvMs &&` guard skipped the
|
|
2181
|
+
// check entirely when lastPingRecvMs was undefined, so a session
|
|
2182
|
+
// that established but never carried a single packet (the path
|
|
2183
|
+
// died at/just-after handshake) lived forever reporting
|
|
2184
|
+
// transport=both while IP forwarding was 100% loss, and never tore
|
|
2185
|
+
// down to re-handshake. Tearing it down here drops it to the
|
|
2186
|
+
// non-established branch below, which re-initiates a fresh
|
|
2187
|
+
// connection — turning a permanent wedge into a ~timeout self-heal.
|
|
2188
|
+
const lastAlive = session.lastPingRecvMs ?? session.sessionEstablishedAtMs ?? now;
|
|
2189
|
+
if (now - lastAlive > FRIEND_TIMEOUT_MS) {
|
|
2190
|
+
this.#debugLog(`session timeout for ${friendId} (no ping in ${now - lastAlive}ms; ` +
|
|
2191
|
+
`lastPingRecv=${session.lastPingRecvMs ?? "never"}) — tearing down to re-handshake`);
|
|
2127
2192
|
this.#friendSessions.delete(friendId);
|
|
2128
2193
|
this.#setFriendOffline(friendId);
|
|
2194
|
+
// Re-assert the relay route so the friend-connection bootstrap
|
|
2195
|
+
// (CONNECTION_NOTIFICATION -> #initiateSession) can re-fire on
|
|
2196
|
+
// re-establishment. requestRoute is idempotent, so this is a
|
|
2197
|
+
// no-op if the route is still live.
|
|
2198
|
+
try {
|
|
2199
|
+
const pk = base58ToBytes(friend.pubkey);
|
|
2200
|
+
if (pk.length === 32)
|
|
2201
|
+
this.#tcpRelays?.requestRoute(pk);
|
|
2202
|
+
}
|
|
2203
|
+
catch { /* skip malformed */ }
|
|
2129
2204
|
continue;
|
|
2130
2205
|
}
|
|
2131
2206
|
if (!session.lastPingSentMs || now - session.lastPingSentMs > FRIEND_PING_INTERVAL_MS) {
|
|
@@ -2163,14 +2238,33 @@ export class Peer {
|
|
|
2163
2238
|
// before the punch is proven, so using it here would stop the
|
|
2164
2239
|
// offer/punch retry prematurely and deadlock a half-punched pair.
|
|
2165
2240
|
// Treat the UDP path as needing re-punch once it's been quiet for
|
|
2166
|
-
// >
|
|
2167
|
-
//
|
|
2168
|
-
// re-
|
|
2169
|
-
//
|
|
2170
|
-
//
|
|
2171
|
-
//
|
|
2172
|
-
//
|
|
2173
|
-
|
|
2241
|
+
// >4s — matched to #sendToFriend's 4s freshness window, so the
|
|
2242
|
+
// moment bulk data starts bridging over the relay we're already
|
|
2243
|
+
// re-offering + re-punching to restore the direct path (e.g. after
|
|
2244
|
+
// a NAT port remap). A healthy path refreshes lastUdpRecvMs every
|
|
2245
|
+
// ~1s via keepalive/data, so this never fires while UDP works.
|
|
2246
|
+
// Relay keepalive — runs even when the direct path is perfectly
|
|
2247
|
+
// healthy, so the TURN relay stays ready as an instant fallback.
|
|
2248
|
+
// Without this the relay's permission (and the candidate exchange)
|
|
2249
|
+
// silently lapse after ~5min and a later direct-path failure falls
|
|
2250
|
+
// all the way back to the slow tcp-relay. Re-send our offer (so the
|
|
2251
|
+
// peer refreshes ITS permission for our relay) and re-permit the
|
|
2252
|
+
// peer's relay on our own allocation. Cheap, every 2min.
|
|
2253
|
+
if (session.established && session.hasTcpRoute) {
|
|
2254
|
+
const RELAY_KEEPALIVE_MS = 120_000;
|
|
2255
|
+
if (now - (session.lastRelayKeepaliveMs ?? 0) > RELAY_KEEPALIVE_MS) {
|
|
2256
|
+
session.lastRelayKeepaliveMs = now;
|
|
2257
|
+
void this.#sendUdpEndpointOffer(friendId).catch(() => undefined);
|
|
2258
|
+
if (session.relayRemote && this.#turnClient) {
|
|
2259
|
+
const rr = session.relayRemote;
|
|
2260
|
+
void this.#turnClient
|
|
2261
|
+
.createPermission({ family: 4, address: rr.host, port: rr.port })
|
|
2262
|
+
.then(() => { session.relayPermittedAtMs = Date.now(); })
|
|
2263
|
+
.catch(() => undefined);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
const udpConfirmed = session.lastUdpRecvMs !== undefined && now - session.lastUdpRecvMs < 4_000;
|
|
2174
2268
|
if (!udpConfirmed && session.hasTcpRoute) {
|
|
2175
2269
|
// Aggressive re-punch cadence: the relay path is lossy, so a few
|
|
2176
2270
|
// offers may drop before one lands and both sides punch in the
|
|
@@ -2280,10 +2374,18 @@ export class Peer {
|
|
|
2280
2374
|
});
|
|
2281
2375
|
}
|
|
2282
2376
|
}
|
|
2283
|
-
// No active session: try to establish one if we know the friend's
|
|
2284
|
-
//
|
|
2285
|
-
//
|
|
2286
|
-
|
|
2377
|
+
// No active session: try to establish one if we know the friend's
|
|
2378
|
+
// endpoint. A friend reachable over the TCP relay counts — the cookie
|
|
2379
|
+
// request + handshake go over the relay OOB (the log shows
|
|
2380
|
+
// `cookie_sent udp=0 tcp=1`). Without including hasTcpRoute here, a
|
|
2381
|
+
// relay-only friend whose handshake didn't complete on the first try
|
|
2382
|
+
// (peer's handshake-back was lost) was NEVER retried — the cookie
|
|
2383
|
+
// retry below was gated on a UDP endpoint — so the session stayed
|
|
2384
|
+
// stuck forever at "friend_online but never established". That is the
|
|
2385
|
+
// post-restart reconnection wedge that took down cn/callpass: relay
|
|
2386
|
+
// bridged them, cookie+handshake fired once, didn't complete, and
|
|
2387
|
+
// nothing re-tried it. Counting the relay route lets the retry fire.
|
|
2388
|
+
const haveEndpoint = (friend.remoteHost && friend.remotePort) || session?.remote || session?.hasTcpRoute;
|
|
2287
2389
|
if (!haveEndpoint) {
|
|
2288
2390
|
const dhtPk = session?.friendDhtPublicKey;
|
|
2289
2391
|
if (dhtPk) {
|
|
@@ -2391,7 +2493,22 @@ export class Peer {
|
|
|
2391
2493
|
let tcpOk = false;
|
|
2392
2494
|
let firstError;
|
|
2393
2495
|
const realUdpRemote = s?.remote && !s.remote.host?.startsWith("tcp:") && s.remote.port !== 0;
|
|
2394
|
-
|
|
2496
|
+
// "Fresh" = we've received UDP from this peer within the last few
|
|
2497
|
+
// seconds, so the direct endpoint is currently live. When the peer's
|
|
2498
|
+
// NAT remaps its port (observed periodically on residential/cloud
|
|
2499
|
+
// NATs) the old endpoint goes dark and lastUdpRecvMs goes stale.
|
|
2500
|
+
const udpFresh = !!realUdpRemote &&
|
|
2501
|
+
s?.lastUdpRecvMs !== undefined &&
|
|
2502
|
+
Date.now() - s.lastUdpRecvMs < 4_000;
|
|
2503
|
+
// Bulk IP data rides the direct path ONLY while it's fresh. When it
|
|
2504
|
+
// goes stale (likely a NAT remap mid-stream), we do NOT keep firing
|
|
2505
|
+
// into the dead endpoint and we do NOT duplicate onto UDP — we bridge
|
|
2506
|
+
// over the relay until the re-punch restores UDP (which refreshes
|
|
2507
|
+
// lastUdpRecvMs). This converts a multi-second blackout into a few
|
|
2508
|
+
// seconds of higher relay latency. Control packets always probe UDP
|
|
2509
|
+
// (to keep the NAT mapping warm) so we still try UDP for them.
|
|
2510
|
+
const tryUdp = realUdpRemote && (isBulkData ? udpFresh : true);
|
|
2511
|
+
if (tryUdp && s?.remote) {
|
|
2395
2512
|
try {
|
|
2396
2513
|
await this.#sendPacket(packet, s.remote);
|
|
2397
2514
|
udpOk = true;
|
|
@@ -2400,28 +2517,24 @@ export class Peer {
|
|
|
2400
2517
|
firstError = error;
|
|
2401
2518
|
}
|
|
2402
2519
|
}
|
|
2403
|
-
//
|
|
2404
|
-
//
|
|
2405
|
-
|
|
2406
|
-
// Bulk IP-forwarding data: send UDP-ONLY once the direct path has ever
|
|
2407
|
-
// been confirmed. Never spill onto the relay (its backlog is the
|
|
2408
|
-
// source of the 20s-late duplicates). A transient UDP gap just drops a
|
|
2409
|
-
// few packets; the re-punch loop (every 3s when UDP is quiet >7s)
|
|
2410
|
-
// restores the path, and control traffic on the relay keeps the
|
|
2411
|
-
// session alive meanwhile.
|
|
2412
|
-
if (isBulkData && udpOk && realUdpRemote && udpEverConfirmed) {
|
|
2520
|
+
// Fresh direct path → send over UDP only: no relay fan-out, no
|
|
2521
|
+
// duplicates, no relay backlog. Applies to both data and control.
|
|
2522
|
+
if (udpFresh && udpOk) {
|
|
2413
2523
|
return;
|
|
2414
2524
|
}
|
|
2415
|
-
//
|
|
2416
|
-
//
|
|
2417
|
-
//
|
|
2418
|
-
//
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2525
|
+
// Tier 2: TURN relay (tokyo) — a fast (~280ms) stable fallback that
|
|
2526
|
+
// never NAT-flaps. Used when the direct path isn't fresh (remap, loss,
|
|
2527
|
+
// symmetric NAT). For bulk data, once the relay path is *confirmed*
|
|
2528
|
+
// (we've received over it recently) it carries data on its own — no
|
|
2529
|
+
// tcp-relay fan-out, no duplicates. Until confirmed, we still also try
|
|
2530
|
+
// tcp below so a packet isn't lost while the relay path warms up.
|
|
2531
|
+
let relayOk = false;
|
|
2532
|
+
if (s?.relayRemote && s.relayPermitted) {
|
|
2533
|
+
relayOk = this.#sendViaRelay(packet, s.relayRemote);
|
|
2534
|
+
const relayConfirmed = s.lastRelayRecvMs !== undefined && Date.now() - s.lastRelayRecvMs < 8_000;
|
|
2535
|
+
if (isBulkData && relayOk && relayConfirmed) {
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2425
2538
|
}
|
|
2426
2539
|
if (this.#tcpRelays) {
|
|
2427
2540
|
// The TCP relay routes by the friend's DHT pubkey (= the pubkey
|
|
@@ -2443,7 +2556,7 @@ export class Peer {
|
|
|
2443
2556
|
}
|
|
2444
2557
|
}
|
|
2445
2558
|
}
|
|
2446
|
-
if (!udpOk && !tcpOk) {
|
|
2559
|
+
if (!udpOk && !relayOk && !tcpOk) {
|
|
2447
2560
|
throw firstError ?? new Error(`no transport accepted send for ${friendId}`);
|
|
2448
2561
|
}
|
|
2449
2562
|
}
|
|
@@ -2570,7 +2683,12 @@ export class Peer {
|
|
|
2570
2683
|
* Returns undefined if no bootnode answers within the timeout.
|
|
2571
2684
|
*/
|
|
2572
2685
|
async #gatherOwnSrflx() {
|
|
2573
|
-
|
|
2686
|
+
// Short cache: when our NAT remaps the socket's external port, we must
|
|
2687
|
+
// re-learn it quickly so the re-punch offers the *new* endpoint. A
|
|
2688
|
+
// long cache would keep us advertising a dead port for its full
|
|
2689
|
+
// lifetime, stalling recovery. Gather is only called during
|
|
2690
|
+
// (re)establishment/re-punch, so a short cache costs little.
|
|
2691
|
+
const CACHE_MS = 5_000;
|
|
2574
2692
|
const now = Date.now();
|
|
2575
2693
|
if (this.#srflxCache && now - this.#srflxCache.atMs < CACHE_MS) {
|
|
2576
2694
|
return this.#srflxCache.addr;
|
|
@@ -2623,6 +2741,74 @@ export class Peer {
|
|
|
2623
2741
|
* endpoint discovery. Both peers do this symmetrically; on receipt each
|
|
2624
2742
|
* feeds the other's endpoint into endpointCandidates and punches.
|
|
2625
2743
|
*/
|
|
2744
|
+
/**
|
|
2745
|
+
* Lazily allocate our node-level TURN relay on its own dedicated UDP
|
|
2746
|
+
* socket. Returns our public relay address (on the TURN server) which we
|
|
2747
|
+
* advertise to peers so they can reach us via the relay even when direct
|
|
2748
|
+
* UDP can't be punched. Relayed data is injected into #onDatagram tagged
|
|
2749
|
+
* `viaRelay` so it updates the relay freshness, not the direct endpoint.
|
|
2750
|
+
*/
|
|
2751
|
+
async #ensureTurnRelay() {
|
|
2752
|
+
if (this.#ourRelayAddr)
|
|
2753
|
+
return this.#ourRelayAddr;
|
|
2754
|
+
if (this.#turnAllocating)
|
|
2755
|
+
return this.#turnAllocating;
|
|
2756
|
+
this.#turnAllocating = (async () => {
|
|
2757
|
+
for (const srv of TURN_RELAY_SERVERS) {
|
|
2758
|
+
try {
|
|
2759
|
+
const sock = createDgramSocket("udp4");
|
|
2760
|
+
await new Promise((resolve, reject) => {
|
|
2761
|
+
sock.once("error", reject);
|
|
2762
|
+
sock.bind(0, () => {
|
|
2763
|
+
sock.off("error", reject);
|
|
2764
|
+
resolve();
|
|
2765
|
+
});
|
|
2766
|
+
});
|
|
2767
|
+
const client = new TurnClient({
|
|
2768
|
+
sock,
|
|
2769
|
+
creds: { host: srv.host, port: srv.port, realm: "", username: srv.username, password: srv.password }
|
|
2770
|
+
});
|
|
2771
|
+
const alloc = await client.allocate();
|
|
2772
|
+
client.onData((peer, data) => {
|
|
2773
|
+
// Re-inject relayed payload as if it arrived directly from the
|
|
2774
|
+
// peer's relay address. viaRelay keeps it off the direct path.
|
|
2775
|
+
this.#onDatagram({
|
|
2776
|
+
data: Buffer.from(data),
|
|
2777
|
+
remote: { address: peer.address, port: peer.port },
|
|
2778
|
+
viaRelay: true
|
|
2779
|
+
});
|
|
2780
|
+
});
|
|
2781
|
+
this.#turnSocket = sock;
|
|
2782
|
+
this.#turnClient = client;
|
|
2783
|
+
this.#ourRelayAddr = { host: alloc.relayedAddress.address, port: alloc.relayedAddress.port };
|
|
2784
|
+
this.#debugLog(`turn relay allocated on ${srv.host}: ${this.#ourRelayAddr.host}:${this.#ourRelayAddr.port}`);
|
|
2785
|
+
return this.#ourRelayAddr;
|
|
2786
|
+
}
|
|
2787
|
+
catch (error) {
|
|
2788
|
+
this.#debugLog(`turn relay allocate failed on ${srv.host}: ${error.message}`);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
return undefined;
|
|
2792
|
+
})();
|
|
2793
|
+
const result = await this.#turnAllocating;
|
|
2794
|
+
this.#turnAllocating = undefined;
|
|
2795
|
+
return result;
|
|
2796
|
+
}
|
|
2797
|
+
/** Wrap + send a packet to a peer's TURN relay address via our own
|
|
2798
|
+
* allocation. Mirrors #sendPacket's carrier-magic framing so the peer's
|
|
2799
|
+
* #onDatagram processes it identically to a direct datagram. */
|
|
2800
|
+
#sendViaRelay(packet, relay) {
|
|
2801
|
+
if (!this.#turnClient)
|
|
2802
|
+
return false;
|
|
2803
|
+
const wrapped = concatBytes([Uint8Array.of(0x69, 0x76, 0x65, 0x67), packet]);
|
|
2804
|
+
try {
|
|
2805
|
+
this.#turnClient.sendTo({ family: 4, address: relay.host, port: relay.port }, wrapped);
|
|
2806
|
+
return true;
|
|
2807
|
+
}
|
|
2808
|
+
catch {
|
|
2809
|
+
return false;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2626
2812
|
async #sendUdpEndpointOffer(friendId) {
|
|
2627
2813
|
const session = this.#friendSessions.get(friendId);
|
|
2628
2814
|
if (!session?.established)
|
|
@@ -2633,13 +2819,25 @@ export class Peer {
|
|
|
2633
2819
|
const octets = srflx.host.split(".").map((s) => parseInt(s, 10));
|
|
2634
2820
|
if (octets.length !== 4 || octets.some((o) => Number.isNaN(o)))
|
|
2635
2821
|
return;
|
|
2636
|
-
|
|
2822
|
+
// Also advertise our TURN relay address (bytes 6-11) as a second
|
|
2823
|
+
// candidate. Best-effort: if the relay can't be allocated we just send
|
|
2824
|
+
// the 6-byte srflx-only offer (older peers ignore the extra bytes).
|
|
2825
|
+
const relay = await this.#ensureTurnRelay().catch(() => undefined);
|
|
2826
|
+
const relayOctets = relay ? relay.host.split(".").map((s) => parseInt(s, 10)) : undefined;
|
|
2827
|
+
const relayValid = relayOctets && relayOctets.length === 4 && !relayOctets.some((o) => Number.isNaN(o));
|
|
2828
|
+
const payload = new Uint8Array(relayValid ? 12 : 6);
|
|
2637
2829
|
payload.set(octets, 0);
|
|
2638
2830
|
payload[4] = (srflx.port >> 8) & 0xff;
|
|
2639
2831
|
payload[5] = srflx.port & 0xff;
|
|
2832
|
+
if (relayValid && relay) {
|
|
2833
|
+
payload.set(relayOctets, 6);
|
|
2834
|
+
payload[10] = (relay.port >> 8) & 0xff;
|
|
2835
|
+
payload[11] = relay.port & 0xff;
|
|
2836
|
+
}
|
|
2640
2837
|
try {
|
|
2641
2838
|
await this.#sendMessengerPacket(friendId, PACKET_ID_UDP_ENDPOINT, payload);
|
|
2642
|
-
this.#debugLog(`udp-endpoint offer sent to ${friendId}:
|
|
2839
|
+
this.#debugLog(`udp-endpoint offer sent to ${friendId}: direct=${srflx.host}:${srflx.port}` +
|
|
2840
|
+
(relayValid && relay ? ` relay=${relay.host}:${relay.port}` : ""));
|
|
2643
2841
|
}
|
|
2644
2842
|
catch {
|
|
2645
2843
|
// best-effort — the retry loop will try again
|
|
@@ -2663,6 +2861,33 @@ export class Peer {
|
|
|
2663
2861
|
const session = this.#friendSessions.get(friendId);
|
|
2664
2862
|
if (!session)
|
|
2665
2863
|
return;
|
|
2864
|
+
// If the offer carries a TURN relay candidate (bytes 6-11), remember it
|
|
2865
|
+
// and permit it on our own allocation so data can flow peer→relay→us.
|
|
2866
|
+
if (payload.length >= 12) {
|
|
2867
|
+
const relayHost = `${payload[6]}.${payload[7]}.${payload[8]}.${payload[9]}`;
|
|
2868
|
+
const relayPort = ((payload[10] << 8) | payload[11]) >>> 0;
|
|
2869
|
+
if (relayPort !== 0) {
|
|
2870
|
+
const changed = session.relayRemote?.host !== relayHost || session.relayRemote?.port !== relayPort;
|
|
2871
|
+
session.relayRemote = { host: relayHost, port: relayPort };
|
|
2872
|
+
if (changed)
|
|
2873
|
+
session.relayPermitted = false;
|
|
2874
|
+
// (Re)create the permission on first sight, on address change, or
|
|
2875
|
+
// when the existing one is aging toward coturn's ~5min expiry.
|
|
2876
|
+
const permitAgeMs = Date.now() - (session.relayPermittedAtMs ?? 0);
|
|
2877
|
+
if (!session.relayPermitted || permitAgeMs > 90_000) {
|
|
2878
|
+
void this.#ensureTurnRelay()
|
|
2879
|
+
.then(() => this.#turnClient?.createPermission({ family: 4, address: relayHost, port: relayPort }))
|
|
2880
|
+
.then(() => {
|
|
2881
|
+
const first = !session.relayPermitted;
|
|
2882
|
+
session.relayPermitted = true;
|
|
2883
|
+
session.relayPermittedAtMs = Date.now();
|
|
2884
|
+
if (first)
|
|
2885
|
+
this.#debugLog(`turn permission created for ${friendId} relay ${relayHost}:${relayPort}`);
|
|
2886
|
+
})
|
|
2887
|
+
.catch(() => undefined);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2666
2891
|
// Don't punch toward ourselves.
|
|
2667
2892
|
if (getLocalIpv4Addresses().includes(host) && this.#udp.localPort() === port)
|
|
2668
2893
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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",
|