@decentnetwork/lan 0.1.81 → 0.1.83

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.
Binary file
Binary file
Binary file
Binary file
@@ -42,6 +42,15 @@ export declare class DaemonServer {
42
42
  private pidFile?;
43
43
  private configDir;
44
44
  constructor(opts: DaemonOptions);
45
+ /**
46
+ * Swap the live TUN to a new IP without restarting the daemon. Called when
47
+ * dora's background registration allocates an IP different from the
48
+ * deterministic fallback the TUN started on. Idempotent (no-op if already at
49
+ * newIp). ORDER MATTERS: close the TunDevice FIRST so the helper subprocess
50
+ * exits and releases the device — otherwise routeManager.cleanup's tuntap
51
+ * del races the kernel (EBUSY) and the respawn leaves the daemon TUN-less.
52
+ */
53
+ private reconfigureTunTo;
45
54
  start(): Promise<void>;
46
55
  stop(): Promise<void>;
47
56
  /**
@@ -66,6 +66,49 @@ export class DaemonServer {
66
66
  this.configDir = opts.configDir ?? ConfigLoader.defaultConfigDir();
67
67
  this.logger = new Logger({ prefix: "Daemon" });
68
68
  }
69
+ /**
70
+ * Swap the live TUN to a new IP without restarting the daemon. Called when
71
+ * dora's background registration allocates an IP different from the
72
+ * deterministic fallback the TUN started on. Idempotent (no-op if already at
73
+ * newIp). ORDER MATTERS: close the TunDevice FIRST so the helper subprocess
74
+ * exits and releases the device — otherwise routeManager.cleanup's tuntap
75
+ * del races the kernel (EBUSY) and the respawn leaves the daemon TUN-less.
76
+ */
77
+ async reconfigureTunTo(newIp) {
78
+ if (!this.tunDevice || !this.routeManager)
79
+ return;
80
+ const currentIp = this.tunDevice.getConfig().ip;
81
+ if (currentIp === newIp)
82
+ return;
83
+ this.logger.info(`Reconfiguring TUN: allocated IP changed ${currentIp} -> ${newIp}`);
84
+ const ifname = this.tunDevice.getInterface();
85
+ try {
86
+ await this.tunDevice.close();
87
+ await this.routeManager.cleanup(ifname, this.config.network.subnet, currentIp);
88
+ this.tunDevice = new TunDevice({
89
+ config: {
90
+ name: this.config.network.interface,
91
+ ip: newIp,
92
+ subnet: this.config.network.subnet,
93
+ },
94
+ mockMode: this.useMockTun,
95
+ });
96
+ await this.tunDevice.open();
97
+ if (!this.useMockTun) {
98
+ await this.routeManager.configureTun({
99
+ ...this.tunDevice.getConfig(),
100
+ name: this.tunDevice.getInterface(),
101
+ });
102
+ }
103
+ // Re-attach the packet-router's TUN listener — it was bound to the old
104
+ // TunDevice instance.
105
+ this.packetRouter?.swapTunDevice(this.tunDevice);
106
+ this.logger.info(`TUN now at ${newIp}`);
107
+ }
108
+ catch (err) {
109
+ this.logger.error(`Failed to reconfigure TUN to ${newIp}: ${err instanceof Error ? err.message : err}`);
110
+ }
111
+ }
69
112
  async start() {
70
113
  if (this.isRunning) {
71
114
  throw new Error("Daemon already running");
@@ -340,56 +383,20 @@ export class DaemonServer {
340
383
  // at 10.86.1.15 — other peers route to .15, our kernel
341
384
  // doesn't recognize it as local, packets drop. Rebuild
342
385
  // the TUN at the new IP so traffic actually reaches us.
343
- onAllocatedIpChanged: async (newIp) => {
344
- if (!this.tunDevice || !this.routeManager)
345
- return;
346
- const currentIp = this.tunDevice.getConfig().ip;
347
- if (currentIp === newIp)
348
- return;
349
- this.logger.info(`Reconfiguring TUN: dora-allocated IP changed ${currentIp} -> ${newIp}`);
350
- const ifname = this.tunDevice.getInterface();
351
- try {
352
- // ORDER MATTERS: close TunDevice FIRST so the helper
353
- // subprocess fully exits and releases agentnet0.
354
- // routeManager.cleanup's `ip tuntap del` would otherwise
355
- // fail with EBUSY and the subsequent helper spawn would
356
- // race the kernel for the device name, exit code=1, and
357
- // leave the daemon TUN-less. tunDevice.close() now waits
358
- // for actual process exit.
359
- await this.tunDevice.close();
360
- await this.routeManager.cleanup(ifname, this.config.network.subnet, currentIp);
361
- this.tunDevice = new TunDevice({
362
- config: {
363
- name: this.config.network.interface,
364
- ip: newIp,
365
- subnet: this.config.network.subnet,
366
- },
367
- mockMode: this.useMockTun,
368
- });
369
- await this.tunDevice.open();
370
- if (!this.useMockTun) {
371
- await this.routeManager.configureTun({
372
- ...this.tunDevice.getConfig(),
373
- name: this.tunDevice.getInterface(),
374
- });
375
- }
376
- // Re-attach the packet-router's TUN listener — the
377
- // listener was bound to the old TunDevice instance.
378
- this.packetRouter?.swapTunDevice(this.tunDevice);
379
- this.logger.info(`TUN now at ${newIp}`);
380
- }
381
- catch (err) {
382
- this.logger.error(`Failed to reconfigure TUN to ${newIp}: ${err instanceof Error ? err.message : err}`);
383
- }
384
- },
386
+ onAllocatedIpChanged: (newIp) => this.reconfigureTunTo(newIp),
385
387
  });
386
- const doraIp = await this.doraIntegration.bootstrap();
387
- if (doraIp && doraIp !== tunIp) {
388
- this.logger.info(`Dora-allocated IP ${doraIp} (was ${tunIp})`);
389
- tunIp = doraIp;
390
- }
388
+ // Do NOT block the data plane on dora. The TUN opens immediately on the
389
+ // deterministic fallback IP (step 7 below); dora registration runs in
390
+ // the background (started after the TUN is up) and swaps the TUN to its
391
+ // allocated IP via reconfigureTunTo when/if it answers. Previously this
392
+ // awaited up to a 30s dora-reachability timeout BEFORE opening the TUN,
393
+ // so a node on a slow path (the exact China→abroad case we exist for)
394
+ // had no data plane for half a minute on every start.
391
395
  }
392
- // 7. Setup TUN device + routing using the (possibly dora-allocated) IP.
396
+ // 7. Setup TUN device + routing immediately on the deterministic
397
+ // fallback IP — the data plane must NOT wait on dora. If dora later
398
+ // hands us a different IP, the backgrounded bootstrap below swaps the
399
+ // TUN via reconfigureTunTo.
393
400
  this.routeManager = new RouteManager();
394
401
  this.tunDevice = new TunDevice({
395
402
  config: {
@@ -411,6 +418,21 @@ export class DaemonServer {
411
418
  name: actualName,
412
419
  });
413
420
  }
421
+ // Data plane is now LIVE on the fallback IP. Kick dora registration in
422
+ // the background — it never blocks startup. When it answers (initial
423
+ // register or a later retry), reconfigureTunTo swaps the TUN to dora's
424
+ // allocated IP. A slow/unreachable dora just means we keep the
425
+ // deterministic fallback, which is already routable.
426
+ if (this.doraIntegration) {
427
+ const fallbackIp = tunIp;
428
+ void this.doraIntegration
429
+ .bootstrap()
430
+ .then(async (doraIp) => {
431
+ if (doraIp && doraIp !== fallbackIp)
432
+ await this.reconfigureTunTo(doraIp);
433
+ })
434
+ .catch((err) => this.logger.warn(`dora bootstrap (background): ${err instanceof Error ? err.message : err}`));
435
+ }
414
436
  // Live friend-request handling. The daemon stays up and accepts
415
437
  // (or queues) every incoming request — no `friend-accept --wait`
416
438
  // ceremony where the operator has to stop the daemon. Default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
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.35",
80
+ "@decentnetwork/peer": "^0.1.37",
81
81
  "js-yaml": "^4.1.0",
82
82
  "yargs": "^17.7.2"
83
83
  },