@decentnetwork/lan 0.1.0

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.
Files changed (79) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +296 -0
  3. package/bin/tun-helper-darwin-amd64 +0 -0
  4. package/bin/tun-helper-darwin-arm64 +0 -0
  5. package/bin/tun-helper-linux-amd64 +0 -0
  6. package/bin/tun-helper-linux-arm64 +0 -0
  7. package/dist/acl/acl-engine.d.ts +43 -0
  8. package/dist/acl/acl-engine.js +189 -0
  9. package/dist/acl/audit.d.ts +70 -0
  10. package/dist/acl/audit.js +144 -0
  11. package/dist/acl/index.d.ts +4 -0
  12. package/dist/acl/index.js +3 -0
  13. package/dist/acl/policy.d.ts +31 -0
  14. package/dist/acl/policy.js +102 -0
  15. package/dist/acl/types.d.ts +18 -0
  16. package/dist/acl/types.js +4 -0
  17. package/dist/carrier/frame.d.ts +18 -0
  18. package/dist/carrier/frame.js +66 -0
  19. package/dist/carrier/index.d.ts +5 -0
  20. package/dist/carrier/index.js +4 -0
  21. package/dist/carrier/packet-session.d.ts +32 -0
  22. package/dist/carrier/packet-session.js +151 -0
  23. package/dist/carrier/peer-manager.d.ts +113 -0
  24. package/dist/carrier/peer-manager.js +392 -0
  25. package/dist/carrier/types.d.ts +10 -0
  26. package/dist/carrier/types.js +11 -0
  27. package/dist/cli/commands.d.ts +223 -0
  28. package/dist/cli/commands.js +932 -0
  29. package/dist/cli/index.d.ts +7 -0
  30. package/dist/cli/index.js +196 -0
  31. package/dist/config/loader.d.ts +10 -0
  32. package/dist/config/loader.js +152 -0
  33. package/dist/daemon/index.d.ts +1 -0
  34. package/dist/daemon/index.js +1 -0
  35. package/dist/daemon/ipc.d.ts +60 -0
  36. package/dist/daemon/ipc.js +144 -0
  37. package/dist/daemon/server.d.ts +63 -0
  38. package/dist/daemon/server.js +510 -0
  39. package/dist/dns/index.d.ts +1 -0
  40. package/dist/dns/index.js +1 -0
  41. package/dist/dns/resolver.d.ts +44 -0
  42. package/dist/dns/resolver.js +82 -0
  43. package/dist/dns/server.d.ts +70 -0
  44. package/dist/dns/server.js +393 -0
  45. package/dist/dora/dora-integration.d.ts +90 -0
  46. package/dist/dora/dora-integration.js +325 -0
  47. package/dist/index.d.ts +13 -0
  48. package/dist/index.js +15 -0
  49. package/dist/ipam/index.d.ts +1 -0
  50. package/dist/ipam/index.js +1 -0
  51. package/dist/ipam/ipam.d.ts +99 -0
  52. package/dist/ipam/ipam.js +254 -0
  53. package/dist/proxy/connect-proxy.d.ts +78 -0
  54. package/dist/proxy/connect-proxy.js +204 -0
  55. package/dist/router/index.d.ts +5 -0
  56. package/dist/router/index.js +4 -0
  57. package/dist/router/ip-parser.d.ts +36 -0
  58. package/dist/router/ip-parser.js +127 -0
  59. package/dist/router/packet-router.d.ts +49 -0
  60. package/dist/router/packet-router.js +251 -0
  61. package/dist/router/session-manager.d.ts +50 -0
  62. package/dist/router/session-manager.js +138 -0
  63. package/dist/router/types.d.ts +21 -0
  64. package/dist/router/types.js +6 -0
  65. package/dist/tun/index.d.ts +3 -0
  66. package/dist/tun/index.js +2 -0
  67. package/dist/tun/route-manager.d.ts +59 -0
  68. package/dist/tun/route-manager.js +353 -0
  69. package/dist/tun/tun-device.d.ts +45 -0
  70. package/dist/tun/tun-device.js +265 -0
  71. package/dist/tun/types.d.ts +28 -0
  72. package/dist/tun/types.js +4 -0
  73. package/dist/types.d.ts +176 -0
  74. package/dist/types.js +4 -0
  75. package/dist/utils/logger.d.ts +20 -0
  76. package/dist/utils/logger.js +43 -0
  77. package/docs/CONFIGURATION.md +197 -0
  78. package/docs/INSTALL.md +145 -0
  79. package/package.json +93 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * IPv4 packet parser
