@decentnetwork/lan 0.1.18 → 0.1.22
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 +31 -0
- package/dist/cli/commands.js +223 -3
- package/dist/cli/index.js +22 -1
- package/dist/config/loader.d.ts +13 -0
- package/dist/config/loader.js +22 -5
- package/dist/daemon/ipc.d.ts +7 -1
- package/dist/daemon/ipc.js +2 -0
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +62 -15
- package/dist/proxy/connect-proxy.d.ts +11 -0
- package/dist/proxy/connect-proxy.js +17 -0
- package/docs/INSTALL.md +123 -10
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -265,3 +265,34 @@ export declare function cmdDoraAutofriend(args: {
|
|
|
265
265
|
values?: string[];
|
|
266
266
|
configDir?: string;
|
|
267
267
|
}): Promise<void>;
|
|
268
|
+
/**
|
|
269
|
+
* Install the daemon as a persistent system service.
|
|
270
|
+
*
|
|
271
|
+
* Linux → writes /etc/systemd/system/agentnet.service, daemon-reload,
|
|
272
|
+
* enable+start. Sets ExecStart to the resolved `agentnet`
|
|
273
|
+
* binary and threads --config-dir so sudo's $HOME=/root
|
|
274
|
+
* doesn't pick up an empty /root/.agentnet.
|
|
275
|
+
* macOS → writes /Library/LaunchDaemons/com.decentlan.agentnet.plist,
|
|
276
|
+
* launchctl load (RunAtLoad + KeepAlive).
|
|
277
|
+
*
|
|
278
|
+
* Both surfaces stream stdout/stderr to /var/log/agentnet.log so a
|
|
279
|
+
* single `tail -f /var/log/agentnet.log` works regardless of platform.
|
|
280
|
+
* `--uninstall` reverses each respectively.
|
|
281
|
+
*
|
|
282
|
+
* Requires root (writes to /etc or /Library). On Linux the resolved
|
|
283
|
+
* unit will run as User=root; on macOS the LaunchDaemon already runs
|
|
284
|
+
* as root. The CLI complains loudly with the missing-sudo hint if
|
|
285
|
+
* the writes fail with EACCES.
|
|
286
|
+
*/
|
|
287
|
+
export declare function cmdServiceInstall(args: {
|
|
288
|
+
uninstall?: boolean;
|
|
289
|
+
configDir?: string;
|
|
290
|
+
}): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Show whether the service is installed + running. Cross-platform
|
|
293
|
+
* pass-through to systemctl status / launchctl list with a one-line
|
|
294
|
+
* up/down summary up top.
|
|
295
|
+
*/
|
|
296
|
+
export declare function cmdServiceStatus(_args: {
|
|
297
|
+
configDir?: string;
|
|
298
|
+
}): Promise<void>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { resolve, dirname } from "path";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
6
6
|
import { createConnection } from "net";
|
|
7
|
-
import { ConfigLoader } from "../config/loader.js";
|
|
7
|
+
import { ConfigLoader, DEFAULT_DORA_USERID, DEFAULT_DORA_ADDRESS } from "../config/loader.js";
|
|
8
8
|
import { ipcSocketPath } from "../daemon/ipc.js";
|
|
9
9
|
import { DaemonServer } from "../daemon/server.js";
|
|
10
10
|
import { Ipam } from "../ipam/ipam.js";
|
|
@@ -129,7 +129,31 @@ export async function cmdInit(args) {
|
|
|
129
129
|
console.log(` Config dir: ${dir}`);
|
|
130
130
|
console.log(` Subnet: ${config.network.subnet}`);
|
|
131
131
|
console.log(` Interface: ${config.network.interface}`);
|
|
132
|
-
|
|
132
|
+
// Send the one-time friend-request to the default dora so the daemon
|
|
133
|
+
// can register on first start. Best-effort: a friend-request requires
|
|
134
|
+
// joinNetwork + an announce round, ~10-30s; we cap the wait and
|
|
135
|
+
// surface a hint if it doesn't go through. Skipped when the user
|
|
136
|
+
// overrode dora.userids away from the default (i.e. they're pointing
|
|
137
|
+
// at a private dora — they'll run `agentnet dora enable` themselves).
|
|
138
|
+
const defaultDoraConfigured = (config.dora?.userids ?? []).includes(DEFAULT_DORA_USERID);
|
|
139
|
+
if (defaultDoraConfigured) {
|
|
140
|
+
console.log(`\nFriending the public dora (${DEFAULT_DORA_USERID.slice(0, 16)}...) so the daemon can join the shared network on first start.`);
|
|
141
|
+
try {
|
|
142
|
+
await cmdFriendRequest({
|
|
143
|
+
address: DEFAULT_DORA_ADDRESS,
|
|
144
|
+
hello: `decentlan init (${nodeName})`,
|
|
145
|
+
waitMs: 8000,
|
|
146
|
+
configDir: dir,
|
|
147
|
+
});
|
|
148
|
+
console.log(` Friend-request dispatched. Auto-accept on the dora side will take it from here.`);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
console.warn(` Friend-request to the default dora failed: ${msg}`);
|
|
153
|
+
console.warn(` Re-run later: agentnet dora enable --address ${DEFAULT_DORA_ADDRESS}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log(`\nNext: sudo agentnet service install # or 'agentnet up --real-tun' to run in foreground`);
|
|
133
157
|
}
|
|
134
158
|
/**
|
|
135
159
|
* Show identity information.
|
|
@@ -787,7 +811,36 @@ export async function cmdProxyAllowHost(args) {
|
|
|
787
811
|
config.proxy = { ...proxy, allowHosts: [...allowHosts] };
|
|
788
812
|
await ConfigLoader.save(config, configPath);
|
|
789
813
|
console.log(`Added '${args.host}' to proxy allow-hosts.`);
|
|
790
|
-
|
|
814
|
+
await applyProxyReloadIfRunning(config);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* If the daemon is up, push the freshly-saved proxy allowlist into the
|
|
818
|
+
* running proxy over IPC — no restart, no Carrier-session churn. Prints
|
|
819
|
+
* the outcome. When the daemon is down (or the proxy isn't running yet),
|
|
820
|
+
* tells the operator a restart is needed.
|
|
821
|
+
*/
|
|
822
|
+
async function applyProxyReloadIfRunning(config) {
|
|
823
|
+
if (daemonPid(config) === null) {
|
|
824
|
+
console.log("Daemon is down — change takes effect on next 'agentnet up'.");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
const res = await ipcCall(config, { op: "proxy-reload" });
|
|
829
|
+
if (!res.ok) {
|
|
830
|
+
console.log(`(live reload failed: ${res.error}) — restart the daemon to apply.`);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const data = res.data;
|
|
834
|
+
if (data.applied) {
|
|
835
|
+
console.log(`Applied live to the running proxy — no restart needed.`);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
console.log(`Not applied live: ${data.reason ?? "proxy not running"}.`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
console.log(`(live reload error: ${err instanceof Error ? err.message : err}) — restart to apply.`);
|
|
843
|
+
}
|
|
791
844
|
}
|
|
792
845
|
/**
|
|
793
846
|
* Remove a host glob from the proxy allowlist.
|
|
@@ -803,6 +856,7 @@ export async function cmdProxyRevokeHost(args) {
|
|
|
803
856
|
await ConfigLoader.save(config, configPath);
|
|
804
857
|
if (filtered.length < before) {
|
|
805
858
|
console.log(`Removed '${args.host}' from proxy allow-hosts.`);
|
|
859
|
+
await applyProxyReloadIfRunning(config);
|
|
806
860
|
}
|
|
807
861
|
else {
|
|
808
862
|
console.log(`No exact match for '${args.host}' in allow-hosts.`);
|
|
@@ -1208,3 +1262,169 @@ export async function cmdDoraAutofriend(args) {
|
|
|
1208
1262
|
}
|
|
1209
1263
|
console.log(`Takes effect on the next roster refresh (~60s) or daemon restart.`);
|
|
1210
1264
|
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Install the daemon as a persistent system service.
|
|
1267
|
+
*
|
|
1268
|
+
* Linux → writes /etc/systemd/system/agentnet.service, daemon-reload,
|
|
1269
|
+
* enable+start. Sets ExecStart to the resolved `agentnet`
|
|
1270
|
+
* binary and threads --config-dir so sudo's $HOME=/root
|
|
1271
|
+
* doesn't pick up an empty /root/.agentnet.
|
|
1272
|
+
* macOS → writes /Library/LaunchDaemons/com.decentlan.agentnet.plist,
|
|
1273
|
+
* launchctl load (RunAtLoad + KeepAlive).
|
|
1274
|
+
*
|
|
1275
|
+
* Both surfaces stream stdout/stderr to /var/log/agentnet.log so a
|
|
1276
|
+
* single `tail -f /var/log/agentnet.log` works regardless of platform.
|
|
1277
|
+
* `--uninstall` reverses each respectively.
|
|
1278
|
+
*
|
|
1279
|
+
* Requires root (writes to /etc or /Library). On Linux the resolved
|
|
1280
|
+
* unit will run as User=root; on macOS the LaunchDaemon already runs
|
|
1281
|
+
* as root. The CLI complains loudly with the missing-sudo hint if
|
|
1282
|
+
* the writes fail with EACCES.
|
|
1283
|
+
*/
|
|
1284
|
+
export async function cmdServiceInstall(args) {
|
|
1285
|
+
const dir = args.configDir || ConfigLoader.defaultConfigDir();
|
|
1286
|
+
const { spawnSync, execSync } = await import("child_process");
|
|
1287
|
+
if (process.platform === "linux") {
|
|
1288
|
+
const unitPath = "/etc/systemd/system/agentnet.service";
|
|
1289
|
+
if (args.uninstall) {
|
|
1290
|
+
spawnSync("systemctl", ["disable", "--now", "agentnet"], { stdio: "inherit" });
|
|
1291
|
+
try {
|
|
1292
|
+
const { unlinkSync, existsSync } = await import("fs");
|
|
1293
|
+
if (existsSync(unitPath))
|
|
1294
|
+
unlinkSync(unitPath);
|
|
1295
|
+
execSync("systemctl daemon-reload");
|
|
1296
|
+
console.log(`Removed ${unitPath} and disabled the service.`);
|
|
1297
|
+
}
|
|
1298
|
+
catch (err) {
|
|
1299
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1300
|
+
throw new Error(`Could not remove ${unitPath} (need root?): ${msg}`);
|
|
1301
|
+
}
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
// Resolve the `agentnet` binary. argv[1] is dist/cli/index.js when
|
|
1305
|
+
// invoked as `node …/index.js`, so a `which` lookup is more
|
|
1306
|
+
// portable. Fall back to /usr/local/bin/agentnet (npm-default
|
|
1307
|
+
// global prefix on most Linux setups) if the lookup fails.
|
|
1308
|
+
let agentnetBin;
|
|
1309
|
+
try {
|
|
1310
|
+
agentnetBin = execSync("command -v agentnet", { encoding: "utf-8" }).trim();
|
|
1311
|
+
if (!agentnetBin)
|
|
1312
|
+
throw new Error("not found");
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
agentnetBin = "/usr/local/bin/agentnet";
|
|
1316
|
+
}
|
|
1317
|
+
const unit = `[Unit]
|
|
1318
|
+
Description=Decent AgentNet daemon
|
|
1319
|
+
After=network-online.target
|
|
1320
|
+
Wants=network-online.target
|
|
1321
|
+
|
|
1322
|
+
[Service]
|
|
1323
|
+
Type=simple
|
|
1324
|
+
User=root
|
|
1325
|
+
ExecStart=${agentnetBin} up --real-tun --config-dir ${dir}
|
|
1326
|
+
Restart=on-failure
|
|
1327
|
+
RestartSec=5
|
|
1328
|
+
StandardOutput=append:/var/log/agentnet.log
|
|
1329
|
+
StandardError=append:/var/log/agentnet.log
|
|
1330
|
+
|
|
1331
|
+
[Install]
|
|
1332
|
+
WantedBy=multi-user.target
|
|
1333
|
+
`;
|
|
1334
|
+
try {
|
|
1335
|
+
const fs = await import("fs/promises");
|
|
1336
|
+
await fs.writeFile(unitPath, unit, "utf-8");
|
|
1337
|
+
}
|
|
1338
|
+
catch (err) {
|
|
1339
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1340
|
+
throw new Error(`Could not write ${unitPath} (need root? try 'sudo'): ${msg}`);
|
|
1341
|
+
}
|
|
1342
|
+
execSync("systemctl daemon-reload");
|
|
1343
|
+
execSync("systemctl enable --now agentnet");
|
|
1344
|
+
console.log(`Installed ${unitPath} and started agentnet.service.`);
|
|
1345
|
+
console.log(`Logs: journalctl -u agentnet -f (or /var/log/agentnet.log)`);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (process.platform === "darwin") {
|
|
1349
|
+
const plistPath = "/Library/LaunchDaemons/com.decentlan.agentnet.plist";
|
|
1350
|
+
if (args.uninstall) {
|
|
1351
|
+
spawnSync("launchctl", ["unload", plistPath], { stdio: "inherit" });
|
|
1352
|
+
try {
|
|
1353
|
+
const { unlinkSync, existsSync } = await import("fs");
|
|
1354
|
+
if (existsSync(plistPath))
|
|
1355
|
+
unlinkSync(plistPath);
|
|
1356
|
+
console.log(`Unloaded and removed ${plistPath}.`);
|
|
1357
|
+
}
|
|
1358
|
+
catch (err) {
|
|
1359
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1360
|
+
throw new Error(`Could not remove ${plistPath} (need root?): ${msg}`);
|
|
1361
|
+
}
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
let agentnetBin;
|
|
1365
|
+
try {
|
|
1366
|
+
agentnetBin = execSync("command -v agentnet", { encoding: "utf-8" }).trim();
|
|
1367
|
+
if (!agentnetBin)
|
|
1368
|
+
throw new Error("not found");
|
|
1369
|
+
}
|
|
1370
|
+
catch {
|
|
1371
|
+
agentnetBin = "/usr/local/bin/agentnet";
|
|
1372
|
+
}
|
|
1373
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1374
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1375
|
+
<plist version="1.0"><dict>
|
|
1376
|
+
<key>Label</key><string>com.decentlan.agentnet</string>
|
|
1377
|
+
<key>UserName</key><string>root</string>
|
|
1378
|
+
<key>ProgramArguments</key><array>
|
|
1379
|
+
<string>${agentnetBin}</string>
|
|
1380
|
+
<string>up</string>
|
|
1381
|
+
<string>--real-tun</string>
|
|
1382
|
+
<string>--config-dir</string>
|
|
1383
|
+
<string>${dir}</string>
|
|
1384
|
+
</array>
|
|
1385
|
+
<key>RunAtLoad</key><true/>
|
|
1386
|
+
<key>KeepAlive</key><true/>
|
|
1387
|
+
<key>StandardOutPath</key><string>/var/log/agentnet.log</string>
|
|
1388
|
+
<key>StandardErrorPath</key><string>/var/log/agentnet.log</string>
|
|
1389
|
+
</dict></plist>
|
|
1390
|
+
`;
|
|
1391
|
+
try {
|
|
1392
|
+
const fs = await import("fs/promises");
|
|
1393
|
+
await fs.writeFile(plistPath, plist, "utf-8");
|
|
1394
|
+
}
|
|
1395
|
+
catch (err) {
|
|
1396
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1397
|
+
throw new Error(`Could not write ${plistPath} (need root? try 'sudo'): ${msg}`);
|
|
1398
|
+
}
|
|
1399
|
+
execSync(`launchctl load ${plistPath}`);
|
|
1400
|
+
console.log(`Installed ${plistPath} and started com.decentlan.agentnet.`);
|
|
1401
|
+
console.log(`Logs: tail -f /var/log/agentnet.log`);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
throw new Error(`'service install' isn't wired up for ${process.platform}. Run 'agentnet up --real-tun' in the foreground for now.`);
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Show whether the service is installed + running. Cross-platform
|
|
1408
|
+
* pass-through to systemctl status / launchctl list with a one-line
|
|
1409
|
+
* up/down summary up top.
|
|
1410
|
+
*/
|
|
1411
|
+
export async function cmdServiceStatus(_args) {
|
|
1412
|
+
const { spawnSync } = await import("child_process");
|
|
1413
|
+
if (process.platform === "linux") {
|
|
1414
|
+
const r = spawnSync("systemctl", ["status", "agentnet", "--no-pager"], { encoding: "utf-8" });
|
|
1415
|
+
process.stdout.write(r.stdout || "");
|
|
1416
|
+
if (r.stderr)
|
|
1417
|
+
process.stderr.write(r.stderr);
|
|
1418
|
+
process.exit(r.status ?? 0);
|
|
1419
|
+
}
|
|
1420
|
+
if (process.platform === "darwin") {
|
|
1421
|
+
const r = spawnSync("launchctl", ["list", "com.decentlan.agentnet"], { encoding: "utf-8" });
|
|
1422
|
+
if ((r.status ?? 0) !== 0) {
|
|
1423
|
+
console.log("Not loaded. Install with 'sudo agentnet service install'.");
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
process.stdout.write(r.stdout || "");
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
console.log(`'service status' isn't wired up for ${process.platform}.`);
|
|
1430
|
+
}
|
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, } 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, cmdServiceStatus, } from "./commands.js";
|
|
14
14
|
async function main() {
|
|
15
15
|
await yargs(hideBin(process.argv))
|
|
16
16
|
.scriptName("agentnet")
|
|
@@ -97,6 +97,27 @@ async function main() {
|
|
|
97
97
|
})
|
|
98
98
|
.demandCommand(1, "Specify a dns subcommand (run 'agentnet dns --help')"), () => {
|
|
99
99
|
// parent handler — never invoked because demandCommand
|
|
100
|
+
})
|
|
101
|
+
// Persistent system service — wraps systemctl (Linux) and
|
|
102
|
+
// launchctl (macOS) so a new operator runs ONE command to install
|
|
103
|
+
// + start + persist instead of hand-writing a unit/plist.
|
|
104
|
+
.command("service", "Install/uninstall/inspect the daemon as a system service (systemd on Linux, launchd on macOS)", (y) => y
|
|
105
|
+
.command("install", "Write the unit/plist, enable + start (needs sudo)", (yy) => yy
|
|
106
|
+
.option("uninstall", { type: "boolean", default: false, describe: "Reverse the install" })
|
|
107
|
+
.option("config-dir", { type: "string" }), async (argv) => {
|
|
108
|
+
await cmdServiceInstall({
|
|
109
|
+
uninstall: argv.uninstall,
|
|
110
|
+
configDir: argv["config-dir"],
|
|
111
|
+
});
|
|
112
|
+
})
|
|
113
|
+
.command("uninstall", "Stop and remove the system service (alias for 'service install --uninstall')", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
114
|
+
await cmdServiceInstall({ uninstall: true, configDir: argv["config-dir"] });
|
|
115
|
+
})
|
|
116
|
+
.command("status", "Show service status (systemctl status / launchctl list)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
|
|
117
|
+
await cmdServiceStatus({ configDir: argv["config-dir"] });
|
|
118
|
+
})
|
|
119
|
+
.demandCommand(1, "Specify a service subcommand (run 'agentnet service --help')"), () => {
|
|
120
|
+
// parent handler — never invoked because demandCommand above
|
|
100
121
|
})
|
|
101
122
|
.command("up", "Start the daemon", (y) => y
|
|
102
123
|
.option("name", { type: "string" })
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { DecentAgentNetConfig } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Public dora server baked into `agentnet init` so an operator can join
|
|
4
|
+
* the canonical Decent AgentNet without first hunting down a registry
|
|
5
|
+
* address. Override by editing `dora.userids` (and re-running
|
|
6
|
+
* `agentnet dora enable --address <yours>`) — or by running a private
|
|
7
|
+
* dora and pointing your own peers at it.
|
|
8
|
+
*
|
|
9
|
+
* Both fields are required: the daemon needs `userid` to talk to the
|
|
10
|
+
* server over Carrier, and `address` so `agentnet init` can send the
|
|
11
|
+
* one-time friend-request that establishes the Carrier session.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_DORA_USERID = "98rsHv17h8G6AP9RagyrBiT1kmw4cn8MFPEembS6ZVjv";
|
|
14
|
+
export declare const DEFAULT_DORA_ADDRESS = "Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd";
|
|
2
15
|
export declare class ConfigLoader {
|
|
3
16
|
static defaultConfigPath(): string;
|
|
4
17
|
static defaultConfigDir(): string;
|
package/dist/config/loader.js
CHANGED
|
@@ -23,6 +23,19 @@ const DEFAULT_BOOTSTRAP_NODES = [
|
|
|
23
23
|
const DEFAULT_EXPRESS_NODES = [
|
|
24
24
|
{ host: "lens.beagle.chat", port: 443, pk: "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95" },
|
|
25
25
|
];
|
|
26
|
+
/**
|
|
27
|
+
* Public dora server baked into `agentnet init` so an operator can join
|
|
28
|
+
* the canonical Decent AgentNet without first hunting down a registry
|
|
29
|
+
* address. Override by editing `dora.userids` (and re-running
|
|
30
|
+
* `agentnet dora enable --address <yours>`) — or by running a private
|
|
31
|
+
* dora and pointing your own peers at it.
|
|
32
|
+
*
|
|
33
|
+
* Both fields are required: the daemon needs `userid` to talk to the
|
|
34
|
+
* server over Carrier, and `address` so `agentnet init` can send the
|
|
35
|
+
* one-time friend-request that establishes the Carrier session.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_DORA_USERID = "98rsHv17h8G6AP9RagyrBiT1kmw4cn8MFPEembS6ZVjv";
|
|
38
|
+
export const DEFAULT_DORA_ADDRESS = "Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd";
|
|
26
39
|
export class ConfigLoader {
|
|
27
40
|
static defaultConfigPath() {
|
|
28
41
|
return DEFAULT_CONFIG_FILE;
|
|
@@ -101,12 +114,16 @@ export class ConfigLoader {
|
|
|
101
114
|
friends: {
|
|
102
115
|
autoAccept: true,
|
|
103
116
|
},
|
|
104
|
-
// Dora integration is
|
|
105
|
-
//
|
|
106
|
-
//
|
|
117
|
+
// Dora integration is ON by default and points at the public
|
|
118
|
+
// canonical dora — `agentnet init` follows up with a one-time
|
|
119
|
+
// friend-request to its address, so a fresh install joins the
|
|
120
|
+
// shared network with zero additional commands. To run in
|
|
121
|
+
// private (no dora) mode: `agentnet dora disable`. To point at
|
|
122
|
+
// your own dora: `agentnet dora enable --address <addr>` (it
|
|
123
|
+
// replaces the default).
|
|
107
124
|
dora: {
|
|
108
|
-
enabled:
|
|
109
|
-
userids: [],
|
|
125
|
+
enabled: true,
|
|
126
|
+
userids: [DEFAULT_DORA_USERID],
|
|
110
127
|
refreshIntervalMs: 60_000,
|
|
111
128
|
// Default: auto-friend every peer in the dora roster. Dora
|
|
112
129
|
// membership IS the trust statement — joining a dora means
|
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -39,9 +39,15 @@ export interface IpcHandlers {
|
|
|
39
39
|
/** Reject (drop) a queued friend-request by userid. Doesn't
|
|
40
40
|
* notify the sender — they'll just see no acceptance. */
|
|
41
41
|
friendsReject: (userid: string) => Promise<void>;
|
|
42
|
+
/** Re-read proxy allowlist from config and apply it to the running
|
|
43
|
+
* proxy WITHOUT restarting the daemon. Lets `agentnet proxy
|
|
44
|
+
* allow-host` take effect instantly instead of forcing a daemon
|
|
45
|
+
* restart (which drops every Carrier session). Returns the applied
|
|
46
|
+
* allowlist for the CLI to echo. */
|
|
47
|
+
proxyReload: () => Promise<Record<string, unknown>>;
|
|
42
48
|
}
|
|
43
49
|
export interface IpcRequest {
|
|
44
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject";
|
|
50
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "proxy-reload";
|
|
45
51
|
address?: string;
|
|
46
52
|
hello?: string;
|
|
47
53
|
userid?: string;
|
package/dist/daemon/ipc.js
CHANGED
package/dist/daemon/server.d.ts
CHANGED
package/dist/daemon/server.js
CHANGED
|
@@ -18,6 +18,7 @@ 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
|
+
import { ConfigLoader } from "../config/loader.js";
|
|
21
22
|
export class DaemonServer {
|
|
22
23
|
config;
|
|
23
24
|
useMockTun;
|
|
@@ -40,9 +41,13 @@ export class DaemonServer {
|
|
|
40
41
|
startedAt = 0;
|
|
41
42
|
isRunning = false;
|
|
42
43
|
pidFile;
|
|
44
|
+
configDir;
|
|
43
45
|
constructor(opts) {
|
|
44
46
|
this.config = opts.config;
|
|
45
47
|
this.useMockTun = opts.useMockTun ?? true; // Default to mock; override for production
|
|
48
|
+
// Remember where config.yaml lives so live-reload ops (e.g.
|
|
49
|
+
// proxy-reload) can re-read it without a daemon restart.
|
|
50
|
+
this.configDir = opts.configDir ?? ConfigLoader.defaultConfigDir();
|
|
46
51
|
this.logger = new Logger({ prefix: "Daemon" });
|
|
47
52
|
}
|
|
48
53
|
async start() {
|
|
@@ -145,6 +150,31 @@ export class DaemonServer {
|
|
|
145
150
|
throw new Error(`No pending friend-request for userid ${userid}`);
|
|
146
151
|
// No-op at the Carrier level — sender just sees no acceptance.
|
|
147
152
|
},
|
|
153
|
+
proxyReload: async () => {
|
|
154
|
+
// Re-read the proxy allowlist from disk and push it into the
|
|
155
|
+
// running proxy without a daemon restart (which would drop
|
|
156
|
+
// every Carrier session). If the proxy isn't running yet,
|
|
157
|
+
// tell the caller so it can fall back to "restart needed".
|
|
158
|
+
const fresh = await ConfigLoader.load(resolve(this.configDir, "config.yaml"));
|
|
159
|
+
const allowHosts = fresh.proxy?.allowHosts ?? [];
|
|
160
|
+
const allowConnectPorts = fresh.proxy?.allowConnectPorts;
|
|
161
|
+
if (!this.connectProxy) {
|
|
162
|
+
return {
|
|
163
|
+
applied: false,
|
|
164
|
+
reason: fresh.proxy?.enabled
|
|
165
|
+
? "proxy enabled in config but not running — restart the daemon once to start the listener"
|
|
166
|
+
: "proxy is disabled — run 'agentnet proxy enable' then restart once",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
this.connectProxy.updateAllowlist(allowHosts, allowConnectPorts);
|
|
170
|
+
// Keep the in-memory config in sync so a later diag reflects it.
|
|
171
|
+
if (this.config.proxy) {
|
|
172
|
+
this.config.proxy.allowHosts = allowHosts;
|
|
173
|
+
if (allowConnectPorts)
|
|
174
|
+
this.config.proxy.allowConnectPorts = allowConnectPorts;
|
|
175
|
+
}
|
|
176
|
+
return { applied: true, allowHosts };
|
|
177
|
+
},
|
|
148
178
|
diag: async () => {
|
|
149
179
|
// Snapshot of everything an operator needs to debug why
|
|
150
180
|
// packets aren't moving: forwarding counters, friend
|
|
@@ -413,26 +443,43 @@ export class DaemonServer {
|
|
|
413
443
|
// explicitly enabled it via `agentnet proxy enable`. Binds to the
|
|
414
444
|
// virtual IP so reach is gated by the existing per-peer ACL — we
|
|
415
445
|
// don't add a second auth layer at the HTTP level.
|
|
446
|
+
//
|
|
447
|
+
// Bind to `tunIp` (the dora-ALLOCATED IP), NOT config.network.ip.
|
|
448
|
+
// They diverge when dora hands out an address different from the
|
|
449
|
+
// init default: the listener would try to bind config's
|
|
450
|
+
// 10.86.1.10 while the TUN is actually at 10.86.1.15, and
|
|
451
|
+
// Node throws EADDRNOTAVAIL because that address isn't local.
|
|
452
|
+
//
|
|
453
|
+
// Also: a proxy bind failure must NOT take down the whole daemon.
|
|
454
|
+
// Packet forwarding, DNS, dora, friends — all work fine without
|
|
455
|
+
// the proxy. Catch + log so an EADDRNOTAVAIL (or port-in-use)
|
|
456
|
+
// degrades gracefully to "no proxy" instead of a crash loop.
|
|
416
457
|
if (this.config.proxy?.enabled) {
|
|
417
458
|
if (this.useMockTun) {
|
|
418
459
|
this.logger.warn("Proxy enabled but daemon is in mock-TUN mode; skipping proxy listener");
|
|
419
460
|
}
|
|
420
461
|
else {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
462
|
+
try {
|
|
463
|
+
this.connectProxy = new ConnectProxy({
|
|
464
|
+
bindIp: tunIp,
|
|
465
|
+
port: this.config.proxy.port,
|
|
466
|
+
allowHosts: this.config.proxy.allowHosts ?? [],
|
|
467
|
+
allowConnectPorts: this.config.proxy.allowConnectPorts,
|
|
468
|
+
resolvePeerName: (srcIp) => this.ipam.resolveIp(srcIp)?.name,
|
|
469
|
+
onTunnelOpen: ({ src, srcName, target }) => {
|
|
470
|
+
this.auditLog.logProxyOpen({ srcIp: src, srcName, target });
|
|
471
|
+
},
|
|
472
|
+
onTunnelClose: ({ src, srcName, target, bytesTransferred }) => {
|
|
473
|
+
this.auditLog.logProxyClose({ srcIp: src, srcName, target, bytesTransferred });
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
await this.connectProxy.start();
|
|
477
|
+
this.logger.info(`Proxy listening on ${tunIp}:${this.config.proxy.port}`);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
this.connectProxy = undefined;
|
|
481
|
+
this.logger.error(`Proxy failed to start on ${tunIp}:${this.config.proxy.port} — continuing without it: ${err instanceof Error ? err.message : err}`);
|
|
482
|
+
}
|
|
436
483
|
}
|
|
437
484
|
}
|
|
438
485
|
this.isRunning = true;
|
|
@@ -65,6 +65,17 @@ export declare class ConnectProxy {
|
|
|
65
65
|
start(): Promise<void>;
|
|
66
66
|
stop(): Promise<void>;
|
|
67
67
|
getStats(): ConnectProxyStats;
|
|
68
|
+
/**
|
|
69
|
+
* Update the host allowlist (and optionally the port allowlist) of a
|
|
70
|
+
* RUNNING proxy without restarting it. handleConnect reads
|
|
71
|
+
* `this.opts.allowHosts` / `allowConnectPorts` at request time, so a
|
|
72
|
+
* plain field swap takes effect on the next CONNECT — no need to tear
|
|
73
|
+
* down the listener or, more importantly, the daemon's Carrier
|
|
74
|
+
* sessions. Lets `agentnet proxy allow-host` apply instantly instead
|
|
75
|
+
* of forcing a daemon restart that would drop every peer session and
|
|
76
|
+
* trigger a slow reconvergence storm.
|
|
77
|
+
*/
|
|
78
|
+
updateAllowlist(allowHosts: string[], allowConnectPorts?: number[]): void;
|
|
68
79
|
private handleConnect;
|
|
69
80
|
private refuse;
|
|
70
81
|
}
|
|
@@ -86,6 +86,23 @@ export class ConnectProxy {
|
|
|
86
86
|
getStats() {
|
|
87
87
|
return { ...this.stats };
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Update the host allowlist (and optionally the port allowlist) of a
|
|
91
|
+
* RUNNING proxy without restarting it. handleConnect reads
|
|
92
|
+
* `this.opts.allowHosts` / `allowConnectPorts` at request time, so a
|
|
93
|
+
* plain field swap takes effect on the next CONNECT — no need to tear
|
|
94
|
+
* down the listener or, more importantly, the daemon's Carrier
|
|
95
|
+
* sessions. Lets `agentnet proxy allow-host` apply instantly instead
|
|
96
|
+
* of forcing a daemon restart that would drop every peer session and
|
|
97
|
+
* trigger a slow reconvergence storm.
|
|
98
|
+
*/
|
|
99
|
+
updateAllowlist(allowHosts, allowConnectPorts) {
|
|
100
|
+
this.opts.allowHosts = allowHosts;
|
|
101
|
+
if (allowConnectPorts)
|
|
102
|
+
this.opts.allowConnectPorts = allowConnectPorts;
|
|
103
|
+
this.logger.info(`Allowlist updated live: ${allowHosts.length} host glob(s)` +
|
|
104
|
+
(allowConnectPorts ? `, ports ${allowConnectPorts.join(",")}` : ""));
|
|
105
|
+
}
|
|
89
106
|
handleConnect(req, clientSocket, head) {
|
|
90
107
|
const src = clientSocket.remoteAddress ?? "?";
|
|
91
108
|
const srcName = this.opts.resolvePeerName?.(src);
|
package/docs/INSTALL.md
CHANGED
|
@@ -102,33 +102,146 @@ ping <peer-name>.decent # if you ran 'dns install'
|
|
|
102
102
|
|
|
103
103
|
## Running the daemon as a service
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
foreground binary; rough sketches:
|
|
105
|
+
The daemon is a foreground binary. Pick one of:
|
|
107
106
|
|
|
108
|
-
###
|
|
107
|
+
### Option A — `nohup` (fastest; no auto-restart)
|
|
109
108
|
|
|
110
|
-
```
|
|
111
|
-
#
|
|
109
|
+
```bash
|
|
110
|
+
# pre-create the log so the unprivileged user can tail it
|
|
111
|
+
sudo touch /var/log/agentnet.log
|
|
112
|
+
sudo chmod 666 /var/log/agentnet.log
|
|
113
|
+
|
|
114
|
+
# IMPORTANT: wrap the whole pipeline in `sudo sh -c '...'`.
|
|
115
|
+
# Otherwise `>>` is interpreted by your unprivileged shell and the
|
|
116
|
+
# redirect fails with "Permission denied" before sudo elevates.
|
|
117
|
+
sudo sh -c 'nohup agentnet up --real-tun --config-dir /home/YOURUSER/.agentnet >> /var/log/agentnet.log 2>&1 &'
|
|
118
|
+
|
|
119
|
+
# verify
|
|
120
|
+
ps -ef | grep -E 'agentnet up|tun-helper' | grep -v grep
|
|
121
|
+
tail -f /var/log/agentnet.log
|
|
122
|
+
|
|
123
|
+
# stop
|
|
124
|
+
sudo pkill -f 'agentnet up'; sudo pkill -f tun-helper
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Always pass `--config-dir /home/YOURUSER/.agentnet` when launching with
|
|
128
|
+
`sudo` — `sudo` changes `$HOME` to `/root`, so without an explicit
|
|
129
|
+
`--config-dir` the daemon (and any one-off CLI invocations) will look
|
|
130
|
+
at `/root/.agentnet/` and report "Not initialized."
|
|
131
|
+
|
|
132
|
+
### Option B — systemd (Linux, persistent + auto-restart)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
sudo tee /etc/systemd/system/agentnet.service <<'EOF'
|
|
112
136
|
[Unit]
|
|
113
137
|
Description=Decent AgentNet daemon
|
|
114
138
|
After=network-online.target
|
|
115
139
|
Wants=network-online.target
|
|
116
140
|
|
|
117
141
|
[Service]
|
|
118
|
-
|
|
142
|
+
Type=simple
|
|
119
143
|
User=root
|
|
144
|
+
ExecStart=/usr/local/bin/agentnet up --real-tun --config-dir /home/YOURUSER/.agentnet
|
|
120
145
|
Restart=on-failure
|
|
121
146
|
RestartSec=5
|
|
147
|
+
StandardOutput=append:/var/log/agentnet.log
|
|
148
|
+
StandardError=append:/var/log/agentnet.log
|
|
122
149
|
|
|
123
150
|
[Install]
|
|
124
151
|
WantedBy=multi-user.target
|
|
152
|
+
EOF
|
|
153
|
+
|
|
154
|
+
sudo systemctl daemon-reload
|
|
155
|
+
sudo systemctl enable --now agentnet
|
|
156
|
+
sudo systemctl status agentnet
|
|
157
|
+
journalctl -u agentnet -f # live logs (alternative to /var/log/agentnet.log)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Persists across reboots. If you ever previously launched via
|
|
161
|
+
`systemd-run --unit=agentnet ...` (transient unit), the unit file
|
|
162
|
+
above replaces it cleanly.
|
|
163
|
+
|
|
164
|
+
### Option C — launchd (macOS, persistent + auto-restart)
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
sudo tee /Library/LaunchDaemons/com.decentlan.agentnet.plist <<EOF
|
|
168
|
+
<?xml version="1.0"?>
|
|
169
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
170
|
+
<plist version="1.0"><dict>
|
|
171
|
+
<key>Label</key><string>com.decentlan.agentnet</string>
|
|
172
|
+
<key>UserName</key><string>root</string>
|
|
173
|
+
<key>ProgramArguments</key><array>
|
|
174
|
+
<string>/usr/local/bin/agentnet</string>
|
|
175
|
+
<string>up</string>
|
|
176
|
+
<string>--real-tun</string>
|
|
177
|
+
<string>--config-dir</string>
|
|
178
|
+
<string>/Users/YOURUSER/.agentnet</string>
|
|
179
|
+
</array>
|
|
180
|
+
<key>RunAtLoad</key><true/>
|
|
181
|
+
<key>KeepAlive</key><true/>
|
|
182
|
+
<key>StandardOutPath</key><string>/var/log/agentnet.log</string>
|
|
183
|
+
<key>StandardErrorPath</key><string>/var/log/agentnet.log</string>
|
|
184
|
+
</dict></plist>
|
|
185
|
+
EOF
|
|
186
|
+
sudo launchctl load /Library/LaunchDaemons/com.decentlan.agentnet.plist
|
|
187
|
+
tail -f /var/log/agentnet.log
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`sudo launchctl unload /Library/LaunchDaemons/com.decentlan.agentnet.plist`
|
|
191
|
+
to stop. Drop the `.plist` file to disable persistence.
|
|
192
|
+
|
|
193
|
+
## Running CLI commands once the daemon is up
|
|
194
|
+
|
|
195
|
+
The CLI talks to the daemon over a Unix socket in the config-dir.
|
|
196
|
+
Most commands (`diag`, `ipam list`, `friends list`, `friends pending`,
|
|
197
|
+
`resolve`, `dora autofriend`) just read or write config files — they
|
|
198
|
+
**don't need sudo**:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
agentnet diag
|
|
202
|
+
agentnet ipam list
|
|
203
|
+
agentnet friends list
|
|
204
|
+
agentnet dora autofriend all
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The few that DO need sudo (writing `/etc/resolver/` on macOS, or
|
|
208
|
+
`/etc/dnsmasq.d/` on Linux) require an explicit `--config-dir`
|
|
209
|
+
because `sudo`'s `$HOME` is `/root`:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
sudo agentnet dns install --config-dir /home/YOURUSER/.agentnet
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## DNS on Linux when dnsPort isn't 53
|
|
216
|
+
|
|
217
|
+
`agentnet init` defaults to `dnsPort: 5354` (5353 is reserved for
|
|
218
|
+
mDNS / Avahi; we step around it). systemd-resolved only forwards to
|
|
219
|
+
upstreams on port 53, so `agentnet dns install` on Linux will warn
|
|
220
|
+
and refuse rather than write a broken config. Two workarounds:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Workaround 1 — static /etc/hosts (simple; re-run on roster changes)
|
|
224
|
+
agentnet dns hosts | sudo tee -a /etc/hosts > /dev/null
|
|
225
|
+
|
|
226
|
+
# Workaround 2 — dnsmasq forward
|
|
227
|
+
agentnet dns install # prints a dnsmasq snippet; paste into
|
|
228
|
+
# /etc/dnsmasq.d/decent.conf, restart dnsmasq
|
|
125
229
|
```
|
|
126
230
|
|
|
127
|
-
|
|
231
|
+
macOS has no port-53 restriction — `agentnet dns install` works
|
|
232
|
+
out of the box there.
|
|
233
|
+
|
|
234
|
+
## Troubleshooting
|
|
128
235
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
236
|
+
| symptom | likely cause | fix |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `Not initialized. Run 'agentnet init' first.` after running with sudo | `sudo` set `$HOME=/root`; CLI is looking at `/root/.agentnet/` | add `--config-dir /home/YOURUSER/.agentnet` |
|
|
239
|
+
| `/var/log/agentnet.log: Permission denied` when starting with `sudo nohup ... >> ...` | `>>` runs in your unprivileged shell before sudo elevates | wrap in `sudo sh -c '...'`, OR pre-create the log file with `chmod 666` |
|
|
240
|
+
| `Unit agentnet.service not found.` after a successful `systemctl restart` | previous launch was a transient `systemd-run` unit (auto-removed on stop) | install the persistent unit file (Option B above) |
|
|
241
|
+
| `dora friend is offline` for >60s right after restart | Carrier session re-handshake takes 30–90s; not a real failure | wait, or `agentnet diag` to confirm `friends[].status == "online"` |
|
|
242
|
+
| `ipam list` shows only the self entry | dora roster fetch hasn't merged yet, or dora server is down | wait one refresh interval (60s default), then check the dora server; also confirm `dora.autoFriend` isn't `none` |
|
|
243
|
+
| `ping <peer>.decent` resolves but times out | TUN routing OK but the remote daemon is older without the auto-learn-IPAM fix | upgrade the remote to `@decentnetwork/lan@0.1.13+` |
|
|
244
|
+
| `ping <peer>.decent` fails with `Unknown host` even though `nslookup -port=5354 <peer>.decent 127.0.0.1` works | macOS hasn't reloaded `/etc/resolver/` | `sudo killall -HUP mDNSResponder` |
|
|
132
245
|
|
|
133
246
|
## Uninstall
|
|
134
247
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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",
|