@decentnetwork/lan 0.1.134 → 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.
- 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/daemon/server.d.ts +8 -0
- package/dist/daemon/server.js +47 -0
- package/dist/proxy/multi-exit-router.js +27 -1
- package/dist/tun/tun-device.js +23 -2
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/daemon/server.js
CHANGED
|
@@ -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.
|
|
@@ -83,6 +83,10 @@ const HEALTH_TIMEOUT_MS = 4000;
|
|
|
83
83
|
// of transient jitter on a long-haul path (don't blackout a region on one blip).
|
|
84
84
|
const DOWN_AFTER_FAILS = 3;
|
|
85
85
|
const CONNECT_TIMEOUT_MS = 8000;
|
|
86
|
+
// An exit that carried a real tunnel within this window is treated as healthy
|
|
87
|
+
// regardless of what the synthetic probe says (the probe adds load and flaps on
|
|
88
|
+
// a busy/jittery exit; live traffic is the truth).
|
|
89
|
+
const RECENT_SUCCESS_MS = 10_000;
|
|
86
90
|
/**
|
|
87
91
|
* Start the multi-exit router. Returns a stop() that closes the listener.
|
|
88
92
|
* Runs until stopped (the CLI command keeps the process alive).
|
|
@@ -108,6 +112,7 @@ export function startMultiExitRouter(opts) {
|
|
|
108
112
|
active: 0,
|
|
109
113
|
served: 0,
|
|
110
114
|
lastRtt: null,
|
|
115
|
+
lastSuccessMs: null,
|
|
111
116
|
}));
|
|
112
117
|
// Per-host routing decision, logged once on first sight so the user can SEE
|
|
113
118
|
// why a hostname went DIRECT vs through a region (e.g. a CCTV video CDN that
|
|
@@ -170,6 +175,13 @@ export function startMultiExitRouter(opts) {
|
|
|
170
175
|
}
|
|
171
176
|
}
|
|
172
177
|
else {
|
|
178
|
+
// A probe miss does NOT mark an exit down while real tunnels are still
|
|
179
|
+
// succeeding through it — live traffic overrides the synthetic probe.
|
|
180
|
+
const liveTraffic = exit.lastSuccessMs !== null && Date.now() - exit.lastSuccessMs < RECENT_SUCCESS_MS;
|
|
181
|
+
if (liveTraffic) {
|
|
182
|
+
exit.fails = 0;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
173
185
|
exit.fails++;
|
|
174
186
|
exit.oks = 0;
|
|
175
187
|
if (exit.healthy && exit.fails >= DOWN_AFTER_FAILS) {
|
|
@@ -184,8 +196,12 @@ export function startMultiExitRouter(opts) {
|
|
|
184
196
|
}
|
|
185
197
|
// ---- exit selection (scoped to one region) --------------------------------
|
|
186
198
|
function pickOrder(region) {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
// An exit is usable if the probe says healthy OR it carried a real tunnel
|
|
201
|
+
// recently (live traffic beats a flapping probe).
|
|
202
|
+
const isUp = (e) => e.healthy || (e.lastSuccessMs !== null && now - e.lastSuccessMs < RECENT_SUCCESS_MS);
|
|
187
203
|
const inRegion = exits.filter((e) => e.region === region);
|
|
188
|
-
const healthy = inRegion.filter(
|
|
204
|
+
const healthy = inRegion.filter(isUp);
|
|
189
205
|
// Best-effort fallback: if NO exit currently passes health checks but the
|
|
190
206
|
// region HAS exits, try them anyway instead of 503-ing the user. A single
|
|
191
207
|
// high-latency exit under load makes the probe flap DOWN/UP every few
|
|
@@ -230,6 +246,16 @@ export function startMultiExitRouter(opts) {
|
|
|
230
246
|
established = true;
|
|
231
247
|
exit.active++;
|
|
232
248
|
exit.served++;
|
|
249
|
+
// Live traffic proves the exit is up — clear any probe-driven failure
|
|
250
|
+
// and mark it healthy so a flapping probe can't take it DOWN while
|
|
251
|
+
// tunnels are flowing.
|
|
252
|
+
exit.lastSuccessMs = Date.now();
|
|
253
|
+
exit.fails = 0;
|
|
254
|
+
if (!exit.healthy) {
|
|
255
|
+
exit.healthy = true;
|
|
256
|
+
log(`✓ ${exit.name} [${exit.region}] UP (live traffic)`);
|
|
257
|
+
printPool();
|
|
258
|
+
}
|
|
233
259
|
up.setTimeout(0);
|
|
234
260
|
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
235
261
|
if (head && head.length)
|
package/dist/tun/tun-device.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|