@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,510 @@
1
+ /**
2
+ * Daemon Server — wires together all components
3
+ */
4
+ import { resolve } from "path";
5
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
6
+ import { PeerManager } from "../carrier/peer-manager.js";
7
+ import { TunDevice } from "../tun/tun-device.js";
8
+ import { RouteManager } from "../tun/route-manager.js";
9
+ import { Ipam } from "../ipam/ipam.js";
10
+ import { DnsResolver } from "../dns/resolver.js";
11
+ import { Policy } from "../acl/policy.js";
12
+ import { AuditLog } from "../acl/audit.js";
13
+ import { AclEngine } from "../acl/acl-engine.js";
14
+ import { PacketRouter } from "../router/packet-router.js";
15
+ import { ConnectProxy } from "../proxy/connect-proxy.js";
16
+ import { DoraIntegration } from "../dora/dora-integration.js";
17
+ import { DnsServer } from "../dns/server.js";
18
+ import { IpcServer, ipcSocketPath } from "./ipc.js";
19
+ import { Logger } from "../utils/logger.js";
20
+ export class DaemonServer {
21
+ config;
22
+ useMockTun;
23
+ logger;
24
+ // Components
25
+ peerManager;
26
+ tunDevice;
27
+ routeManager;
28
+ ipam;
29
+ dnsResolver;
30
+ policy;
31
+ auditLog;
32
+ aclEngine;
33
+ packetRouter;
34
+ connectProxy;
35
+ doraIntegration;
36
+ ipcServer;
37
+ dnsServer;
38
+ startedAt = 0;
39
+ isRunning = false;
40
+ pidFile;
41
+ constructor(opts) {
42
+ this.config = opts.config;
43
+ this.useMockTun = opts.useMockTun ?? true; // Default to mock; override for production
44
+ this.logger = new Logger({ prefix: "Daemon" });
45
+ }
46
+ async start() {
47
+ if (this.isRunning) {
48
+ throw new Error("Daemon already running");
49
+ }
50
+ // Reject a second instance for the same identity. Two daemons using
51
+ // the same keypair race on Carrier session establishment, fight over
52
+ // the TUN interface, and produce a stream of "Replacing stale session"
53
+ // messages with no useful outcome. Use a pidfile next to the keypair
54
+ // so the check is keyed by identity, not by config dir.
55
+ this.pidFile = resolve(this.config.carrier.dataDir, "daemon.pid");
56
+ if (existsSync(this.pidFile)) {
57
+ const oldPid = parseInt(readFileSync(this.pidFile, "utf-8").trim(), 10);
58
+ if (oldPid && oldPid !== process.pid && isProcessAlive(oldPid)) {
59
+ throw new Error(`Another decentlan daemon (pid ${oldPid}) is already running for this identity ` +
60
+ `(${this.pidFile}). Stop it first, or remove the pidfile if you're sure it's stale.`);
61
+ }
62
+ // Stale pidfile (process gone) — clear it.
63
+ }
64
+ try {
65
+ writeFileSync(this.pidFile, String(process.pid), "utf-8");
66
+ }
67
+ catch (err) {
68
+ this.logger.warn(`Could not write pidfile ${this.pidFile}: ${err}`);
69
+ }
70
+ this.logger.info(`Starting daemon (node: ${this.config.node.name})`);
71
+ this.startedAt = Date.now();
72
+ try {
73
+ // 1. Load IPAM
74
+ this.ipam = await Ipam.loadOrCreate(this.config.paths.ipamFile, this.config.node.namespace);
75
+ this.logger.info(`IPAM loaded: ${this.ipam.getPeers().length} peers`);
76
+ // 2. Load DNS resolver
77
+ this.dnsResolver = new DnsResolver(this.ipam, this.config.network.dnsDomain);
78
+ // 3. Load policy + audit
79
+ this.policy = await Policy.loadOrCreate(this.config.paths.policyFile);
80
+ this.auditLog = new AuditLog(this.config.paths.auditLog);
81
+ // 4. ACL engine
82
+ this.aclEngine = new AclEngine({
83
+ policy: this.policy,
84
+ ipam: this.ipam,
85
+ auditLog: this.auditLog,
86
+ });
87
+ // 5. Carrier peer manager — started BEFORE TUN so that an
88
+ // optional dora registration can decide our IP.
89
+ const keyFile = resolve(this.config.carrier.dataDir, "keypair.json");
90
+ this.peerManager = new PeerManager();
91
+ // Daemon does NOT use express nodes. Express is the offline-message
92
+ // store-and-forward relay — appropriate for friend-request delivery
93
+ // (peer might be offline) but not for live packet forwarding (peer
94
+ // must be online; we drop packets via SessionManager.isFriendOnline).
95
+ // Passing empty array prevents the SDK from falling back to express.
96
+ await this.peerManager.create({
97
+ keyFile,
98
+ bootstrapNodes: this.config.carrier.bootstrapNodes,
99
+ expressNodes: [],
100
+ });
101
+ await this.peerManager.start();
102
+ this.logger.info(`Identity: ${this.peerManager.getAddress()}`);
103
+ // Start IPC as soon as the peer is up. The CLI uses it to drive
104
+ // operations that need the daemon's Carrier identity (e.g.
105
+ // friend-request) without spawning a competing Peer instance.
106
+ this.ipcServer = new IpcServer(ipcSocketPath(this.config.carrier.dataDir), {
107
+ friendRequest: async (address, hello) => {
108
+ await this.peerManager.sendFriendRequest(address, hello);
109
+ },
110
+ diag: async () => {
111
+ // Snapshot of everything an operator needs to debug why
112
+ // packets aren't moving: forwarding counters, friend
113
+ // connectivity, IPAM-resolved peers, dora-allocated IP.
114
+ const router = this.packetRouter;
115
+ const stats = router ? router.getStats() : null;
116
+ const peers = this.peerManager?.getFriends() ?? [];
117
+ const ipamPeers = (this.ipam?.getPeers() ?? []).map((p) => ({
118
+ name: p.name,
119
+ virtualIp: p.virtualIp,
120
+ carrierId: p.carrierId,
121
+ }));
122
+ return {
123
+ identity: this.peerManager?.getIdentity(),
124
+ tun: this.tunDevice?.getConfig(),
125
+ allocatedIp: this.doraIntegration?.getAllocatedIp() ?? this.config.network.ip,
126
+ dns: this.dnsServer
127
+ ? { domain: this.dnsServer.getDomain(), port: this.dnsServer.getBoundPort() }
128
+ : null,
129
+ stats,
130
+ friends: peers.map((f) => ({
131
+ pubkey: f.pubkey,
132
+ carrierId: f.carrierId,
133
+ name: f.name,
134
+ status: f.status,
135
+ })),
136
+ ipam: ipamPeers,
137
+ };
138
+ },
139
+ });
140
+ await this.ipcServer.start();
141
+ // 6. Optional dora (DHCP-style) registration. When enabled and the
142
+ // server is reachable, dora hands us a virtual IP and tells us
143
+ // who else is on the network — eliminating the manual ipam.yaml
144
+ // sync between operators. On any failure we fall through to the
145
+ // IP already loaded from config + ipam.yaml.
146
+ let tunIp = this.config.network.ip;
147
+ if (this.config.dora?.enabled && (this.config.dora.userids?.length ?? 0) > 0) {
148
+ // Need to be on the Carrier network before dora can talk to its
149
+ // server. Wait synchronously here — without joinNetwork the
150
+ // first sendText will fail with "no transport".
151
+ await this.peerManager.joinNetwork();
152
+ await this.peerManager.announceSelf(15000).catch((err) => {
153
+ this.logger.warn(`announceSelf during dora bootstrap failed: ${err}`);
154
+ });
155
+ this.doraIntegration = new DoraIntegration({
156
+ config: this.config.dora,
157
+ peerManager: this.peerManager,
158
+ ipam: this.ipam,
159
+ nodeName: this.config.node.name,
160
+ preferredIp: this.config.network.ip,
161
+ });
162
+ const doraIp = await this.doraIntegration.bootstrap();
163
+ if (doraIp && doraIp !== tunIp) {
164
+ this.logger.info(`Dora-allocated IP ${doraIp} (was ${tunIp})`);
165
+ tunIp = doraIp;
166
+ }
167
+ }
168
+ // 7. Setup TUN device + routing using the (possibly dora-allocated) IP.
169
+ this.routeManager = new RouteManager();
170
+ this.tunDevice = new TunDevice({
171
+ config: {
172
+ name: this.config.network.interface,
173
+ ip: tunIp,
174
+ subnet: this.config.network.subnet,
175
+ },
176
+ mockMode: this.useMockTun,
177
+ });
178
+ // In real mode: helper creates the TUN, then we configure IP + route on it.
179
+ // In mock mode: just open the mock device.
180
+ await this.tunDevice.open();
181
+ if (!this.useMockTun) {
182
+ // Use the actual device name from the helper (e.g. utun12 on macOS,
183
+ // agentnet0 on Linux) — different from the configured name.
184
+ const actualName = this.tunDevice.getInterface();
185
+ await this.routeManager.configureTun({
186
+ ...this.tunDevice.getConfig(),
187
+ name: actualName,
188
+ });
189
+ }
190
+ // Live friend-request handling. The daemon stays up and accepts
191
+ // (or just logs) incoming requests — no more `friend-accept --wait`
192
+ // ceremony where the operator has to stop the daemon. Default is
193
+ // auto-accept; the Carrier friend store IS the trust boundary, and
194
+ // requests only reach a peer whose address was deliberately shared.
195
+ const autoAcceptFriends = this.config.friends?.autoAccept ?? true;
196
+ this.peerManager.on("friend-request", (req) => {
197
+ const who = `${req.name || "(unnamed)"} ${req.pubkey.slice(0, 16)}...`;
198
+ const hello = req.hello ? ` hello="${req.hello.slice(0, 60)}"` : "";
199
+ if (autoAcceptFriends) {
200
+ this.logger.info(`Friend request from ${who}${hello} — auto-accepting`);
201
+ this.peerManager?.acceptFriendRequest(req.pubkey).catch((err) => {
202
+ this.logger.warn(`Auto-accept failed for ${who}: ${err}`);
203
+ });
204
+ }
205
+ else {
206
+ this.logger.info(`Friend request from ${who}${hello} — NOT auto-accepting ` +
207
+ `(friends.autoAccept=false; accept manually later)`);
208
+ }
209
+ });
210
+ // NOTE: auto-IPAM (hash-derived virtual IP per userid) was tried and
211
+ // reverted — it produced IPs like 10.86.175.35 that conflicted with
212
+ // peers' own config.network.ip and silently broke the return path
213
+ // (the original peer kept routing for its configured IP, not the
214
+ // hash-derived one). A proper fix needs peers to *announce* their
215
+ // chosen IP over Carrier; until that's built, IPAM stays manual.
216
+ // Network bootstrap runs in the background so we don't block startup.
217
+ // Steps: joinNetwork -> announceSelf -> waitForFriendConnected(...) for
218
+ // each known IPAM peer. The waitForFriendConnected call is what tells
219
+ // the SDK to actively pursue UDP holepunching for that friend.
220
+ //
221
+ // When dora is enabled, joinNetwork + announceSelf already ran
222
+ // synchronously during dora bootstrap; skip the duplicate calls.
223
+ const networkAlreadyJoined = !!this.doraIntegration;
224
+ (async () => {
225
+ try {
226
+ if (!networkAlreadyJoined) {
227
+ await this.peerManager.joinNetwork();
228
+ this.logger.info("Announcing self on DHT");
229
+ await this.peerManager.announceSelf(15000);
230
+ this.logger.info("Self-announce complete");
231
+ }
232
+ // Trigger friend connection for every known peer in IPAM —
233
+ // but skip our own entry (operators ship a single canonical
234
+ // IPAM file to every node, so self appears in the list).
235
+ // Probing yourself loops forever logging "self is OFFLINE".
236
+ const myUserid = this.peerManager.getPubkey();
237
+ const peers = this.ipam.getPeers().filter((p) => p.carrierId !== myUserid);
238
+ if (peers.length > 0) {
239
+ this.logger.info(`Probing ${peers.length} IPAM peer(s) for online status...`);
240
+ for (const p of peers) {
241
+ this.kickSessionForever(p.carrierId, p.name);
242
+ this.probeFriendForever(p.carrierId, p.name, p.virtualIp);
243
+ }
244
+ }
245
+ }
246
+ catch (err) {
247
+ this.logger.warn("Network bootstrap failed:", err);
248
+ }
249
+ })();
250
+ // 7. Packet router
251
+ this.packetRouter = new PacketRouter({
252
+ tunDevice: this.tunDevice,
253
+ peerManager: this.peerManager,
254
+ ipam: this.ipam,
255
+ acl: this.aclEngine,
256
+ });
257
+ await this.packetRouter.start();
258
+ // 7b. In-process DNS server. Answers A/PTR for *.{dnsDomain}
259
+ // (default .decent) from the same IPAM dora populates; forwards
260
+ // everything else to the system upstream.
261
+ //
262
+ // Bind address: 0.0.0.0 (all interfaces). A socket bound to a
263
+ // specific interface IP only sees packets that arrive on that
264
+ // interface; on macOS the kernel routes 10.86.1.11 → lo0 for
265
+ // self-queries, so /etc/resolver/<domain> pointed at the TUN
266
+ // IP would NXDOMAIN locally. Binding on 0.0.0.0 makes the
267
+ // resolver work both for local queries (via 127.0.0.1 / TUN
268
+ // IP from the same host) and for peers on the virtual LAN.
269
+ // Auto-fallback in DnsServer dodges port collisions
270
+ // (mDNSResponder, Bonjour, openclaw-gateway all hold :5353 on
271
+ // 0.0.0.0; we land on 5354 in practice).
272
+ if (!this.useMockTun) {
273
+ this.dnsServer = new DnsServer({
274
+ ipam: this.ipam,
275
+ domain: this.config.network.dnsDomain,
276
+ port: this.config.network.dnsPort,
277
+ bindAddress: "0.0.0.0",
278
+ });
279
+ try {
280
+ await this.dnsServer.start();
281
+ }
282
+ catch (err) {
283
+ this.logger.warn(`DNS server failed to start on 0.0.0.0:${this.config.network.dnsPort}: ${err instanceof Error ? err.message : err}`);
284
+ this.dnsServer = undefined;
285
+ }
286
+ }
287
+ // 8. Optional CONNECT proxy. Only spawned when the operator has
288
+ // explicitly enabled it via `agentnet proxy enable`. Binds to the
289
+ // virtual IP so reach is gated by the existing per-peer ACL — we
290
+ // don't add a second auth layer at the HTTP level.
291
+ if (this.config.proxy?.enabled) {
292
+ if (this.useMockTun) {
293
+ this.logger.warn("Proxy enabled but daemon is in mock-TUN mode; skipping proxy listener");
294
+ }
295
+ else {
296
+ this.connectProxy = new ConnectProxy({
297
+ bindIp: this.config.network.ip,
298
+ port: this.config.proxy.port,
299
+ allowHosts: this.config.proxy.allowHosts ?? [],
300
+ allowConnectPorts: this.config.proxy.allowConnectPorts,
301
+ resolvePeerName: (srcIp) => this.ipam.resolveIp(srcIp)?.name,
302
+ onTunnelOpen: ({ src, srcName, target }) => {
303
+ this.auditLog.logProxyOpen({ srcIp: src, srcName, target });
304
+ },
305
+ onTunnelClose: ({ src, srcName, target, bytesTransferred }) => {
306
+ this.auditLog.logProxyClose({ srcIp: src, srcName, target, bytesTransferred });
307
+ },
308
+ });
309
+ await this.connectProxy.start();
310
+ this.logger.info(`Proxy listening on ${this.config.network.ip}:${this.config.proxy.port}`);
311
+ }
312
+ }
313
+ this.isRunning = true;
314
+ this.logger.info("Daemon started successfully");
315
+ }
316
+ catch (error) {
317
+ this.logger.error("Failed to start daemon:", error);
318
+ await this.cleanup();
319
+ throw error;
320
+ }
321
+ }
322
+ async stop() {
323
+ if (!this.isRunning)
324
+ return;
325
+ this.logger.info("Stopping daemon");
326
+ this.isRunning = false;
327
+ await this.cleanup();
328
+ this.logger.info("Daemon stopped");
329
+ }
330
+ getStatus() {
331
+ return {
332
+ isRunning: this.isRunning,
333
+ uptime: this.isRunning ? Date.now() - this.startedAt : 0,
334
+ version: "0.1.0",
335
+ identity: this.peerManager?.getIdentity(),
336
+ tunInterface: this.tunDevice?.getConfig(),
337
+ peers: this.peerManager?.getFriends() || [],
338
+ activeSessions: this.packetRouter?.getStats().activeSessions || 0,
339
+ };
340
+ }
341
+ /**
342
+ * Periodically poke the SDK to (re-)initiate a session with the friend.
343
+ * This works around the asymmetric friendOnline issue: without this,
344
+ * one side may never spontaneously try to talk to the other and the
345
+ * friend stays perpetually offline.
346
+ */
347
+ async kickSessionForever(carrierId, name) {
348
+ while (this.isRunning) {
349
+ try {
350
+ await this.peerManager.kickSessionEstablishment(carrierId);
351
+ }
352
+ catch (err) {
353
+ this.logger.debug(`kickSessionEstablishment ${name}: ${err}`);
354
+ }
355
+ // 8s cadence — fast enough to retry across cookie/handshake
356
+ // round-trips, slow enough to avoid hammering the SDK
357
+ await new Promise((r) => setTimeout(r, 8000));
358
+ }
359
+ }
360
+ /**
361
+ * Repeatedly call waitForFriendConnected for the given peer until the
362
+ * daemon is stopped. Logs ONLINE/OFFLINE transitions.
363
+ */
364
+ async probeFriendForever(carrierId, name, virtualIp) {
365
+ let lastState = "unknown";
366
+ while (this.isRunning) {
367
+ try {
368
+ const online = await this.peerManager.waitForFriendConnected(carrierId, 60000);
369
+ const state = online ? "online" : "offline";
370
+ if (state !== lastState) {
371
+ if (online) {
372
+ this.logger.info(`Peer ${name} (${virtualIp}) is ONLINE`);
373
+ }
374
+ else {
375
+ this.logger.info(`Peer ${name} (${virtualIp}) is OFFLINE — will keep probing`);
376
+ }
377
+ lastState = state;
378
+ }
379
+ if (!online) {
380
+ // Brief pause before re-probing to avoid tight loop
381
+ await new Promise((r) => setTimeout(r, 5000));
382
+ }
383
+ else {
384
+ // Once online, poll status less aggressively
385
+ await new Promise((r) => setTimeout(r, 30000));
386
+ }
387
+ }
388
+ catch (err) {
389
+ this.logger.debug(`probeFriendForever ${name}: ${err}`);
390
+ await new Promise((r) => setTimeout(r, 5000));
391
+ }
392
+ }
393
+ }
394
+ // Component accessors for CLI
395
+ getPeerManager() {
396
+ return this.peerManager;
397
+ }
398
+ getIpam() {
399
+ return this.ipam;
400
+ }
401
+ getDnsResolver() {
402
+ return this.dnsResolver;
403
+ }
404
+ getAclEngine() {
405
+ return this.aclEngine;
406
+ }
407
+ getAuditLog() {
408
+ return this.auditLog;
409
+ }
410
+ getPacketRouter() {
411
+ return this.packetRouter;
412
+ }
413
+ getConnectProxy() {
414
+ return this.connectProxy;
415
+ }
416
+ getDoraIntegration() {
417
+ return this.doraIntegration;
418
+ }
419
+ async cleanup() {
420
+ try {
421
+ if (this.dnsServer) {
422
+ await this.dnsServer.stop();
423
+ }
424
+ }
425
+ catch (e) {
426
+ this.logger.warn("Error stopping DNS server:", e);
427
+ }
428
+ try {
429
+ if (this.ipcServer) {
430
+ await this.ipcServer.stop();
431
+ }
432
+ }
433
+ catch (e) {
434
+ this.logger.warn("Error stopping IPC server:", e);
435
+ }
436
+ try {
437
+ if (this.doraIntegration) {
438
+ this.doraIntegration.stop();
439
+ }
440
+ }
441
+ catch (e) {
442
+ this.logger.warn("Error stopping dora integration:", e);
443
+ }
444
+ try {
445
+ if (this.connectProxy) {
446
+ await this.connectProxy.stop();
447
+ }
448
+ }
449
+ catch (e) {
450
+ this.logger.warn("Error stopping proxy:", e);
451
+ }
452
+ try {
453
+ if (this.packetRouter) {
454
+ await this.packetRouter.stop();
455
+ }
456
+ }
457
+ catch (e) {
458
+ this.logger.warn("Error stopping router:", e);
459
+ }
460
+ try {
461
+ if (this.peerManager) {
462
+ await this.peerManager.stop();
463
+ }
464
+ }
465
+ catch (e) {
466
+ this.logger.warn("Error stopping peer:", e);
467
+ }
468
+ try {
469
+ if (this.tunDevice) {
470
+ await this.tunDevice.close();
471
+ }
472
+ }
473
+ catch (e) {
474
+ this.logger.warn("Error closing TUN:", e);
475
+ }
476
+ try {
477
+ if (this.routeManager && !this.useMockTun) {
478
+ // Use the configured name on Linux (where we created the device),
479
+ // or the actual utun name on macOS (no-op cleanup since helper exit
480
+ // handles it). Pass the TUN IP so macOS cleanup also removes
481
+ // the lo0 self-alias.
482
+ const ifname = this.tunDevice?.getInterface() || this.config.network.interface;
483
+ const ip = this.doraIntegration?.getAllocatedIp() ?? this.config.network.ip;
484
+ await this.routeManager.cleanup(ifname, this.config.network.subnet, ip);
485
+ }
486
+ }
487
+ catch (e) {
488
+ this.logger.warn("Error cleaning routes:", e);
489
+ }
490
+ try {
491
+ if (this.pidFile && existsSync(this.pidFile)) {
492
+ unlinkSync(this.pidFile);
493
+ }
494
+ }
495
+ catch {
496
+ // best-effort; a stale pidfile will be detected on next start
497
+ }
498
+ }
499
+ }
500
+ function isProcessAlive(pid) {
501
+ try {
502
+ // Signal 0 doesn't actually send anything — it only performs the
503
+ // permission/existence check. Throws ESRCH if the process is gone.
504
+ process.kill(pid, 0);
505
+ return true;
506
+ }
507
+ catch {
508
+ return false;
509
+ }
510
+ }
@@ -0,0 +1 @@
1
+ export { DnsResolver } from "./resolver.js";
@@ -0,0 +1 @@
1
+ export { DnsResolver } from "./resolver.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Local DNS resolver for AgentNet hostnames
3
+ * Resolves names like "partner-openclaw.agentnet" to virtual IPs
4
+ */
5
+ import type { Ipam } from "../ipam/ipam.js";
6
+ import type { IpamRecord } from "../types.js";
7
+ export declare class DnsResolver {
8
+ private ipam;
9
+ private logger;
10
+ private domain;
11
+ constructor(ipam: Ipam, domain?: string);
12
+ /**
13
+ * Resolve hostname to IP
14
+ * Supports:
15
+ * - "partner-openclaw.agentnet" -> "10.86.12.34"
16
+ * - "10.86.12.34.agentnet" -> "10.86.12.34" (reverse lookup)
17
+ * - Carrier ID as hex string
18
+ */
19
+ resolve(hostname: string): string | null;
20
+ /**
21
+ * Resolve IP to IpamRecord
22
+ */
23
+ resolveToRecord(ip: string): IpamRecord | null;
24
+ /**
25
+ * Check if hostname is in our domain
26
+ */
27
+ isInDomain(hostname: string): boolean;
28
+ /**
29
+ * Get all peer entries for listing
30
+ */
31
+ getEntries(): Array<{
32
+ hostname: string;
33
+ ip: string;
34
+ carrierId: string;
35
+ }>;
36
+ /**
37
+ * Format a hostname
38
+ */
39
+ formatHostname(peerName: string): string;
40
+ /**
41
+ * Get domain name
42
+ */
43
+ getDomain(): string;
44
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Local DNS resolver for AgentNet hostnames
3
+ * Resolves names like "partner-openclaw.agentnet" to virtual IPs
4
+ */
5
+ import { Logger } from "../utils/logger.js";
6
+ export class DnsResolver {
7
+ ipam;
8
+ logger;
9
+ domain; // e.g., "agentnet"
10
+ constructor(ipam, domain = "agentnet") {
11
+ this.ipam = ipam;
12
+ this.domain = domain;
13
+ this.logger = new Logger({ prefix: "DnsResolver" });
14
+ }
15
+ /**
16
+ * Resolve hostname to IP
17
+ * Supports:
18
+ * - "partner-openclaw.agentnet" -> "10.86.12.34"
19
+ * - "10.86.12.34.agentnet" -> "10.86.12.34" (reverse lookup)
20
+ * - Carrier ID as hex string
21
+ */
22
+ resolve(hostname) {
23
+ // Remove .agentnet suffix if present
24
+ const name = hostname.replace(new RegExp(`\\.${this.domain}$`), "");
25
+ // Try to resolve by peer name
26
+ const ipByName = this.ipam.resolveName(name);
27
+ if (ipByName) {
28
+ this.logger.debug(`Resolved ${hostname} -> ${ipByName}`);
29
+ return ipByName;
30
+ }
31
+ // Try to resolve by IP directly (reverse lookup)
32
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
33
+ const record = this.ipam.resolveIp(name);
34
+ if (record) {
35
+ this.logger.debug(`Resolved IP ${name} -> ${record.virtualIp}`);
36
+ return record.virtualIp;
37
+ }
38
+ }
39
+ // Try to resolve by Carrier ID
40
+ const record = this.ipam.resolveCarrierId(name);
41
+ if (record) {
42
+ this.logger.debug(`Resolved Carrier ID ${name} -> ${record.virtualIp}`);
43
+ return record.virtualIp;
44
+ }
45
+ this.logger.debug(`Failed to resolve: ${hostname}`);
46
+ return null;
47
+ }
48
+ /**
49
+ * Resolve IP to IpamRecord
50
+ */
51
+ resolveToRecord(ip) {
52
+ return this.ipam.resolveIp(ip);
53
+ }
54
+ /**
55
+ * Check if hostname is in our domain
56
+ */
57
+ isInDomain(hostname) {
58
+ return hostname.endsWith(`.${this.domain}`) || !hostname.includes(".");
59
+ }
60
+ /**
61
+ * Get all peer entries for listing
62
+ */
63
+ getEntries() {
64
+ return this.ipam.getPeers().map((peer) => ({
65
+ hostname: `${peer.name}.${this.domain}`,
66
+ ip: peer.virtualIp,
67
+ carrierId: peer.carrierId,
68
+ }));
69
+ }
70
+ /**
71
+ * Format a hostname
72
+ */
73
+ formatHostname(peerName) {
74
+ return `${peerName}.${this.domain}`;
75
+ }
76
+ /**
77
+ * Get domain name
78
+ */
79
+ getDomain() {
80
+ return this.domain;
81
+ }
82
+ }