@decentnetwork/peer 0.1.37 → 0.1.39
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.
|
@@ -64,7 +64,7 @@ export declare class TcpRelayPool extends EventEmitter {
|
|
|
64
64
|
* online. Returns the count of relays that accepted the write — 0 if
|
|
65
65
|
* the friend isn't online on any TCP relay.
|
|
66
66
|
*/
|
|
67
|
-
sendToFriend(friendPublicKey: Uint8Array, payload: Uint8Array): number;
|
|
67
|
+
sendToFriend(friendPublicKey: Uint8Array, payload: Uint8Array, droppable?: boolean): number;
|
|
68
68
|
/** Diagnostics. */
|
|
69
69
|
connectedCount(): number;
|
|
70
70
|
/**
|
|
@@ -173,7 +173,7 @@ export class TcpRelayPool extends EventEmitter {
|
|
|
173
173
|
* online. Returns the count of relays that accepted the write — 0 if
|
|
174
174
|
* the friend isn't online on any TCP relay.
|
|
175
175
|
*/
|
|
176
|
-
sendToFriend(friendPublicKey, payload) {
|
|
176
|
+
sendToFriend(friendPublicKey, payload, droppable = false) {
|
|
177
177
|
if (friendPublicKey.length !== KEY_SIZE)
|
|
178
178
|
return 0;
|
|
179
179
|
// Send via the FIRST connected relay where the friend is online; only
|
|
@@ -191,7 +191,7 @@ export class TcpRelayPool extends EventEmitter {
|
|
|
191
191
|
continue;
|
|
192
192
|
if (!r.client.hasFriendOnline(friendPublicKey))
|
|
193
193
|
continue;
|
|
194
|
-
if (r.client.sendToFriend(friendPublicKey, payload)) {
|
|
194
|
+
if (r.client.sendToFriend(friendPublicKey, payload, droppable)) {
|
|
195
195
|
return 1;
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -82,11 +82,11 @@ export declare class TcpRelayClient extends EventEmitter {
|
|
|
82
82
|
* if the relay says they're offline. Caller should treat false as
|
|
83
83
|
* "try another transport".
|
|
84
84
|
*/
|
|
85
|
-
sendToFriend(friendPublicKey: Uint8Array, payload: Uint8Array): boolean;
|
|
85
|
+
sendToFriend(friendPublicKey: Uint8Array, payload: Uint8Array, droppable?: boolean): boolean;
|
|
86
86
|
/** Send a PING; relay echoes it as PONG. ping_id is opaque 8 bytes. */
|
|
87
87
|
sendPing(): boolean;
|
|
88
88
|
/** Send DATA payload to the friend at `connectionId`. */
|
|
89
|
-
sendData(connectionId: number, payload: Uint8Array): boolean;
|
|
89
|
+
sendData(connectionId: number, payload: Uint8Array, droppable?: boolean): boolean;
|
|
90
90
|
/** Send OOB_SEND (used for delivering friend requests via TCP relay). */
|
|
91
91
|
sendOob(receiverPublicKey: Uint8Array, payload: Uint8Array): boolean;
|
|
92
92
|
close(reason?: string): void;
|
package/dist/compat/tcp-relay.js
CHANGED
|
@@ -37,6 +37,15 @@ import { incrementNonce } from "./net-crypto.js";
|
|
|
37
37
|
const KEY_SIZE = 32;
|
|
38
38
|
const NONCE_SIZE = 24;
|
|
39
39
|
const MAC_SIZE = 16;
|
|
40
|
+
// Max bytes we let pile up in the relay socket's write buffer before we start
|
|
41
|
+
// DROPPING droppable (bulk IP) frames. Node's socket.write() buffers without
|
|
42
|
+
// bound when the kernel send buffer is full, so a congested relay link would
|
|
43
|
+
// otherwise queue SECONDS of IP packets — the multi-second bufferbloat behind
|
|
44
|
+
// the snoopy/lili 13s pings. ~64KB is well under a second of queue even on a
|
|
45
|
+
// slow transpacific relay; past it we behave like a real congested network and
|
|
46
|
+
// drop, letting the inner protocol (TCP/QUIC) retransmit instead of delivering
|
|
47
|
+
// stale packets late. Control frames (droppable=false) always go through.
|
|
48
|
+
const RELAY_SEND_BUFFER_CAP = 64 * 1024;
|
|
40
49
|
/** TCP_HANDSHAKE_PLAIN_SIZE in toxcore — temp pubkey + initial nonce. */
|
|
41
50
|
const TCP_HANDSHAKE_PLAIN_SIZE = KEY_SIZE + NONCE_SIZE;
|
|
42
51
|
/** Outbound handshake: self_dht_pk(32) + nonce(24) + box(plain)(KEY_SIZE+NONCE_SIZE+MAC_SIZE = 72) = 128. */
|
|
@@ -276,13 +285,18 @@ export class TcpRelayClient extends EventEmitter {
|
|
|
276
285
|
return opened;
|
|
277
286
|
}
|
|
278
287
|
/** Encrypt a payload and write it as one framed packet. */
|
|
279
|
-
#sendFrame(payload) {
|
|
288
|
+
#sendFrame(payload, droppable = false) {
|
|
280
289
|
if (this.#state !== "connected" || !this.#sessionKey || !this.#sentNonce || !this.#socket) {
|
|
281
290
|
return false;
|
|
282
291
|
}
|
|
283
292
|
if (payload.length === 0 || payload.length > MAX_PACKET_SIZE - MAC_SIZE) {
|
|
284
293
|
return false;
|
|
285
294
|
}
|
|
295
|
+
// Bound the relay queue: drop bulk IP frames once the socket buffer is
|
|
296
|
+
// backed up rather than letting Node buffer seconds of packets (bufferbloat).
|
|
297
|
+
if (droppable && this.#socket.writableLength > RELAY_SEND_BUFFER_CAP) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
286
300
|
const cipher = nacl.secretbox(payload, this.#sentNonce, this.#sessionKey);
|
|
287
301
|
incrementNonce(this.#sentNonce);
|
|
288
302
|
// Wire layout: [4-byte iveg][2-byte BE length][cipher].
|
|
@@ -418,13 +432,13 @@ export class TcpRelayClient extends EventEmitter {
|
|
|
418
432
|
* if the relay says they're offline. Caller should treat false as
|
|
419
433
|
* "try another transport".
|
|
420
434
|
*/
|
|
421
|
-
sendToFriend(friendPublicKey, payload) {
|
|
435
|
+
sendToFriend(friendPublicKey, payload, droppable = false) {
|
|
422
436
|
if (friendPublicKey.length !== KEY_SIZE)
|
|
423
437
|
return false;
|
|
424
438
|
const cid = this.#cidByFriend.get(Buffer.from(friendPublicKey).toString("hex"));
|
|
425
439
|
if (cid === undefined)
|
|
426
440
|
return false;
|
|
427
|
-
return this.sendData(cid, payload);
|
|
441
|
+
return this.sendData(cid, payload, droppable);
|
|
428
442
|
}
|
|
429
443
|
/** Send a PING; relay echoes it as PONG. ping_id is opaque 8 bytes. */
|
|
430
444
|
sendPing() {
|
|
@@ -432,12 +446,12 @@ export class TcpRelayClient extends EventEmitter {
|
|
|
432
446
|
return this.#sendFrame(concatBytes([Uint8Array.of(TCP_PACKET_PING), pingId]));
|
|
433
447
|
}
|
|
434
448
|
/** Send DATA payload to the friend at `connectionId`. */
|
|
435
|
-
sendData(connectionId, payload) {
|
|
449
|
+
sendData(connectionId, payload, droppable = false) {
|
|
436
450
|
if (connectionId < 16 || connectionId > 0xff)
|
|
437
451
|
return false;
|
|
438
452
|
if (payload.length === 0)
|
|
439
453
|
return false;
|
|
440
|
-
return this.#sendFrame(concatBytes([Uint8Array.of(connectionId), payload]));
|
|
454
|
+
return this.#sendFrame(concatBytes([Uint8Array.of(connectionId), payload]), droppable);
|
|
441
455
|
}
|
|
442
456
|
/** Send OOB_SEND (used for delivering friend requests via TCP relay). */
|
|
443
457
|
sendOob(receiverPublicKey, payload) {
|
package/dist/peer.js
CHANGED
|
@@ -2775,7 +2775,10 @@ export class Peer {
|
|
|
2775
2775
|
// would silently drop on iPad's side and leave the session
|
|
2776
2776
|
// looking healthy on our side while iPad never sees the
|
|
2777
2777
|
// packets.
|
|
2778
|
-
|
|
2778
|
+
// Bulk IP data is droppable over the relay: under backpressure the
|
|
2779
|
+
// relay client drops it (bounded queue) rather than buffering seconds
|
|
2780
|
+
// of packets. Control frames pass isBulkData=false and always queue.
|
|
2781
|
+
const sent = this.#tcpRelays.sendToFriend(tcpKey, packet, isBulkData);
|
|
2779
2782
|
if (sent > 0) {
|
|
2780
2783
|
tcpOk = true;
|
|
2781
2784
|
}
|
|
@@ -3055,7 +3058,7 @@ export class Peer {
|
|
|
3055
3058
|
// "direct" option we advertise is our srflx (public) address, which the
|
|
3056
3059
|
// NAT must hairpin back onto the LAN — most don't, so two machines in one
|
|
3057
3060
|
// office silently fall back to the TURN relay (~260ms for a <1ms hop).
|
|
3058
|
-
const lanIp =
|
|
3061
|
+
const lanIp = getPhysicalLanAddresses().find((ip) => isPrivateAddress(ip));
|
|
3059
3062
|
const lanOctets = lanIp ? lanIp.split(".").map((s) => parseInt(s, 10)) : undefined;
|
|
3060
3063
|
const lanPort = this.#udp.localPort() ?? 0;
|
|
3061
3064
|
const lanValid = !!lanOctets && lanOctets.length === 4 && !lanOctets.some((o) => Number.isNaN(o)) && lanPort > 0;
|
|
@@ -3144,8 +3147,9 @@ export class Peer {
|
|
|
3144
3147
|
const lanPort = ((payload[16] << 8) | payload[17]) >>> 0;
|
|
3145
3148
|
const sameLan = lanPort !== 0 &&
|
|
3146
3149
|
isPrivateAddress(lanHost) &&
|
|
3147
|
-
|
|
3148
|
-
|
|
3150
|
+
!isCgnatAddress(lanHost) &&
|
|
3151
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(lanHost, s)) &&
|
|
3152
|
+
!(getPhysicalLanAddresses().includes(lanHost) && this.#udp.localPort() === lanPort);
|
|
3149
3153
|
if (sameLan) {
|
|
3150
3154
|
this.#rememberEndpointCandidate(session, lanHost, lanPort);
|
|
3151
3155
|
this.#debugLog(`udp-endpoint LAN candidate from ${friendId}: ${lanHost}:${lanPort} (same subnet) — punching`);
|
|
@@ -4430,6 +4434,72 @@ function isInIpv4Subnet(host, subnet) {
|
|
|
4430
4434
|
return false;
|
|
4431
4435
|
return ((ip & subnet.maskBits) >>> 0) === subnet.networkBits;
|
|
4432
4436
|
}
|
|
4437
|
+
// Interface names that are VIRTUAL / overlay networks, not a physical LAN:
|
|
4438
|
+
// Tailscale & utun tunnels (utun* on macOS, tailscale*/tun* on Linux),
|
|
4439
|
+
// WireGuard (wg*), ZeroTier (zt*), the decentlan TUN itself (agentnet*), and
|
|
4440
|
+
// assorted macOS virtual links (awdl/llw/gif/stf/bridge). A LAN-direct host
|
|
4441
|
+
// candidate MUST be a physical-LAN address: advertising/punching an overlay
|
|
4442
|
+
// address (e.g. Tailscale 100.x, or our own 10.86.x mesh IP) sends the "direct"
|
|
4443
|
+
// path onto the overlay or loops into our own mesh, then silently falls back to
|
|
4444
|
+
// the relay — the snoopy/lili "lastUdpRecv=never -> bufferbloat" path. (lili
|
|
4445
|
+
// runs Tailscale; that's exactly this.)
|
|
4446
|
+
const VIRTUAL_IFACE_RE = /^(utun|tun|tap|wg|tailscale|zt|ham|agentnet|awdl|llw|gif|stf|bridge|vnic|vmnet|veth|docker)/i;
|
|
4447
|
+
/** True for RFC 6598 carrier-grade-NAT space 100.64.0.0/10 (Tailscale's range). */
|
|
4448
|
+
function isCgnatAddress(host) {
|
|
4449
|
+
const o = host.split(".");
|
|
4450
|
+
if (o.length !== 4)
|
|
4451
|
+
return false;
|
|
4452
|
+
const a = Number(o[0]);
|
|
4453
|
+
const b = Number(o[1]);
|
|
4454
|
+
return a === 100 && b >= 64 && b <= 127;
|
|
4455
|
+
}
|
|
4456
|
+
/** Local IPv4 addresses on PHYSICAL interfaces only (excludes overlay/virtual
|
|
4457
|
+
* interfaces by name and the CGNAT range). Used for LAN-direct host candidates. */
|
|
4458
|
+
function getPhysicalLanAddresses() {
|
|
4459
|
+
const out = [];
|
|
4460
|
+
try {
|
|
4461
|
+
for (const [name, list] of Object.entries(networkInterfaces())) {
|
|
4462
|
+
if (!list || VIRTUAL_IFACE_RE.test(name))
|
|
4463
|
+
continue;
|
|
4464
|
+
for (const info of list) {
|
|
4465
|
+
if (info.family !== "IPv4" || info.internal)
|
|
4466
|
+
continue;
|
|
4467
|
+
if (isCgnatAddress(info.address))
|
|
4468
|
+
continue;
|
|
4469
|
+
out.push(info.address);
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
catch {
|
|
4474
|
+
// best-effort
|
|
4475
|
+
}
|
|
4476
|
+
return out;
|
|
4477
|
+
}
|
|
4478
|
+
/** Subnets of PHYSICAL interfaces only — for matching a peer's host candidate
|
|
4479
|
+
* to OUR real LAN (so we never match a peer's Tailscale/TUN address against our
|
|
4480
|
+
* own overlay subnet). */
|
|
4481
|
+
function getPhysicalLanSubnets() {
|
|
4482
|
+
const out = [];
|
|
4483
|
+
try {
|
|
4484
|
+
for (const [name, list] of Object.entries(networkInterfaces())) {
|
|
4485
|
+
if (!list || VIRTUAL_IFACE_RE.test(name))
|
|
4486
|
+
continue;
|
|
4487
|
+
for (const info of list) {
|
|
4488
|
+
if (info.family !== "IPv4" || info.internal || isCgnatAddress(info.address))
|
|
4489
|
+
continue;
|
|
4490
|
+
const addr = ipv4ToInt(info.address);
|
|
4491
|
+
const mask = netmaskToInt(info.netmask);
|
|
4492
|
+
if (addr === undefined || mask === undefined || mask === 0)
|
|
4493
|
+
continue;
|
|
4494
|
+
out.push({ networkBits: (addr & mask) >>> 0, maskBits: mask });
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
catch {
|
|
4499
|
+
// best-effort
|
|
4500
|
+
}
|
|
4501
|
+
return out;
|
|
4502
|
+
}
|
|
4433
4503
|
function isPrivateAddress(host) {
|
|
4434
4504
|
// RFC1918 IPv4: 10/8, 172.16/12, 192.168/16; loopback 127/8;
|
|
4435
4505
|
// link-local 169.254/16; carrier-grade NAT 100.64/10. IPv6 link-local fe80::/10
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
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",
|