@decentnetwork/lan 0.1.135 → 0.1.136

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
@@ -46,6 +46,8 @@ export declare class DaemonServer {
46
46
  private readonly activeSends;
47
47
  private startedAt;
48
48
  private isRunning;
49
+ private tunRebuildInProgress;
50
+ private lastTunRebuildMs;
49
51
  private statusTimer?;
50
52
  private pidFile?;
51
53
  private configDir;
@@ -59,6 +61,12 @@ export declare class DaemonServer {
59
61
  * del races the kernel (EBUSY) and the respawn leaves the daemon TUN-less.
60
62
  */
61
63
  private reconfigureTunTo;
64
+ /** Watch a TUN device for an unexpected `closed` (the helper died — e.g. on
65
+ * WSL after "write tun: invalid argument") and rebuild it, so the data plane
66
+ * self-heals instead of staying down until a manual restart. Backoff-guarded
67
+ * so a helper that keeps dying immediately doesn't tight-loop. */
68
+ private attachTunRecovery;
69
+ private rebuildTun;
62
70
  start(): Promise<void>;
63
71
  stop(): Promise<void>;
64
72
  /**
@@ -82,6 +82,8 @@ export class DaemonServer {
82
82
  activeSends = new Map();
83
83
  startedAt = 0;
84
84
  isRunning = false;
85
+ tunRebuildInProgress = false;
86
+ lastTunRebuildMs = 0;
85
87
  statusTimer;
86
88
  pidFile;
87
89
  configDir;
@@ -130,12 +132,56 @@ export class DaemonServer {
130
132
  // Re-attach the packet-router's TUN listener — it was bound to the old
131
133
  // TunDevice instance.
132
134
  this.packetRouter?.swapTunDevice(this.tunDevice);
135
+ this.attachTunRecovery(this.tunDevice);
133
136
  this.logger.info(`TUN now at ${newIp}`);
134
137
  }
135
138
  catch (err) {
136
139
  this.logger.error(`Failed to reconfigure TUN to ${newIp}: ${err instanceof Error ? err.message : err}`);
137
140
  }
138
141
  }
142
+ /** Watch a TUN device for an unexpected `closed` (the helper died — e.g. on
143
+ * WSL after "write tun: invalid argument") and rebuild it, so the data plane
144
+ * self-heals instead of staying down until a manual restart. Backoff-guarded
145
+ * so a helper that keeps dying immediately doesn't tight-loop. */
146
+ attachTunRecovery(device) {
147
+ if (this.useMockTun)
148
+ return;
149
+ device.once("closed", () => {
150
+ if (!this.isRunning || this.useMockTun)
151
+ return;
152
+ const delay = Date.now() - this.lastTunRebuildMs < 10_000 ? 5_000 : 1_000;
153
+ this.logger.warn(`TUN closed unexpectedly — rebuilding data plane in ${delay}ms`);
154
+ setTimeout(() => void this.rebuildTun(), delay);
155
+ });
156
+ }
157
+ async rebuildTun() {
158
+ const rm = this.routeManager;
159
+ if (!this.isRunning || this.useMockTun || this.tunRebuildInProgress || !this.tunDevice || !rm)
160
+ return;
161
+ this.tunRebuildInProgress = true;
162
+ this.lastTunRebuildMs = Date.now();
163
+ try {
164
+ const ip = this.tunDevice.getConfig().ip;
165
+ const dev = new TunDevice({
166
+ config: { name: this.config.network.interface, ip, subnet: this.config.network.subnet },
167
+ mockMode: false,
168
+ });
169
+ await dev.open();
170
+ await rm.configureTun({ ...dev.getConfig(), name: dev.getInterface() });
171
+ this.tunDevice = dev;
172
+ this.packetRouter?.swapTunDevice(dev);
173
+ this.attachTunRecovery(dev);
174
+ this.logger.info(`TUN rebuilt at ${ip} (data plane recovered)`);
175
+ }
176
+ catch (err) {
177
+ this.logger.error(`TUN rebuild failed: ${err instanceof Error ? err.message : err} — retrying in 5s`);
178
+ this.tunRebuildInProgress = false;
179
+ if (this.isRunning)
180
+ setTimeout(() => void this.rebuildTun(), 5_000);
181
+ return;
182
+ }
183
+ this.tunRebuildInProgress = false;
184
+ }
139
185
  async start() {
140
186
  if (this.isRunning) {
141
187
  throw new Error("Daemon already running");
@@ -540,6 +586,7 @@ export class DaemonServer {
540
586
  // In real mode: helper creates the TUN, then we configure IP + route on it.
541
587
  // In mock mode: just open the mock device.
542
588
  await this.tunDevice.open();
589
+ this.attachTunRecovery(this.tunDevice);
543
590
  if (!this.useMockTun) {
544
591
  // Use the actual device name from the helper (e.g. utun12 on macOS,
545
592
  // agentnet0 on Linux) — different from the configured name.
@@ -97,8 +97,11 @@ export class TunDevice extends EventEmitter {
97
97
  });
98
98
  return;
99
99
  }
100
- // Real mode: framed write to helper stdin
101
- if (!this.helper || !this.helper.stdin) {
100
+ // Real mode: framed write to helper stdin. Bail out cleanly if the pipe is
101
+ // gone or no longer writable — throwing here is caught by the router's
102
+ // try/catch (→ recordDrop "tun-write-failed"), whereas writing into a dead
103
+ // pipe would emit an async EPIPE 'error' event that crashes the daemon.
104
+ if (!this.helper || !this.helper.stdin || this.helper.stdin.destroyed || !this.helper.stdin.writable) {
102
105
  throw new Error("Helper process not available");
103
106
  }
104
107
  const lenBuf = Buffer.alloc(4);
@@ -204,6 +207,24 @@ export class TunDevice extends EventEmitter {
204
207
  this.emit("closed");
205
208
  }
206
209
  });
210
+ // Error handlers on the process AND its pipes. Without these, an EPIPE when
211
+ // the helper dies mid-write (observed on WSL: repeated "write tun: invalid
212
+ // argument" → helper exits → the next TUN write hits a broken stdin) is
213
+ // emitted as an UNHANDLED 'error' event on the stdin socket and crashes the
214
+ // entire daemon — taking the TUN and every session down. Catch them: log,
215
+ // and mark the device closed so the router stops writing (drops with
216
+ // "tun-down") and the reopen path can rebuild the TUN instead of crashing.
217
+ const onStreamError = (where) => (err) => {
218
+ this.logger.warn(`TUN helper ${where} error: ${err.message}`);
219
+ if (this.isOpen) {
220
+ this.isOpen = false;
221
+ this.emit("closed");
222
+ }
223
+ };
224
+ this.helper.on("error", onStreamError("process"));
225
+ this.helper.stdin.on("error", onStreamError("stdin"));
226
+ this.helper.stdout.on("error", onStreamError("stdout"));
227
+ this.helper.stderr.on("error", onStreamError("stderr"));
207
228
  this.isOpen = true;
208
229
  this.emit("ready");
209
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.135",
3
+ "version": "0.1.136",
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",