3
+ * Extracts source/dest IP, protocol, and (for TCP/UDP) ports
4
+ */
5
+ import { IP_PROTO_TCP, IP_PROTO_UDP } from "./types.js";
6
+ export class IpParser {
7
+ /**
8
+ * Parse an IPv4 packet.
9
+ * Returns null if not a valid IPv4 packet.
10
+ */
11
+ static parse(packet) {
12
+ if (packet.length < 20) {
13
+ return null;
14
+ }
15
+ // Version & header length (first byte)
16
+ const versionAndIhl = packet[0];
17
+ const version = (versionAndIhl >> 4) & 0x0f;
18
+ const ihl = versionAndIhl & 0x0f; // Header length in 32-bit words
19
+ if (version !== 4) {
20
+ return null; // Only IPv4 supported in MVP
21
+ }
22
+ const headerBytes = ihl * 4;
23
+ if (packet.length < headerBytes) {
24
+ return null;
25
+ }
26
+ // Protocol (byte 9)
27
+ const protocol = packet[9];
28
+ // Source IP (bytes 12-15)
29
+ const srcIp = `${packet[12]}.${packet[13]}.${packet[14]}.${packet[15]}`;
30
+ // Destination IP (bytes 16-19)
31
+ const dstIp = `${packet[16]}.${packet[17]}.${packet[18]}.${packet[19]}`;
32
+ const result = {
33
+ version,
34
+ protocol,
35
+ srcIp,
36
+ dstIp,
37
+ };
38
+ // For TCP/UDP, extract ports
39
+ if ((protocol === IP_PROTO_TCP || protocol === IP_PROTO_UDP) && packet.length >= headerBytes + 4) {
40
+ result.srcPort = (packet[headerBytes] << 8) | packet[headerBytes + 1];
41
+ result.dstPort = (packet[headerBytes + 2] << 8) | packet[headerBytes + 3];
42
+ }
43
+ return result;
44
+ }
45
+ /**
46
+ * Get the protocol name for logging.
47
+ */
48
+ static protoName(protocol) {
49
+ switch (protocol) {
50
+ case IP_PROTO_TCP:
51
+ return "tcp";
52
+ case IP_PROTO_UDP:
53
+ return "udp";
54
+ case 1:
55
+ return "icmp";
56
+ default:
57
+ return `proto-${protocol}`;
58
+ }
59
+ }
60
+ /**
61
+ * Check if protocol is TCP.
62
+ */
63
+ static isTcp(protocol) {
64
+ return protocol === IP_PROTO_TCP;
65
+ }
66
+ /**
67
+ * Check if protocol is UDP.
68
+ */
69
+ static isUdp(protocol) {
70
+ return protocol === IP_PROTO_UDP;
71
+ }
72
+ /**
73
+ * Build a minimal IPv4 packet header for testing.
74
+ * Returns a 20-byte header plus optional payload.
75
+ */
76
+ static buildIpv4Header(opts) {
77
+ const payloadLen = opts.payload?.length || 0;
78
+ const portsLen = opts.srcPort !== undefined && opts.dstPort !== undefined ? 4 : 0;
79
+ const totalLen = 20 + portsLen + payloadLen;
80
+ const packet = new Uint8Array(totalLen);
81
+ // Version=4, IHL=5
82
+ packet[0] = 0x45;
83
+ // DSCP/ECN
84
+ packet[1] = 0;
85
+ // Total length
86
+ packet[2] = (totalLen >> 8) & 0xff;
87
+ packet[3] = totalLen & 0xff;
88
+ // Identification
89
+ packet[4] = 0;
90
+ packet[5] = 0;
91
+ // Flags + Fragment offset
92
+ packet[6] = 0;
93
+ packet[7] = 0;
94
+ // TTL
95
+ packet[8] = 64;
96
+ // Protocol
97
+ packet[9] = opts.protocol;
98
+ // Header checksum (skip for testing)
99
+ packet[10] = 0;
100
+ packet[11] = 0;
101
+ // Source IP
102
+ const srcParts = opts.srcIp.split(".").map((s) => parseInt(s, 10));
103
+ packet[12] = srcParts[0];
104
+ packet[13] = srcParts[1];
105
+ packet[14] = srcParts[2];
106
+ packet[15] = srcParts[3];
107
+ // Destination IP
108
+ const dstParts = opts.dstIp.split(".").map((s) => parseInt(s, 10));
109
+ packet[16] = dstParts[0];
110
+ packet[17] = dstParts[1];
111
+ packet[18] = dstParts[2];
112
+ packet[19] = dstParts[3];
113
+ let offset = 20;
114
+ // Optional ports (TCP/UDP)
115
+ if (portsLen > 0) {
116
+ packet[offset++] = (opts.srcPort >> 8) & 0xff;
117
+ packet[offset++] = opts.srcPort & 0xff;
118
+ packet[offset++] = (opts.dstPort >> 8) & 0xff;
119
+ packet[offset++] = opts.dstPort & 0xff;
120
+ }
121
+ // Payload
122
+ if (opts.payload) {
123
+ packet.set(opts.payload, offset);
124
+ }
125
+ return packet;
126
+ }
127
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * PacketRouter — main forwarding engine
3
+ *
4
+ * Flow:
5
+ * TUN read -> parse IP -> IPAM lookup -> ACL check -> Carrier send
6
+ * Carrier recv -> ACL check -> TUN write
7
+ */
8
+ import { EventEmitter } from "events";
9
+ import type { TunDevice } from "../tun/tun-device.js";
10
+ import type { PeerManager } from "../carrier/peer-manager.js";
11
+ import type { Ipam } from "../ipam/ipam.js";
12
+ import type { AclEngine } from "../acl/acl-engine.js";
13
+ import type { ForwardingStats } from "./types.js";
14
+ export interface PacketRouterOptions {
15
+ tunDevice: TunDevice;
16
+ peerManager: PeerManager;
17
+ ipam: Ipam;
18
+ acl: AclEngine;
19
+ }
20
+ export declare class PacketRouter extends EventEmitter {
21
+ private tunDevice;
22
+ private peerManager;
23
+ private acl;
24
+ private sessionManager;
25
+ private logger;
26
+ private isRunning;
27
+ private listenedSessions;
28
+ private keepaliveTimer?;
29
+ private stats;
30
+ constructor(opts: PacketRouterOptions);
31
+ start(): Promise<void>;
32
+ stop(): Promise<void>;
33
+ /**
34
+ * Periodically send a 1-byte ping through each connected packet session
35
+ * so the Carrier crypto session doesn't idle out. The receiver's IpParser
36
+ * will reject the malformed IP packet (length<20) and silently drop it.
37
+ */
38
+ private sendKeepalivesToActiveSessions;
39
+ getStats(): ForwardingStats;
40
+ /**
41
+ * Handle outgoing packet (app -> TUN -> Carrier).
42
+ */
43
+ private handleOutgoingPacket;
44
+ /**
45
+ * Handle incoming packet (Carrier -> peer -> TUN).
46
+ */
47
+ private handleIncomingPacket;
48
+ private setupCarrierHandlers;
49
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * PacketRouter — main forwarding engine
3
+ *
4
+ * Flow:
5
+ * TUN read -> parse IP -> IPAM lookup -> ACL check -> Carrier send
6
+ * Carrier recv -> ACL check -> TUN write
7
+ */
8
+ import { EventEmitter } from "events";
9
+ import { IpParser } from "./ip-parser.js";
10
+ import { SessionManager } from "./session-manager.js";
11
+ import { Logger } from "../utils/logger.js";
12
+ export class PacketRouter extends EventEmitter {
13
+ tunDevice;
14
+ peerManager;
15
+ acl;
16
+ sessionManager;
17
+ logger;
18
+ isRunning = false;
19
+ listenedSessions = new WeakSet();
20
+ keepaliveTimer;
21
+ stats = {
22
+ packetsForwarded: 0,
23
+ packetsReceived: 0,
24
+ packetsDenied: 0,
25
+ packetsDropped: 0,
26
+ activeSessions: 0,
27
+ };
28
+ constructor(opts) {
29
+ super();
30
+ this.tunDevice = opts.tunDevice;
31
+ this.peerManager = opts.peerManager;
32
+ this.acl = opts.acl;
33
+ this.sessionManager = new SessionManager({
34
+ peerManager: opts.peerManager,
35
+ ipam: opts.ipam,
36
+ });
37
+ this.logger = new Logger({ prefix: "PacketRouter" });
38
+ this.setupCarrierHandlers();
39
+ }
40
+ async start() {
41
+ if (this.isRunning)
42
+ return;
43
+ this.logger.info("Starting packet router");
44
+ this.isRunning = true;
45
+ // Listen for TUN packets
46
+ this.tunDevice.on("packet", (packet) => {
47
+ this.handleOutgoingPacket(packet).catch((err) => {
48
+ // "friend offline" / "no transport" / "Handshake timeout" are all
49
+ // expected when a peer's daemon isn't up or hasn't completed the
50
+ // Carrier session yet — they're informational, not errors. Drop
51
+ // them to debug so the operator's log shows real failures.
52
+ const msg = err instanceof Error ? err.message : String(err);
53
+ if (msg.includes("offline") ||
54
+ msg.includes("no transport") ||
55
+ msg.includes("Handshake timeout")) {
56
+ this.logger.debug(`Outgoing packet dropped: ${msg}`);
57
+ }
58
+ else {
59
+ this.logger.error("Outgoing packet error:", err);
60
+ }
61
+ });
62
+ });
63
+ await this.tunDevice.startReadLoop();
64
+ // Periodic keepalive: send a tiny zero-byte packet through each active
65
+ // session every 10s. This keeps the Carrier crypto session alive (which
66
+ // times out at 32s of no data) so an idle TCP connection like SSH won't
67
+ // get reaped between user keystrokes.
68
+ this.keepaliveTimer = setInterval(() => {
69
+ this.sendKeepalivesToActiveSessions();
70
+ }, 10000);
71
+ this.logger.info("Packet router started");
72
+ }
73
+ async stop() {
74
+ if (!this.isRunning)
75
+ return;
76
+ this.logger.info("Stopping packet router");
77
+ this.isRunning = false;
78
+ if (this.keepaliveTimer) {
79
+ clearInterval(this.keepaliveTimer);
80
+ this.keepaliveTimer = undefined;
81
+ }
82
+ await this.tunDevice.stopReadLoop();
83
+ this.sessionManager.closeAll();
84
+ this.logger.info("Packet router stopped");
85
+ }
86
+ /**
87
+ * Periodically send a 1-byte ping through each connected packet session
88
+ * so the Carrier crypto session doesn't idle out. The receiver's IpParser
89
+ * will reject the malformed IP packet (length<20) and silently drop it.
90
+ */
91
+ async sendKeepalivesToActiveSessions() {
92
+ if (!this.isRunning)
93
+ return;
94
+ const peerIds = this.sessionManager.getActiveSessions();
95
+ for (const dstIp of peerIds) {
96
+ const session = this.sessionManager.getSession(dstIp);
97
+ if (!session?.isConnected())
98
+ continue;
99
+ try {
100
+ await session.send(new Uint8Array([0])); // 1-byte keepalive, dropped at receiver
101
+ }
102
+ catch {
103
+ // ignore — session may have just gone offline
104
+ }
105
+ }
106
+ }
107
+ getStats() {
108
+ return {
109
+ ...this.stats,
110
+ activeSessions: this.sessionManager.getActiveCount(),
111
+ };
112
+ }
113
+ /**
114
+ * Handle outgoing packet (app -> TUN -> Carrier).
115
+ */
116
+ async handleOutgoingPacket(packet) {
117
+ if (!this.isRunning)
118
+ return;
119
+ const parsed = IpParser.parse(packet);
120
+ if (!parsed) {
121
+ this.stats.packetsDropped++;
122
+ this.logger.debug("Dropped invalid IP packet");
123
+ return;
124
+ }
125
+ // Get our own pubkey for ACL (we are the source for outbound)
126
+ const srcPubkey = this.peerManager.getPubkey();
127
+ const proto = IpParser.protoName(parsed.protocol);
128
+ // Only forward TCP/UDP packets that have ports
129
+ if (parsed.dstPort === undefined && proto !== "icmp") {
130
+ this.stats.packetsDropped++;
131
+ return;
132
+ }
133
+ // ACL check (outbound)
134
+ if (parsed.dstPort !== undefined && (proto === "tcp" || proto === "udp")) {
135
+ const result = this.acl.evaluate({
136
+ srcPubkey,
137
+ dstIp: parsed.dstIp,
138
+ dstPort: parsed.dstPort,
139
+ srcPort: parsed.srcPort,
140
+ proto,
141
+ direction: "outbound",
142
+ });
143
+ if (!result.allowed) {
144
+ this.stats.packetsDenied++;
145
+ this.logger.debug(`Denied outbound: ${parsed.srcIp}:${parsed.srcPort || "-"} -> ${parsed.dstIp}:${parsed.dstPort} (${result.reason})`);
146
+ return;
147
+ }
148
+ }
149
+ // Get session for destination
150
+ const session = await this.sessionManager.getOrOpenSession(parsed.dstIp);
151
+ if (!session) {
152
+ this.stats.packetsDropped++;
153
+ this.logger.debug(`No peer for ${parsed.dstIp}, dropping`);
154
+ return;
155
+ }
156
+ // Send packet
157
+ try {
158
+ await session.send(packet);
159
+ this.stats.packetsForwarded++;
160
+ this.logger.debug(`Forwarded ${proto} ${parsed.srcIp}:${parsed.srcPort || "-"} -> ${parsed.dstIp}:${parsed.dstPort || "-"} (${packet.length} bytes)`);
161
+ }
162
+ catch (err) {
163
+ // Friend going offline mid-send is normal during connection flapping;
164
+ // log at debug rather than warn to avoid spamming the operator.
165
+ const msg = err instanceof Error ? err.message : String(err);
166
+ this.stats.packetsDropped++;
167
+ if (msg.includes("offline") || msg.includes("no transport")) {
168
+ this.logger.debug(`Drop ${parsed.dstIp}: ${msg}`);
169
+ }
170
+ else {
171
+ this.logger.warn(`Failed to forward to ${parsed.dstIp}: ${msg}`);
172
+ }
173
+ }
174
+ }
175
+ /**
176
+ * Handle incoming packet (Carrier -> peer -> TUN).
177
+ */
178
+ async handleIncomingPacket(srcPubkey, packet) {
179
+ if (!this.isRunning)
180
+ return;
181
+ const parsed = IpParser.parse(packet);
182
+ if (!parsed) {
183
+ this.stats.packetsDropped++;
184
+ this.logger.debug("Dropped invalid incoming IP packet");
185
+ return;
186
+ }
187
+ const proto = IpParser.protoName(parsed.protocol);
188
+ // ACL check (inbound)
189
+ if (parsed.dstPort !== undefined && (proto === "tcp" || proto === "udp")) {
190
+ const result = this.acl.evaluate({
191
+ srcPubkey,
192
+ dstIp: parsed.dstIp,
193
+ dstPort: parsed.dstPort,
194
+ srcPort: parsed.srcPort,
195
+ proto,
196
+ direction: "inbound",
197
+ });
198
+ if (!result.allowed) {
199
+ this.stats.packetsDenied++;
200
+ this.logger.debug(`Denied inbound: ${parsed.srcIp}:${parsed.srcPort || "-"} -> ${parsed.dstIp}:${parsed.dstPort} (${result.reason})`);
201
+ return;
202
+ }
203
+ }
204
+ // Write to TUN. A second isRunning check handles the shutdown race:
205
+ // Carrier keeps delivering packets via TCP relay even after we've
206
+ // closed the TUN device, and our PacketSession listeners are still
207
+ // attached. Without this guard, every late packet that arrives
208
+ // between TUN close and full session teardown produces a "TUN device
209
+ // not open" warning. Treat that case as a silent drop.
210
+ if (!this.isRunning || !this.tunDevice.isActive()) {
211
+ this.stats.packetsDropped++;
212
+ return;
213
+ }
214
+ try {
215
+ await this.tunDevice.write(packet);
216
+ this.stats.packetsReceived++;
217
+ this.logger.debug(`Received ${proto} ${parsed.srcIp}:${parsed.srcPort || "-"} -> ${parsed.dstIp}:${parsed.dstPort || "-"} (${packet.length} bytes)`);
218
+ }
219
+ catch (err) {
220
+ this.stats.packetsDropped++;
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ if (msg.includes("TUN device not open")) {
223
+ // Late packet during shutdown — already covered by the guard
224
+ // above on most paths but the TUN can also close mid-write.
225
+ this.logger.debug(`Drop late inbound packet: ${msg}`);
226
+ }
227
+ else {
228
+ this.logger.warn(`Failed to write to TUN:`, err);
229
+ }
230
+ }
231
+ }
232
+ setupCarrierHandlers() {
233
+ // When PeerManager creates a session (inbound or outbound), register packet handler.
234
+ // Guard against double-attachment: this event fires from both the responder-side
235
+ // handshake-req handler and the initiator-side openPacketSession completion. Without
236
+ // the WeakSet check, each peer-arriving packet would be written to TUN twice, which
237
+ // shows up as `(DUP!)` ICMP replies on the sender side.
238
+ this.peerManager.on("session-opened", ({ pubkey }) => {
239
+ const session = this.peerManager.getSession(pubkey);
240
+ if (session && !this.listenedSessions.has(session)) {
241
+ this.listenedSessions.add(session);
242
+ this.sessionManager.registerInboundSession(pubkey, session);
243
+ session.on("packet", (packet) => {
244
+ this.handleIncomingPacket(pubkey, packet).catch((err) => {
245
+ this.logger.error("Incoming packet error:", err);
246
+ });
247
+ });
248
+ }
249
+ });
250
+ }
251
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Session Manager
3
+ * Manages packet sessions keyed by destination IP
4
+ */
5
+ import type { PacketSession } from "../carrier/packet-session.js";
6
+ import type { PeerManager } from "../carrier/peer-manager.js";
7
+ import type { Ipam } from "../ipam/ipam.js";
8
+ export declare class SessionManager {
9
+ private peerManager;
10
+ private ipam;
11
+ private sessions;
12
+ private opening;
13
+ private logger;
14
+ constructor(opts: {
15
+ peerManager: PeerManager;
16
+ ipam: Ipam;
17
+ });
18
+ /**
19
+ * Get or open a session to the peer responsible for dstIp.
20
+ * Coalesces concurrent calls to avoid multiple handshakes.
21
+ * Returns null if peer is unknown or offline.
22
+ */
23
+ getOrOpenSession(dstIp: string): Promise<PacketSession | null>;
24
+ /**
25
+ * Get session by destination IP (no handshake)
26
+ */
27
+ getSession(dstIp: string): PacketSession | null;
28
+ /**
29
+ * Register a session that was opened by remote peer (responder side).
30
+ * Called when PeerManager auto-creates session for inbound HANDSHAKE_REQ.
31
+ */
32
+ registerInboundSession(srcPubkey: string, session: PacketSession): void;
33
+ /**
34
+ * Close session for a specific destination IP
35
+ */
36
+ closeSession(dstIp: string): void;
37
+ /**
38
+ * Close all sessions
39
+ */
40
+ closeAll(): void;
41
+ /**
42
+ * Get number of active sessions
43
+ */
44
+ getActiveCount(): number;
45
+ /**
46
+ * Get all active session destinations
47
+ */
48
+ getActiveSessions(): string[];
49
+ private openSession;
50
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Session Manager
3
+ * Manages packet sessions keyed by destination IP
4
+ */
5
+ import { Logger } from "../utils/logger.js";
6
+ export class SessionManager {
7
+ peerManager;
8
+ ipam;
9
+ sessions = new Map(); // dstIp -> session
10
+ opening = new Map(); // dstIp -> in-flight handshake
11
+ logger;
12
+ constructor(opts) {
13
+ this.peerManager = opts.peerManager;
14
+ this.ipam = opts.ipam;
15
+ this.logger = new Logger({ prefix: "SessionManager" });
16
+ // Drop our IP→session cache when a friend's Carrier-level
17
+ // connection flips. PeerManager already closes the underlying
18
+ // PacketSession, but our map still holds the (now-closed)
19
+ // reference — and an isConnected() check during the brief
20
+ // window between the close() call and our re-keying race
21
+ // could return stale data. Belt-and-braces.
22
+ this.peerManager.on("friend-connection", (evt) => {
23
+ const record = this.ipam.resolveCarrierId(evt.pubkey);
24
+ if (record && this.sessions.has(record.virtualIp)) {
25
+ this.logger.debug(`Dropping ${record.name} (${record.virtualIp}) session on friend-${evt.status}`);
26
+ this.sessions.delete(record.virtualIp);
27
+ }
28
+ });
29
+ }
30
+ /**
31
+ * Get or open a session to the peer responsible for dstIp.
32
+ * Coalesces concurrent calls to avoid multiple handshakes.
33
+ * Returns null if peer is unknown or offline.
34
+ */
35
+ async getOrOpenSession(dstIp) {
36
+ // Already have session
37
+ const existing = this.sessions.get(dstIp);
38
+ if (existing && existing.isConnected()) {
39
+ return existing;
40
+ }
41
+ // In-flight handshake
42
+ const inFlight = this.opening.get(dstIp);
43
+ if (inFlight) {
44
+ return inFlight;
45
+ }
46
+ // Look up peer in IPAM
47
+ const record = this.ipam.resolveIp(dstIp);
48
+ if (!record) {
49
+ this.logger.debug(`No IPAM record for ${dstIp}`);
50
+ return null;
51
+ }
52
+ // Skip if friend isn't online — sendText() would otherwise queue
53
+ // packets via the slow express HTTP relay, which can't keep up with
54
+ // packet rates and produces error spam.
55
+ if (!this.peerManager.isFriendOnline(record.carrierId)) {
56
+ this.logger.debug(`Friend ${record.name} (${dstIp}) not online — dropping`);
57
+ return null;
58
+ }
59
+ // Open new session
60
+ const promise = this.openSession(dstIp, record.carrierId);
61
+ this.opening.set(dstIp, promise);
62
+ try {
63
+ const session = await promise;
64
+ this.sessions.set(dstIp, session);
65
+ return session;
66
+ }
67
+ finally {
68
+ this.opening.delete(dstIp);
69
+ }
70
+ }
71
+ /**
72
+ * Get session by destination IP (no handshake)
73
+ */
74
+ getSession(dstIp) {
75
+ return this.sessions.get(dstIp) || null;
76
+ }
77
+ /**
78
+ * Register a session that was opened by remote peer (responder side).
79
+ * Called when PeerManager auto-creates session for inbound HANDSHAKE_REQ.
80
+ */
81
+ registerInboundSession(srcPubkey, session) {
82
+ const record = this.ipam.resolveCarrierId(srcPubkey);
83
+ if (record) {
84
+ this.sessions.set(record.virtualIp, session);
85
+ this.logger.info(`Registered inbound session from ${record.name} (${record.virtualIp})`);
86
+ }
87
+ else {
88
+ this.logger.warn(`Inbound session from unknown peer ${srcPubkey}`);
89
+ }
90
+ }
91
+ /**
92
+ * Close session for a specific destination IP
93
+ */
94
+ closeSession(dstIp) {
95
+ const session = this.sessions.get(dstIp);
96
+ if (session) {
97
+ session.close();
98
+ this.sessions.delete(dstIp);
99
+ }
100
+ }
101
+ /**
102
+ * Close all sessions
103
+ */
104
+ closeAll() {
105
+ for (const session of this.sessions.values()) {
106
+ session.close();
107
+ }
108
+ this.sessions.clear();
109
+ this.opening.clear();
110
+ }
111
+ /**
112
+ * Get number of active sessions
113
+ */
114
+ getActiveCount() {
115
+ let count = 0;
116
+ for (const session of this.sessions.values()) {
117
+ if (session.isConnected())
118
+ count++;
119
+ }
120
+ return count;
121
+ }
122
+ /**
123
+ * Get all active session destinations
124
+ */
125
+ getActiveSessions() {
126
+ const result = [];
127
+ for (const [dstIp, session] of this.sessions) {
128
+ if (session.isConnected()) {
129
+ result.push(dstIp);
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+ async openSession(dstIp, carrierId) {
135
+ this.logger.info(`Opening session to ${dstIp} (carrier: ${carrierId.slice(0, 16)}...)`);
136
+ return await this.peerManager.openPacketSession(carrierId);
137
+ }
138
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Router types
3
+ */
4
+ export interface ParsedPacket {
5
+ version: number;
6
+ protocol: number;
7
+ srcIp: string;
8
+ dstIp: string;
9
+ srcPort?: number;
10
+ dstPort?: number;
11
+ }
12
+ export declare const IP_PROTO_TCP = 6;
13
+ export declare const IP_PROTO_UDP = 17;
14
+ export declare const IP_PROTO_ICMP = 1;
15
+ export interface ForwardingStats {
16
+ packetsForwarded: number;
17
+ packetsReceived: number;
18
+ packetsDenied: number;
19
+ packetsDropped: number;
20
+ activeSessions: number;
21
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Router types
3
+ */
4
+ export const IP_PROTO_TCP = 6;
5
+ export const IP_PROTO_UDP = 17;
6
+ export const IP_PROTO_ICMP = 1;
@@ -0,0 +1,3 @@
1
+ export { TunDevice, type TunDeviceOptions } from "./tun-device.js";
2
+ export { RouteManager } from "./route-manager.js";
3
+ export type { TunDeviceConfig, RouteConfig, IpHeader } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { TunDevice } from "./tun-device.js";
2
+ export { RouteManager } from "./route-manager.js";