@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.
- package/LICENSE +31 -0
- package/README.md +296 -0
- 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/acl/acl-engine.d.ts +43 -0
- package/dist/acl/acl-engine.js +189 -0
- package/dist/acl/audit.d.ts +70 -0
- package/dist/acl/audit.js +144 -0
- package/dist/acl/index.d.ts +4 -0
- package/dist/acl/index.js +3 -0
- package/dist/acl/policy.d.ts +31 -0
- package/dist/acl/policy.js +102 -0
- package/dist/acl/types.d.ts +18 -0
- package/dist/acl/types.js +4 -0
- package/dist/carrier/frame.d.ts +18 -0
- package/dist/carrier/frame.js +66 -0
- package/dist/carrier/index.d.ts +5 -0
- package/dist/carrier/index.js +4 -0
- package/dist/carrier/packet-session.d.ts +32 -0
- package/dist/carrier/packet-session.js +151 -0
- package/dist/carrier/peer-manager.d.ts +113 -0
- package/dist/carrier/peer-manager.js +392 -0
- package/dist/carrier/types.d.ts +10 -0
- package/dist/carrier/types.js +11 -0
- package/dist/cli/commands.d.ts +223 -0
- package/dist/cli/commands.js +932 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +196 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +152 -0
- package/dist/daemon/index.d.ts +1 -0
- package/dist/daemon/index.js +1 -0
- package/dist/daemon/ipc.d.ts +60 -0
- package/dist/daemon/ipc.js +144 -0
- package/dist/daemon/server.d.ts +63 -0
- package/dist/daemon/server.js +510 -0
- package/dist/dns/index.d.ts +1 -0
- package/dist/dns/index.js +1 -0
- package/dist/dns/resolver.d.ts +44 -0
- package/dist/dns/resolver.js +82 -0
- package/dist/dns/server.d.ts +70 -0
- package/dist/dns/server.js +393 -0
- package/dist/dora/dora-integration.d.ts +90 -0
- package/dist/dora/dora-integration.js +325 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +15 -0
- package/dist/ipam/index.d.ts +1 -0
- package/dist/ipam/index.js +1 -0
- package/dist/ipam/ipam.d.ts +99 -0
- package/dist/ipam/ipam.js +254 -0
- package/dist/proxy/connect-proxy.d.ts +78 -0
- package/dist/proxy/connect-proxy.js +204 -0
- package/dist/router/index.d.ts +5 -0
- package/dist/router/index.js +4 -0
- package/dist/router/ip-parser.d.ts +36 -0
- package/dist/router/ip-parser.js +127 -0
- package/dist/router/packet-router.d.ts +49 -0
- package/dist/router/packet-router.js +251 -0
- package/dist/router/session-manager.d.ts +50 -0
- package/dist/router/session-manager.js +138 -0
- package/dist/router/types.d.ts +21 -0
- package/dist/router/types.js +6 -0
- package/dist/tun/index.d.ts +3 -0
- package/dist/tun/index.js +2 -0
- package/dist/tun/route-manager.d.ts +59 -0
- package/dist/tun/route-manager.js +353 -0
- package/dist/tun/tun-device.d.ts +45 -0
- package/dist/tun/tun-device.js +265 -0
- package/dist/tun/types.d.ts +28 -0
- package/dist/tun/types.js +4 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.js +4 -0
- package/dist/utils/logger.d.ts +20 -0
- package/dist/utils/logger.js +43 -0
- package/docs/CONFIGURATION.md +197 -0
- package/docs/INSTALL.md +145 -0
- 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
|
+
}
|