@decentnetwork/lan 0.1.76 → 0.1.77
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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/carrier/packet-session.d.ts +2 -4
- package/dist/carrier/packet-session.js +8 -8
- package/dist/carrier/peer-manager.d.ts +0 -11
- package/dist/carrier/peer-manager.js +50 -62
- package/dist/carrier/types.d.ts +0 -3
- package/dist/carrier/types.js +0 -7
- package/dist/dora/dora-integration.js +1 -1
- package/dist/proxy/connect-proxy.js +24 -0
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,15 +6,13 @@ import type { PacketFrame } from "./types.js";
|
|
|
6
6
|
export interface PacketSessionOptions {
|
|
7
7
|
peerId: string;
|
|
8
8
|
sessionId: number;
|
|
9
|
-
|
|
10
|
-
* (PACKET_ID_DL_SESSION for handshake, PACKET_ID_DL_IP for data). */
|
|
11
|
-
sendFrame: (frame: Uint8Array, packetId: number) => Promise<void>;
|
|
9
|
+
sendText: (text: string) => Promise<void>;
|
|
12
10
|
onClose?: () => void;
|
|
13
11
|
}
|
|
14
12
|
export declare class PacketSession extends EventEmitter {
|
|
15
13
|
private peerId;
|
|
16
14
|
private sessionId;
|
|
17
|
-
private
|
|
15
|
+
private sendText;
|
|
18
16
|
private isActive;
|
|
19
17
|
private handshakeTimeout;
|
|
20
18
|
private handshakePromise;
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from "events";
|
|
5
5
|
import { FrameCodec } from "./frame.js";
|
|
6
|
-
import { FRAME_OPCODE_HANDSHAKE_REQ, FRAME_OPCODE_DATA,
|
|
6
|
+
import { FRAME_OPCODE_HANDSHAKE_REQ, FRAME_OPCODE_DATA, } from "./types.js";
|
|
7
7
|
export class PacketSession extends EventEmitter {
|
|
8
8
|
peerId;
|
|
9
9
|
sessionId;
|
|
10
|
-
|
|
10
|
+
sendText;
|
|
11
11
|
isActive = false;
|
|
12
12
|
handshakeTimeout = null;
|
|
13
13
|
handshakePromise = null;
|
|
@@ -15,7 +15,7 @@ export class PacketSession extends EventEmitter {
|
|
|
15
15
|
super();
|
|
16
16
|
this.peerId = opts.peerId;
|
|
17
17
|
this.sessionId = opts.sessionId;
|
|
18
|
-
this.
|
|
18
|
+
this.sendText = opts.sendText;
|
|
19
19
|
this.on("close", () => {
|
|
20
20
|
if (opts.onClose) {
|
|
21
21
|
opts.onClose();
|
|
@@ -38,8 +38,9 @@ export class PacketSession extends EventEmitter {
|
|
|
38
38
|
return this.handshakePromise;
|
|
39
39
|
}
|
|
40
40
|
this.handshakePromise = new Promise((resolve, reject) => {
|
|
41
|
-
// Send handshake request
|
|
41
|
+
// Send handshake request
|
|
42
42
|
const frame = FrameCodec.encode(new Uint8Array(0), this.sessionId, FRAME_OPCODE_HANDSHAKE_REQ);
|
|
43
|
+
const frameBase64 = Buffer.from(frame).toString("base64");
|
|
43
44
|
// 30s default — Carrier express relay can add several seconds of latency
|
|
44
45
|
// for cross-region peers. Configurable via AGENTNET_HANDSHAKE_TIMEOUT_MS.
|
|
45
46
|
const timeoutMs = parseInt(process.env.AGENTNET_HANDSHAKE_TIMEOUT_MS || "30000", 10);
|
|
@@ -66,7 +67,7 @@ export class PacketSession extends EventEmitter {
|
|
|
66
67
|
this.handshakePromise = null;
|
|
67
68
|
reject(new Error(`Handshake aborted for peer ${this.peerId}`));
|
|
68
69
|
});
|
|
69
|
-
this.
|
|
70
|
+
this.sendText(frameBase64).catch((error) => {
|
|
70
71
|
if (this.handshakeTimeout) {
|
|
71
72
|
clearTimeout(this.handshakeTimeout);
|
|
72
73
|
}
|
|
@@ -81,10 +82,9 @@ export class PacketSession extends EventEmitter {
|
|
|
81
82
|
throw new Error("Packet session not active. Call handshake() first.");
|
|
82
83
|
}
|
|
83
84
|
const frame = FrameCodec.encode(packet, this.sessionId, FRAME_OPCODE_DATA);
|
|
85
|
+
const frameBase64 = Buffer.from(frame).toString("base64");
|
|
84
86
|
try {
|
|
85
|
-
|
|
86
|
-
// reliability — see docs/PROTOCOL.md, avoids TCP-over-TCP).
|
|
87
|
-
await this.sendFrame(frame, PACKET_ID_DL_IP);
|
|
87
|
+
await this.sendText(frameBase64);
|
|
88
88
|
}
|
|
89
89
|
catch (error) {
|
|
90
90
|
// Don't tear down the whole session for a single failed sendText —
|
|
@@ -152,16 +152,5 @@ export declare class PeerManager extends EventEmitter {
|
|
|
152
152
|
* Send a HANDSHAKE_ACK frame to a peer.
|
|
153
153
|
*/
|
|
154
154
|
private sendHandshakeAck;
|
|
155
|
-
/**
|
|
156
|
-
* Send a dora control message to a dora server over the dora custom packet
|
|
157
|
-
* (162, lossless). Replaces the old `DORA:`-prefixed text on packet 64.
|
|
158
|
-
*/
|
|
159
|
-
sendDora(userid: string, text: string): Promise<void>;
|
|
160
|
-
/**
|
|
161
|
-
* Route a decoded decentlan frame to its packet-session, creating or
|
|
162
|
-
* replacing the session on a HANDSHAKE_REQ (handles the Carrier re-pair
|
|
163
|
-
* case where both sides hold different sessionIds).
|
|
164
|
-
*/
|
|
165
|
-
private handleDecodedFrame;
|
|
166
155
|
private setupEventHandlers;
|
|
167
156
|
}
|
|
@@ -6,8 +6,13 @@ import { Peer } from "@decentnetwork/peer";
|
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
7
|
import { PacketSession } from "./packet-session.js";
|
|
8
8
|
import { FrameCodec } from "./frame.js";
|
|
9
|
-
import { FRAME_OPCODE_HANDSHAKE_ACK,
|
|
9
|
+
import { FRAME_OPCODE_HANDSHAKE_ACK, } from "./types.js";
|
|
10
10
|
import { Logger } from "../utils/logger.js";
|
|
11
|
+
// Dora wire-prefix. Kept inline (rather than imported from @decentnetwork/dora) to
|
|
12
|
+
// avoid coupling peer-manager — a transport-layer module — to an
|
|
13
|
+
// application-protocol package. The prefix is part of decentlan's text
|
|
14
|
+
// channel contract and must match the constant defined in dora's types.ts.
|
|
15
|
+
const DORA_PREFIX = "DORA:";
|
|
11
16
|
export class PeerManager extends EventEmitter {
|
|
12
17
|
peer = null;
|
|
13
18
|
identity = null;
|
|
@@ -326,11 +331,11 @@ export class PeerManager extends EventEmitter {
|
|
|
326
331
|
const session = new PacketSession({
|
|
327
332
|
peerId: pubkey,
|
|
328
333
|
sessionId,
|
|
329
|
-
|
|
334
|
+
sendText: async (text) => {
|
|
330
335
|
if (!this.peer) {
|
|
331
336
|
throw new Error("Peer not available");
|
|
332
337
|
}
|
|
333
|
-
await this.peer.
|
|
338
|
+
await this.peer.sendText(pubkey, text);
|
|
334
339
|
},
|
|
335
340
|
onClose: () => {
|
|
336
341
|
this.sessions.delete(pubkey);
|
|
@@ -359,42 +364,8 @@ export class PeerManager extends EventEmitter {
|
|
|
359
364
|
throw new Error("Peer not available");
|
|
360
365
|
}
|
|
361
366
|
const frame = FrameCodec.encode(new Uint8Array(0), sessionId, FRAME_OPCODE_HANDSHAKE_ACK);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Send a dora control message to a dora server over the dora custom packet
|
|
366
|
-
* (162, lossless). Replaces the old `DORA:`-prefixed text on packet 64.
|
|
367
|
-
*/
|
|
368
|
-
async sendDora(userid, text) {
|
|
369
|
-
if (!this.peer) {
|
|
370
|
-
throw new Error("Peer not created. Call create() first.");
|
|
371
|
-
}
|
|
372
|
-
await this.peer.sendCustomPacket(userid, PACKET_ID_DL_DORA, Buffer.from(text, "utf-8"));
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Route a decoded decentlan frame to its packet-session, creating or
|
|
376
|
-
* replacing the session on a HANDSHAKE_REQ (handles the Carrier re-pair
|
|
377
|
-
* case where both sides hold different sessionIds).
|
|
378
|
-
*/
|
|
379
|
-
handleDecodedFrame(pubkey, frame) {
|
|
380
|
-
let session = this.sessions.get(pubkey);
|
|
381
|
-
if (FrameCodec.isHandshakeRequest(frame.opcode)) {
|
|
382
|
-
if (session && session.getSessionId() !== frame.sessionId) {
|
|
383
|
-
this.logger.info(`Replacing stale session for ${pubkey} (old=${session.getSessionId()}, new=${frame.sessionId})`);
|
|
384
|
-
session.close();
|
|
385
|
-
session = undefined;
|
|
386
|
-
}
|
|
387
|
-
if (!session) {
|
|
388
|
-
this.logger.info(`Auto-creating session for inbound peer ${pubkey} (sessionId=${frame.sessionId})`);
|
|
389
|
-
session = this.createSession(pubkey, frame.sessionId);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (session) {
|
|
393
|
-
session.handleIncomingFrame(frame);
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
this.logger.debug(`No session for peer ${pubkey}, ignoring frame opcode=0x${frame.opcode.toString(16)}`);
|
|
397
|
-
}
|
|
367
|
+
const frameBase64 = Buffer.from(frame).toString("base64");
|
|
368
|
+
await this.peer.sendText(pubkey, frameBase64);
|
|
398
369
|
}
|
|
399
370
|
setupEventHandlers() {
|
|
400
371
|
if (!this.peer) {
|
|
@@ -435,37 +406,54 @@ export class PeerManager extends EventEmitter {
|
|
|
435
406
|
}
|
|
436
407
|
}
|
|
437
408
|
});
|
|
438
|
-
//
|
|
439
|
-
// interoperable with native Carrier clients. IP traffic and dora moved to
|
|
440
|
-
// their own custom packets (onCustomPacket below). See docs/PROTOCOL.md.
|
|
409
|
+
// Text messages contain our framed packets
|
|
441
410
|
this.peer.onText((message) => {
|
|
442
|
-
// Empty messages are kickSessionEstablishment keepalives — ignore.
|
|
443
|
-
if (message.text.length === 0) {
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
this.emit("message", message.pubkey, message.text);
|
|
447
|
-
});
|
|
448
|
-
// decentlan custom packets: IP data (192, lossy), session handshake
|
|
449
|
-
// (161, lossless), dora control (162, lossless). Each on its own Carrier
|
|
450
|
-
// packet id — a chat message can never be mistaken for an IP packet, and
|
|
451
|
-
// no crafted message can inject onto the TUN.
|
|
452
|
-
this.peer.onCustomPacket(({ pubkey, id, data }) => {
|
|
453
411
|
try {
|
|
454
|
-
|
|
455
|
-
|
|
412
|
+
// Silently skip empty messages — kickSessionEstablishment sends
|
|
413
|
+
// an empty sendText to trigger the SDK's #initiateSession path,
|
|
414
|
+
// and the receiver doesn't need to log a warning for that.
|
|
415
|
+
if (message.text.length === 0) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Dora messages share the same text channel as packet frames but
|
|
419
|
+
// carry a `DORA:` ASCII prefix that's not valid base64. Route them
|
|
420
|
+
// to whoever is subscribed (DoraClient/DoraServer in daemon).
|
|
421
|
+
if (message.text.startsWith(DORA_PREFIX)) {
|
|
422
|
+
this.emit("dora-message", message.pubkey, message.text);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const frameData = Buffer.from(message.text, "base64");
|
|
426
|
+
const frame = FrameCodec.decode(new Uint8Array(frameData));
|
|
427
|
+
if (!frame) {
|
|
428
|
+
this.logger.warn(`Invalid frame from ${message.pubkey}`);
|
|
456
429
|
return;
|
|
457
430
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
431
|
+
let session = this.sessions.get(message.pubkey);
|
|
432
|
+
// For HANDSHAKE_REQ: always create or REPLACE the session with the
|
|
433
|
+
// new sessionId. Without this, after a Carrier session drop + re-pair
|
|
434
|
+
// the peer's new HANDSHAKE_REQ would be dropped because the existing
|
|
435
|
+
// local session has the old sessionId — so the responder never sends
|
|
436
|
+
// HANDSHAKE_ACK and the initiator times out forever.
|
|
437
|
+
if (FrameCodec.isHandshakeRequest(frame.opcode)) {
|
|
438
|
+
if (session && session.getSessionId() !== frame.sessionId) {
|
|
439
|
+
this.logger.info(`Replacing stale session for ${message.pubkey} (old=${session.getSessionId()}, new=${frame.sessionId})`);
|
|
440
|
+
session.close();
|
|
441
|
+
session = undefined;
|
|
463
442
|
}
|
|
464
|
-
|
|
443
|
+
if (!session) {
|
|
444
|
+
this.logger.info(`Auto-creating session for inbound peer ${message.pubkey} (sessionId=${frame.sessionId})`);
|
|
445
|
+
session = this.createSession(message.pubkey, frame.sessionId);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (session) {
|
|
449
|
+
session.handleIncomingFrame(frame);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
this.logger.debug(`No session for peer ${message.pubkey}, ignoring frame opcode=0x${frame.opcode.toString(16)}`);
|
|
465
453
|
}
|
|
466
454
|
}
|
|
467
455
|
catch (error) {
|
|
468
|
-
this.logger.error(`Error processing
|
|
456
|
+
this.logger.error(`Error processing frame from ${message.pubkey}:`, error);
|
|
469
457
|
}
|
|
470
458
|
});
|
|
471
459
|
// Friend requests
|
package/dist/carrier/types.d.ts
CHANGED
|
@@ -8,6 +8,3 @@ export declare const FRAME_OPCODE_HANDSHAKE_ACK = 2;
|
|
|
8
8
|
export declare const FRAME_OPCODE_DATA = 16;
|
|
9
9
|
export declare const FRAME_MAGIC = 170;
|
|
10
10
|
export declare const FRAME_HEADER_SIZE = 6;
|
|
11
|
-
export declare const PACKET_ID_DL_SESSION = 161;
|
|
12
|
-
export declare const PACKET_ID_DL_DORA = 162;
|
|
13
|
-
export declare const PACKET_ID_DL_IP = 192;
|
package/dist/carrier/types.js
CHANGED
|
@@ -9,10 +9,3 @@ export const FRAME_OPCODE_DATA = 0x10;
|
|
|
9
9
|
// Frame structure
|
|
10
10
|
export const FRAME_MAGIC = 0xaa;
|
|
11
11
|
export const FRAME_HEADER_SIZE = 6; // magic(1) + length(2) + sessionId(2) + opcode(1)
|
|
12
|
-
// decentlan application packet IDs in toxcore's custom ranges (see
|
|
13
|
-
// docs/PROTOCOL.md). Sent via peer.sendCustomPacket / received via
|
|
14
|
-
// peer.onCustomPacket — a separate channel from chat (PACKET_ID_MESSAGE 64),
|
|
15
|
-
// so IP traffic can never be confused with a Carrier message.
|
|
16
|
-
export const PACKET_ID_DL_SESSION = 161; // lossless: session handshake frames (must arrive)
|
|
17
|
-
export const PACKET_ID_DL_DORA = 162; // lossless: dora control (must arrive)
|
|
18
|
-
export const PACKET_ID_DL_IP = 192; // lossy: IP data frames (best-effort; inner TCP retransmits)
|
|
@@ -40,7 +40,7 @@ export class DoraIntegration {
|
|
|
40
40
|
this.logger = new Logger({ prefix: "Dora" });
|
|
41
41
|
this.client = new DoraClient({
|
|
42
42
|
registryUserids: opts.config.userids ?? [],
|
|
43
|
-
sendText: (toUserid, text) => opts.peerManager.
|
|
43
|
+
sendText: (toUserid, text) => opts.peerManager.sendText(toUserid, text),
|
|
44
44
|
onText: (handler) => {
|
|
45
45
|
opts.peerManager.on("dora-message", (fromUserid, text) => {
|
|
46
46
|
handler(fromUserid, text);
|
|
@@ -126,11 +126,15 @@ export class ConnectProxy {
|
|
|
126
126
|
}
|
|
127
127
|
let bytes = 0;
|
|
128
128
|
let closed = false;
|
|
129
|
+
let lastBytes = -1;
|
|
130
|
+
let idleTimer;
|
|
129
131
|
const upstream = net.connect(port, host);
|
|
130
132
|
const tearDown = (reason) => {
|
|
131
133
|
if (closed)
|
|
132
134
|
return;
|
|
133
135
|
closed = true;
|
|
136
|
+
if (idleTimer)
|
|
137
|
+
clearInterval(idleTimer);
|
|
134
138
|
upstream.destroy();
|
|
135
139
|
clientSocket.destroy();
|
|
136
140
|
this.stats.active = Math.max(0, this.stats.active - 1);
|
|
@@ -181,6 +185,26 @@ export class ConnectProxy {
|
|
|
181
185
|
upstream.on("end", () => tearDown("upstream end"));
|
|
182
186
|
clientSocket.on("error", (err) => tearDown(`client error: ${err.message}`));
|
|
183
187
|
clientSocket.on("end", () => tearDown("client end"));
|
|
188
|
+
// "close" catches abrupt teardowns (RST / socket destroyed) that never
|
|
189
|
+
// emit "end" — without these, a half-dead tunnel leaks.
|
|
190
|
+
upstream.on("close", () => tearDown("upstream close"));
|
|
191
|
+
clientSocket.on("close", () => tearDown("client close"));
|
|
192
|
+
// Idle reaper: when the Carrier path drops mid-stream the broken side
|
|
193
|
+
// sends no FIN, so neither "end" nor "error" fires and the sockets would
|
|
194
|
+
// leak until the kernel's multi-hour TCP keepalive — they pile up under
|
|
195
|
+
// load (1080p opens many high-bandwidth tunnels; two clients on one exit
|
|
196
|
+
// compounds it) and starve the exit. If no bytes flow in EITHER direction
|
|
197
|
+
// across two idle ticks, tear it down. Active streaming keeps `bytes`
|
|
198
|
+
// climbing (and backpressure pauses a one-sided flow), so only genuinely
|
|
199
|
+
// dead tunnels are reaped. Tunable via AGENTNET_TUNNEL_IDLE_MS.
|
|
200
|
+
const idleMs = Number(process.env.AGENTNET_TUNNEL_IDLE_MS) || 120_000;
|
|
201
|
+
idleTimer = setInterval(() => {
|
|
202
|
+
if (bytes === lastBytes)
|
|
203
|
+
tearDown("idle (no data — Carrier path likely dropped)");
|
|
204
|
+
else
|
|
205
|
+
lastBytes = bytes;
|
|
206
|
+
}, idleMs);
|
|
207
|
+
idleTimer.unref?.();
|
|
184
208
|
}
|
|
185
209
|
refuse(clientSocket, src, srcName, target, code, reason) {
|
|
186
210
|
this.stats.totalRefused += 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.77",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
79
|
"@decentnetwork/dora": "^0.1.6",
|
|
80
|
-
"@decentnetwork/peer": "^0.1.
|
|
80
|
+
"@decentnetwork/peer": "^0.1.32",
|
|
81
81
|
"js-yaml": "^4.1.0",
|
|
82
82
|
"yargs": "^17.7.2"
|
|
83
83
|
},
|