@decentnetwork/lan 0.1.75 → 0.1.76
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 +4 -2
- package/dist/carrier/packet-session.js +8 -8
- package/dist/carrier/peer-manager.d.ts +11 -0
- package/dist/carrier/peer-manager.js +62 -50
- package/dist/carrier/types.d.ts +3 -0
- package/dist/carrier/types.js +7 -0
- package/dist/dora/dora-integration.js +1 -1
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,13 +6,15 @@ import type { PacketFrame } from "./types.js";
|
|
|
6
6
|
export interface PacketSessionOptions {
|
|
7
7
|
peerId: string;
|
|
8
8
|
sessionId: number;
|
|
9
|
-
|
|
9
|
+
/** Send a raw decentlan frame over the given Carrier custom packet id
|
|
10
|
+
* (PACKET_ID_DL_SESSION for handshake, PACKET_ID_DL_IP for data). */
|
|
11
|
+
sendFrame: (frame: Uint8Array, packetId: number) => Promise<void>;
|
|
10
12
|
onClose?: () => void;
|
|
11
13
|
}
|
|
12
14
|
export declare class PacketSession extends EventEmitter {
|
|
13
15
|
private peerId;
|
|
14
16
|
private sessionId;
|
|
15
|
-
private
|
|
17
|
+
private sendFrame;
|
|
16
18
|
private isActive;
|
|
17
19
|
private handshakeTimeout;
|
|
18
20
|
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, } from "./types.js";
|
|
6
|
+
import { FRAME_OPCODE_HANDSHAKE_REQ, FRAME_OPCODE_DATA, PACKET_ID_DL_SESSION, PACKET_ID_DL_IP, } from "./types.js";
|
|
7
7
|
export class PacketSession extends EventEmitter {
|
|
8
8
|
peerId;
|
|
9
9
|
sessionId;
|
|
10
|
-
|
|
10
|
+
sendFrame;
|
|
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.sendFrame = opts.sendFrame;
|
|
19
19
|
this.on("close", () => {
|
|
20
20
|
if (opts.onClose) {
|
|
21
21
|
opts.onClose();
|
|
@@ -38,9 +38,8 @@ 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 (lossless session channel — must arrive)
|
|
42
42
|
const frame = FrameCodec.encode(new Uint8Array(0), this.sessionId, FRAME_OPCODE_HANDSHAKE_REQ);
|
|
43
|
-
const frameBase64 = Buffer.from(frame).toString("base64");
|
|
44
43
|
// 30s default — Carrier express relay can add several seconds of latency
|
|
45
44
|
// for cross-region peers. Configurable via AGENTNET_HANDSHAKE_TIMEOUT_MS.
|
|
46
45
|
const timeoutMs = parseInt(process.env.AGENTNET_HANDSHAKE_TIMEOUT_MS || "30000", 10);
|
|
@@ -67,7 +66,7 @@ export class PacketSession extends EventEmitter {
|
|
|
67
66
|
this.handshakePromise = null;
|
|
68
67
|
reject(new Error(`Handshake aborted for peer ${this.peerId}`));
|
|
69
68
|
});
|
|
70
|
-
this.
|
|
69
|
+
this.sendFrame(frame, PACKET_ID_DL_SESSION).catch((error) => {
|
|
71
70
|
if (this.handshakeTimeout) {
|
|
72
71
|
clearTimeout(this.handshakeTimeout);
|
|
73
72
|
}
|
|
@@ -82,9 +81,10 @@ export class PacketSession extends EventEmitter {
|
|
|
82
81
|
throw new Error("Packet session not active. Call handshake() first.");
|
|
83
82
|
}
|
|
84
83
|
const frame = FrameCodec.encode(packet, this.sessionId, FRAME_OPCODE_DATA);
|
|
85
|
-
const frameBase64 = Buffer.from(frame).toString("base64");
|
|
86
84
|
try {
|
|
87
|
-
|
|
85
|
+
// IP data → lossy custom packet (best-effort; inner TCP handles
|
|
86
|
+
// reliability — see docs/PROTOCOL.md, avoids TCP-over-TCP).
|
|
87
|
+
await this.sendFrame(frame, PACKET_ID_DL_IP);
|
|
88
88
|
}
|
|
89
89
|
catch (error) {
|
|
90
90
|
// Don't tear down the whole session for a single failed sendText —
|
|
@@ -152,5 +152,16 @@ 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;
|
|
155
166
|
private setupEventHandlers;
|
|
156
167
|
}
|
|
@@ -6,13 +6,8 @@ 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, } from "./types.js";
|
|
9
|
+
import { FRAME_OPCODE_HANDSHAKE_ACK, PACKET_ID_DL_SESSION, PACKET_ID_DL_DORA, PACKET_ID_DL_IP, } 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:";
|
|
16
11
|
export class PeerManager extends EventEmitter {
|
|
17
12
|
peer = null;
|
|
18
13
|
identity = null;
|
|
@@ -331,11 +326,11 @@ export class PeerManager extends EventEmitter {
|
|
|
331
326
|
const session = new PacketSession({
|
|
332
327
|
peerId: pubkey,
|
|
333
328
|
sessionId,
|
|
334
|
-
|
|
329
|
+
sendFrame: async (frame, packetId) => {
|
|
335
330
|
if (!this.peer) {
|
|
336
331
|
throw new Error("Peer not available");
|
|
337
332
|
}
|
|
338
|
-
await this.peer.
|
|
333
|
+
await this.peer.sendCustomPacket(pubkey, packetId, frame);
|
|
339
334
|
},
|
|
340
335
|
onClose: () => {
|
|
341
336
|
this.sessions.delete(pubkey);
|
|
@@ -364,8 +359,42 @@ export class PeerManager extends EventEmitter {
|
|
|
364
359
|
throw new Error("Peer not available");
|
|
365
360
|
}
|
|
366
361
|
const frame = FrameCodec.encode(new Uint8Array(0), sessionId, FRAME_OPCODE_HANDSHAKE_ACK);
|
|
367
|
-
|
|
368
|
-
|
|
362
|
+
await this.peer.sendCustomPacket(pubkey, PACKET_ID_DL_SESSION, frame);
|
|
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
|
+
}
|
|
369
398
|
}
|
|
370
399
|
setupEventHandlers() {
|
|
371
400
|
if (!this.peer) {
|
|
@@ -406,54 +435,37 @@ export class PeerManager extends EventEmitter {
|
|
|
406
435
|
}
|
|
407
436
|
}
|
|
408
437
|
});
|
|
409
|
-
//
|
|
438
|
+
// Packet 64 (PACKET_ID_MESSAGE) is now CHAT only — plain Carrier text,
|
|
439
|
+
// interoperable with native Carrier clients. IP traffic and dora moved to
|
|
440
|
+
// their own custom packets (onCustomPacket below). See docs/PROTOCOL.md.
|
|
410
441
|
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 }) => {
|
|
411
453
|
try {
|
|
412
|
-
|
|
413
|
-
|
|
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}`);
|
|
454
|
+
if (id === PACKET_ID_DL_DORA) {
|
|
455
|
+
this.emit("dora-message", pubkey, Buffer.from(data).toString("utf-8"));
|
|
429
456
|
return;
|
|
430
457
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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;
|
|
458
|
+
if (id === PACKET_ID_DL_IP || id === PACKET_ID_DL_SESSION) {
|
|
459
|
+
const frame = FrameCodec.decode(data);
|
|
460
|
+
if (!frame) {
|
|
461
|
+
this.logger.warn(`Invalid frame from ${pubkey} (packet ${id})`);
|
|
462
|
+
return;
|
|
442
463
|
}
|
|
443
|
-
|
|
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)}`);
|
|
464
|
+
this.handleDecodedFrame(pubkey, frame);
|
|
453
465
|
}
|
|
454
466
|
}
|
|
455
467
|
catch (error) {
|
|
456
|
-
this.logger.error(`Error processing
|
|
468
|
+
this.logger.error(`Error processing custom packet from ${pubkey}:`, error);
|
|
457
469
|
}
|
|
458
470
|
});
|
|
459
471
|
// Friend requests
|
package/dist/carrier/types.d.ts
CHANGED
|
@@ -8,3 +8,6 @@ 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,3 +9,10 @@ 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.sendDora(toUserid, text),
|
|
44
44
|
onText: (handler) => {
|
|
45
45
|
opts.peerManager.on("dora-message", (fromUserid, text) => {
|
|
46
46
|
handler(fromUserid, text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.76",
|
|
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.33",
|
|
81
81
|
"js-yaml": "^4.1.0",
|
|
82
82
|
"yargs": "^17.7.2"
|
|
83
83
|
},
|