@decentnetwork/lan 0.1.27 → 0.1.29
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/cli/commands.d.ts +9 -0
- package/dist/cli/commands.js +19 -0
- package/dist/cli/index.js +9 -1
- package/dist/daemon/ipc.d.ts +9 -1
- package/dist/daemon/ipc.js +2 -0
- package/dist/daemon/server.js +80 -3
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -296,3 +296,12 @@ export declare function cmdServiceInstall(args: {
|
|
|
296
296
|
export declare function cmdServiceStatus(_args: {
|
|
297
297
|
configDir?: string;
|
|
298
298
|
}): Promise<void>;
|
|
299
|
+
/**
|
|
300
|
+
* Ask the running daemon to re-exec itself. Used after
|
|
301
|
+
* `npm install -g @decentnetwork/lan@<new>` so the daemon picks up
|
|
302
|
+
* the new code without a sudo prompt — the daemon already has the
|
|
303
|
+
* privileges it needs to rebind the TUN.
|
|
304
|
+
*/
|
|
305
|
+
export declare function cmdRestart(args: {
|
|
306
|
+
configDir?: string;
|
|
307
|
+
}): Promise<void>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -1453,3 +1453,22 @@ export async function cmdServiceStatus(_args) {
|
|
|
1453
1453
|
}
|
|
1454
1454
|
console.log(`'service status' isn't wired up for ${process.platform}.`);
|
|
1455
1455
|
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Ask the running daemon to re-exec itself. Used after
|
|
1458
|
+
* `npm install -g @decentnetwork/lan@<new>` so the daemon picks up
|
|
1459
|
+
* the new code without a sudo prompt — the daemon already has the
|
|
1460
|
+
* privileges it needs to rebind the TUN.
|
|
1461
|
+
*/
|
|
1462
|
+
export async function cmdRestart(args) {
|
|
1463
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
1464
|
+
const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
|
|
1465
|
+
if (daemonPid(config) === null) {
|
|
1466
|
+
throw new Error("Daemon not running. Start it once with 'agentnet up' (needs sudo for TUN); after that 'agentnet restart' takes care of upgrades without further sudo prompts.");
|
|
1467
|
+
}
|
|
1468
|
+
const res = await ipcCall(config, { op: "self-restart" });
|
|
1469
|
+
if (!res.ok)
|
|
1470
|
+
throw new Error(`Daemon refused: ${res.error}`);
|
|
1471
|
+
const data = res.data;
|
|
1472
|
+
console.log(`Daemon scheduled for self-restart. New process will exec: ${(data.argv || []).join(" ")}`);
|
|
1473
|
+
console.log("Allow ~5s for the daemon to come back up. Verify with 'agentnet diag'.");
|
|
1474
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hideBin } from "yargs/helpers";
|
|
|
10
10
|
// Belt-and-braces — also raise it here in case the CLI is run directly
|
|
11
11
|
// (e.g. `node dist/cli/index.js` rather than via dist/index.js).
|
|
12
12
|
EventEmitter.defaultMaxListeners = 100;
|
|
13
|
-
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdServiceStatus, } from "./commands.js";
|
|
13
|
+
import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, } from "./commands.js";
|
|
14
14
|
async function main() {
|
|
15
15
|
await yargs(hideBin(process.argv))
|
|
16
16
|
.scriptName("agentnet")
|
|
@@ -118,6 +118,14 @@ async function main() {
|
|
|
118
118
|
})
|
|
119
119
|
.demandCommand(1, "Specify a service subcommand (run 'agentnet service --help')"), () => {
|
|
120
120
|
// parent handler — never invoked because demandCommand above
|
|
121
|
+
})
|
|
122
|
+
// Tell the running daemon to re-exec itself with its original argv.
|
|
123
|
+
// The daemon inherits its own uid (root if it was launched as root)
|
|
124
|
+
// so the relaunch needs NO sudo prompt — useful after
|
|
125
|
+
// `npm install -g @decentnetwork/lan@<new>` to pick up new code
|
|
126
|
+
// without breaking the operator's flow.
|
|
127
|
+
.command("restart", "Tell the running daemon to re-exec itself (no sudo prompt; daemon must be up)", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
|
|
128
|
+
await cmdRestart({ configDir: argv["config-dir"] });
|
|
121
129
|
})
|
|
122
130
|
.command("up", "Start the daemon", (y) => y
|
|
123
131
|
.option("name", { type: "string" })
|
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -45,9 +45,17 @@ export interface IpcHandlers {
|
|
|
45
45
|
* restart (which drops every Carrier session). Returns the applied
|
|
46
46
|
* allowlist for the CLI to echo. */
|
|
47
47
|
proxyReload: () => Promise<Record<string, unknown>>;
|
|
48
|
+
/** Re-exec the running daemon with its original argv. The new
|
|
49
|
+
* process inherits the current process's privileges (so a
|
|
50
|
+
* root-owned daemon re-execs as root with no sudo prompt),
|
|
51
|
+
* picks up freshly-installed code on disk, and rebinds the
|
|
52
|
+
* TUN. Used for the upgrade flow: `npm install` the new
|
|
53
|
+
* decentlan, then call this to roll forward without an
|
|
54
|
+
* operator-side sudo step. */
|
|
55
|
+
selfRestart: () => Promise<Record<string, unknown>>;
|
|
48
56
|
}
|
|
49
57
|
export interface IpcRequest {
|
|
50
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "proxy-reload";
|
|
58
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "proxy-reload" | "self-restart";
|
|
51
59
|
address?: string;
|
|
52
60
|
hello?: string;
|
|
53
61
|
userid?: string;
|
package/dist/daemon/ipc.js
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -18,6 +18,18 @@ import { DnsServer } from "../dns/server.js";
|
|
|
18
18
|
import { IpcServer, ipcSocketPath } from "./ipc.js";
|
|
19
19
|
import { PendingFriendsStore } from "./pending-friends.js";
|
|
20
20
|
import { Logger } from "../utils/logger.js";
|
|
21
|
+
/**
|
|
22
|
+
* Quote argv safely for `sh -c`. Wraps single-quotes by closing,
|
|
23
|
+
* escaping the quote, and reopening — covers paths with spaces and
|
|
24
|
+
* the rare argv that contains a literal '. Used by the self-restart
|
|
25
|
+
* relauncher so the spawned `sh -c '... exec X Y Z'` shell receives
|
|
26
|
+
* argv intact even if a path has weird characters.
|
|
27
|
+
*/
|
|
28
|
+
function shellQuote(parts) {
|
|
29
|
+
return parts
|
|
30
|
+
.map((p) => "'" + p.replace(/'/g, `'\\''`) + "'")
|
|
31
|
+
.join(" ");
|
|
32
|
+
}
|
|
21
33
|
import { ConfigLoader } from "../config/loader.js";
|
|
22
34
|
export class DaemonServer {
|
|
23
35
|
config;
|
|
@@ -175,6 +187,47 @@ export class DaemonServer {
|
|
|
175
187
|
}
|
|
176
188
|
return { applied: true, allowHosts };
|
|
177
189
|
},
|
|
190
|
+
selfRestart: async () => {
|
|
191
|
+
// Detach a child process that waits briefly and then re-execs
|
|
192
|
+
// the same argv we were started with. Inherits privileges
|
|
193
|
+
// (root daemon → root child, no sudo prompt). The current
|
|
194
|
+
// process exits cleanly after responding to the IPC call so
|
|
195
|
+
// the child can rebind the TUN. The brief sleep gives the
|
|
196
|
+
// current process time to flush its IPC response, close
|
|
197
|
+
// sockets, and release the pidfile/TUN before the new
|
|
198
|
+
// process tries to claim them.
|
|
199
|
+
const { spawn } = await import("child_process");
|
|
200
|
+
const argv = process.argv.slice();
|
|
201
|
+
const node = argv.shift();
|
|
202
|
+
this.logger.info(`Self-restart requested via IPC — relaunching ${node} ${argv.join(" ")}`);
|
|
203
|
+
// Schedule shutdown for after the IPC response goes out. The
|
|
204
|
+
// 50ms timeout is plenty for the response to leave; the
|
|
205
|
+
// child's own 1s sleep below is the real arbiter.
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
try {
|
|
208
|
+
// sh -c so we can shell-quote argv safely and use sleep.
|
|
209
|
+
// The trailing `&` puts the relauncher into the
|
|
210
|
+
// background of sh — sh then exits — and Node's detached
|
|
211
|
+
// flag reparents the relauncher to PID 1. Result: a
|
|
212
|
+
// fully detached new daemon that inherits our uid/gid.
|
|
213
|
+
const cmd = `sleep 1 && exec ${shellQuote([node, ...argv])}`;
|
|
214
|
+
const child = spawn("sh", ["-c", cmd], {
|
|
215
|
+
detached: true,
|
|
216
|
+
stdio: "ignore",
|
|
217
|
+
env: process.env,
|
|
218
|
+
});
|
|
219
|
+
child.unref();
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
this.logger.error(`Self-restart spawn failed: ${err instanceof Error ? err.message : err}`);
|
|
223
|
+
}
|
|
224
|
+
// Give the child its 1s head-start, then exit so the
|
|
225
|
+
// pidfile and TUN are released before the relauncher tries
|
|
226
|
+
// to grab them.
|
|
227
|
+
setTimeout(() => process.exit(0), 200);
|
|
228
|
+
}, 50);
|
|
229
|
+
return { scheduledMs: 250, argv: [node, ...argv] };
|
|
230
|
+
},
|
|
178
231
|
diag: async () => {
|
|
179
232
|
// Snapshot of everything an operator needs to debug why
|
|
180
233
|
// packets aren't moving: forwarding counters, friend
|
|
@@ -218,9 +271,29 @@ export class DaemonServer {
|
|
|
218
271
|
// 6. Optional dora (DHCP-style) registration. When enabled and the
|
|
219
272
|
// server is reachable, dora hands us a virtual IP and tells us
|
|
220
273
|
// who else is on the network — eliminating the manual ipam.yaml
|
|
221
|
-
// sync between operators.
|
|
222
|
-
//
|
|
274
|
+
// sync between operators.
|
|
275
|
+
//
|
|
276
|
+
// Fallback policy when dora is unreachable: DON'T use
|
|
277
|
+
// config.network.ip blindly. `agentnet init` defaults every
|
|
278
|
+
// fresh install to 10.86.1.10, so two new peers that can't
|
|
279
|
+
// reach dora will BOTH claim the same fallback and silently
|
|
280
|
+
// collide — symptom seen in the wild as packets going to the
|
|
281
|
+
// wrong daemon and 100% loss to legitimate peers. Use the
|
|
282
|
+
// deterministic-from-userid IP instead (sha256(userid) → last
|
|
283
|
+
// two octets of 10.86.X.Y). That guarantees every peer gets a
|
|
284
|
+
// unique IP keyed off identity, with no shared default to
|
|
285
|
+
// collide on. If dora comes up later, the
|
|
286
|
+
// onAllocatedIpChanged callback swaps the TUN to dora's value.
|
|
223
287
|
let tunIp = this.config.network.ip;
|
|
288
|
+
const ownUserid = this.peerManager.getPubkey();
|
|
289
|
+
const deterministicFallback = Ipam.deterministicIpForUserid(ownUserid);
|
|
290
|
+
if (tunIp === "10.86.1.10" || !tunIp) {
|
|
291
|
+
// The init-default fallback IP — every fresh decentlan installs with
|
|
292
|
+
// this. Override with a per-identity deterministic value before
|
|
293
|
+
// dora even gets to try.
|
|
294
|
+
tunIp = deterministicFallback;
|
|
295
|
+
this.logger.info(`Using deterministic fallback IP ${tunIp} (derived from userid; avoids the 10.86.1.10 init-default collision)`);
|
|
296
|
+
}
|
|
224
297
|
if (this.config.dora?.enabled && (this.config.dora.userids?.length ?? 0) > 0) {
|
|
225
298
|
// Need to be on the Carrier network before dora can talk to its
|
|
226
299
|
// server. Wait synchronously here — without joinNetwork the
|
|
@@ -234,7 +307,11 @@ export class DaemonServer {
|
|
|
234
307
|
peerManager: this.peerManager,
|
|
235
308
|
ipam: this.ipam,
|
|
236
309
|
nodeName: this.config.node.name,
|
|
237
|
-
|
|
310
|
+
// Hand dora our deterministic fallback as the requestedIp so
|
|
311
|
+
// a successful register returns the same IP across restarts
|
|
312
|
+
// (avoids the dora-stole-someone-else's-IP race that bit us
|
|
313
|
+
// when ubuntu was momentarily offline during reallocation).
|
|
314
|
+
preferredIp: tunIp,
|
|
238
315
|
// Fires when dora's background retry eventually succeeds
|
|
239
316
|
// AFTER the initial bootstrap already returned the fallback
|
|
240
317
|
// IP. At that point the TUN is up on the fallback (e.g.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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",
|