@decentnetwork/lan 0.1.273 → 0.1.275

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.
@@ -70,6 +70,7 @@ export declare function cmdUp(args: {
70
70
  configDir?: string;
71
71
  realTun?: boolean;
72
72
  }): Promise<void>;
73
+ export declare function resolveBeaglesName(input: string): Promise<string | null>;
73
74
  /**
74
75
  * Send a friend request to another peer's address.
75
76
  * Run while daemon is DOWN — opens a temporary peer, sends request, exits.
@@ -486,6 +487,24 @@ export declare function windowsServiceLauncherScript(args: {
486
487
  configDir: string;
487
488
  logPath: string;
488
489
  }): string;
490
+ /**
491
+ * Key-only SSH for connections arriving over the AgentNet subnet.
492
+ *
493
+ * Runs automatically whenever the user turns on the TUN / virtual LAN
494
+ * (`agentnet up --real-tun`, `agentnet service install`) — a daemon friend
495
+ * gets an L3 route to this machine, and password-guessable SSH over that
496
+ * route is the #1 exposure (docs/SECURITY-L3-HARDENING.md).
497
+ *
498
+ * STRICTLY SCOPED (product rule 2026-08-06): appends a
499
+ * `Match Address 10.86.0.0/16` block to the END of sshd_config — the
500
+ * user's existing physical-LAN/WAN login policy is untouched, and we
501
+ * never rewrite anything they configured. Idempotent via a marker line;
502
+ * original config backed up once at sshd_config.bak.agentnet; validated
503
+ * with `sshd -t` and rolled back if the daemon rejects it.
504
+ */
505
+ export declare function cmdHardenSsh(args?: {
506
+ quiet?: boolean;
507
+ }): Promise<void>;
489
508
  export declare function cmdServiceInstall(args: {
490
509
  uninstall?: boolean;
491
510
  configDir?: string;
@@ -449,6 +449,17 @@ export async function cmdUp(args) {
449
449
  // full disk stops the daemon outright. Covers the SDK's [peer-debug] output
450
450
  // too, since this wraps the process console rather than our Logger.
451
451
  installLogThrottle();
452
+ // Turning on the real TUN = giving friends an L3 route here. Close the
453
+ // password-SSH door on that route first (scoped, idempotent, no-op when
454
+ // already done or when there is no sshd).
455
+ if (args.realTun) {
456
+ try {
457
+ await cmdHardenSsh({ quiet: true });
458
+ }
459
+ catch (err) {
460
+ console.warn(String(err.message ?? err));
461
+ }
462
+ }
452
463
  const daemon = new DaemonServer({
453
464
  config,
454
465
  configDir: dir,
@@ -469,6 +480,42 @@ export async function cmdUp(args) {
469
480
  // Keep alive
470
481
  await new Promise(() => { });
471
482
  }
483
+ /**
484
+ * Resolve a *.beagles.eth name to its Carrier address via the ens-gateway
485
+ * worker (the registry the Beagle mobile apps register into). Only names
486
+ * from OUR gateway resolve — this is not a general ENS resolver.
487
+ *
488
+ * Returns null when the input doesn't look like a name (so callers can
489
+ * fall through to treating it as a raw Carrier address). Throws when it
490
+ * IS a name but the gateway doesn't know it.
491
+ */
492
+ const ENS_GATEWAY = "https://ens-gateway.beaglechat.workers.dev";
493
+ export async function resolveBeaglesName(input) {
494
+ const raw = input.trim();
495
+ // A Carrier address is 52 base58 chars, a userid 44 — both dot-free.
496
+ // Anything with a dot, a space, or too short to be either is a name.
497
+ const looksLikeName = raw.includes(".") || raw.includes(" ") || raw.length < 40;
498
+ if (!looksLikeName)
499
+ return null;
500
+ const full = raw.includes(".") ? raw : `${raw}.beagles.eth`;
501
+ // Registered labels aren't strictly normalized upstream (mixed case,
502
+ // stray spaces exist in the registry) — compare loosely.
503
+ const norm = (s) => s.toLowerCase().replace(/\s+/g, "");
504
+ const want = norm(full);
505
+ const res = await fetch(`${ENS_GATEWAY}/names`);
506
+ if (!res.ok)
507
+ throw new Error(`name lookup failed: gateway HTTP ${res.status}`);
508
+ const names = (await res.json());
509
+ for (const [key, val] of Object.entries(names)) {
510
+ if (norm(key) !== want)
511
+ continue;
512
+ const addr = val?.texts?.carrierAddress;
513
+ if (!addr)
514
+ throw new Error(`'${key}' is registered but has no Carrier address bound`);
515
+ return addr;
516
+ }
517
+ throw new Error(`name '${full}' is not registered on the beagles.eth gateway`);
518
+ }
472
519
  /**
473
520
  * Send a friend request to another peer's address.
474
521
  * Run while daemon is DOWN — opens a temporary peer, sends request, exits.
@@ -478,6 +525,12 @@ export async function cmdUp(args) {
478
525
  export async function cmdFriendRequest(args) {
479
526
  const dir = args.configDir || ConfigLoader.defaultConfigDir();
480
527
  const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
528
+ // Accept a *.beagles.eth name in place of a raw Carrier address.
529
+ const resolved = await resolveBeaglesName(args.address);
530
+ if (resolved !== null) {
531
+ console.log(`Resolved '${args.address.trim()}' → ${resolved}`);
532
+ args = { ...args, address: resolved };
533
+ }
481
534
  // If the daemon is running, route through IPC instead of spawning a
482
535
  // second Carrier peer. The daemon's existing Peer instance sends the
483
536
  // request — no session conflict, no second DHT announce, no race.
@@ -867,7 +920,7 @@ export async function cmdFriendsAutoAccept(args) {
867
920
  const configPath = resolve(dir, "config.yaml");
868
921
  const config = await ConfigLoader.load(configPath);
869
922
  if (!args.mode) {
870
- const cur = config.friends?.autoAccept ?? true;
923
+ const cur = config.friends?.autoAccept ?? false;
871
924
  console.log(`friends.autoAccept: ${cur ? "on" : "off"}`);
872
925
  console.log(cur
873
926
  ? " Incoming friend-requests are accepted automatically."
@@ -2195,6 +2248,76 @@ while ($true) {
2195
2248
  }
2196
2249
  `;
2197
2250
  }
2251
+ /**
2252
+ * Key-only SSH for connections arriving over the AgentNet subnet.
2253
+ *
2254
+ * Runs automatically whenever the user turns on the TUN / virtual LAN
2255
+ * (`agentnet up --real-tun`, `agentnet service install`) — a daemon friend
2256
+ * gets an L3 route to this machine, and password-guessable SSH over that
2257
+ * route is the #1 exposure (docs/SECURITY-L3-HARDENING.md).
2258
+ *
2259
+ * STRICTLY SCOPED (product rule 2026-08-06): appends a
2260
+ * `Match Address 10.86.0.0/16` block to the END of sshd_config — the
2261
+ * user's existing physical-LAN/WAN login policy is untouched, and we
2262
+ * never rewrite anything they configured. Idempotent via a marker line;
2263
+ * original config backed up once at sshd_config.bak.agentnet; validated
2264
+ * with `sshd -t` and rolled back if the daemon rejects it.
2265
+ */
2266
+ export async function cmdHardenSsh(args = {}) {
2267
+ const MARK = "agentnet L3 hardening";
2268
+ const say = (m) => { if (!args.quiet)
2269
+ console.log(m); };
2270
+ if (process.platform === "win32") {
2271
+ say("[harden-ssh] Windows: append to C:\\ProgramData\\ssh\\sshd_config:\n" +
2272
+ " Match Address 10.86.0.0/16\n PasswordAuthentication no\n" +
2273
+ "then Restart-Service sshd (as Administrator).");
2274
+ return;
2275
+ }
2276
+ const cfg = "/etc/ssh/sshd_config";
2277
+ if (!existsSync(cfg)) {
2278
+ say("[harden-ssh] no /etc/ssh/sshd_config (no OpenSSH server) — nothing to do");
2279
+ return;
2280
+ }
2281
+ if (readFileSync(cfg, "utf-8").includes(MARK)) {
2282
+ say("[harden-ssh] already hardened — nothing to do");
2283
+ return;
2284
+ }
2285
+ if (typeof process.getuid === "function" && process.getuid() !== 0) {
2286
+ // Not fatal: the caller may be a non-root `up` on a mock TUN box.
2287
+ console.log("[harden-ssh] needs root — run: sudo agentnet harden-ssh");
2288
+ return;
2289
+ }
2290
+ const { execSync } = await import("child_process");
2291
+ const { copyFileSync, appendFileSync } = await import("fs");
2292
+ const bak = `${cfg}.bak.agentnet`;
2293
+ if (!existsSync(bak))
2294
+ copyFileSync(cfg, bak);
2295
+ appendFileSync(cfg, `\n# ${MARK}: key-only SSH from the virtual net (10.86/16 only)\n` +
2296
+ `Match Address 10.86.0.0/16\n PasswordAuthentication no\n KbdInteractiveAuthentication no\n`);
2297
+ try {
2298
+ execSync("/usr/sbin/sshd -t", { stdio: "pipe" });
2299
+ }
2300
+ catch (err) {
2301
+ copyFileSync(bak, cfg);
2302
+ throw new Error(`[harden-ssh] sshd rejected the change — restored original config: ${err.message}`);
2303
+ }
2304
+ if (process.platform === "darwin") {
2305
+ try {
2306
+ execSync("launchctl kickstart -k system/com.openssh.sshd", { stdio: "pipe" });
2307
+ }
2308
+ catch { /* socket-activated: new connections read the config anyway */ }
2309
+ }
2310
+ else {
2311
+ for (const c of ["systemctl reload ssh", "systemctl reload sshd", "service ssh reload"]) {
2312
+ try {
2313
+ execSync(c, { stdio: "pipe" });
2314
+ break;
2315
+ }
2316
+ catch { /* try the next spelling */ }
2317
+ }
2318
+ }
2319
+ console.log(`[harden-ssh] SSH from 10.86.0.0/16 is now key-only. Physical-network logins unchanged; backup: ${bak}`);
2320
+ }
2198
2321
  export async function cmdServiceInstall(args) {
2199
2322
  // Detect sudo's $HOME=/root trap. When `service install` is run via
2200
2323
  // sudo and no --config-dir is provided, derive the home dir from
@@ -2337,6 +2460,12 @@ WantedBy=multi-user.target
2337
2460
  }
2338
2461
  execSync("systemctl daemon-reload");
2339
2462
  execSync("systemctl enable --now agentnet");
2463
+ try {
2464
+ await cmdHardenSsh({ quiet: true });
2465
+ }
2466
+ catch (err) {
2467
+ console.warn(String(err.message ?? err));
2468
+ }
2340
2469
  console.log(`Installed ${unitPath} and started agentnet.service.`);
2341
2470
  console.log(`Logs: journalctl -u agentnet -f`);
2342
2471
  console.log(`Optional — watch China video (CCTV etc.): agentnet proxy trust-ca && agentnet proxy router --hls-accel (docs/CCTV-VIEWING.md)`);
@@ -2414,6 +2543,12 @@ WantedBy=multi-user.target
2414
2543
  console.log(`Note: could not install the newsyslog rotation rule (${err instanceof Error ? err.message : err}). The log is still throttled in-process.`);
2415
2544
  }
2416
2545
  execSync(`launchctl load ${plistPath}`);
2546
+ try {
2547
+ await cmdHardenSsh({ quiet: true });
2548
+ }
2549
+ catch (err) {
2550
+ console.warn(String(err.message ?? err));
2551
+ }
2417
2552
  console.log(`Installed ${plistPath} and started com.decentlan.agentnet.`);
2418
2553
  console.log(`Optional — watch China video (CCTV etc.): agentnet proxy trust-ca && agentnet proxy router --hls-accel (docs/CCTV-VIEWING.md)`);
2419
2554
  console.log(`Logs: tail -f /var/log/agentnet.log`);
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, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyWhitelist, cmdProxyFileWhitelist, cmdProxyBusyMbps, cmdProxyUse, cmdProxyRouter, cmdProxyTrustCa, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdBootstrapShow, cmdBootstrapUpdate, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
13
+ import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsAutoAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyWho, cmdProxyAccess, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyWhitelist, cmdProxyFileWhitelist, cmdProxyBusyMbps, cmdProxyUse, cmdProxyRouter, cmdProxyTrustCa, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdBootstrapShow, cmdBootstrapUpdate, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdHardenSsh, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
14
14
  async function main() {
15
15
  await yargs(hideBin(process.argv))
16
16
  .scriptName("agentnet")
@@ -149,6 +149,9 @@ async function main() {
149
149
  })
150
150
  .demandCommand(1, "Specify a dns subcommand (run 'agentnet dns --help')"), () => {
151
151
  // parent handler — never invoked because demandCommand
152
+ })
153
+ .command("harden-ssh", "Make SSH key-only for connections from the AgentNet subnet (10.86/16 only; physical-network logins unchanged)", (y) => y, async () => {
154
+ await cmdHardenSsh();
152
155
  })
153
156
  // Persistent system service — wraps systemctl (Linux), launchctl
154
157
  // (macOS), or a highest-privilege startup task (Windows) so a new operator runs ONE command to install
@@ -224,7 +227,7 @@ async function main() {
224
227
  });
225
228
  })
226
229
  .command("friend-request <address>", "Send a friend request (routes through the running daemon when up; opens a standalone peer when down)", (y) => y
227
- .positional("address", { type: "string", demandOption: true, describe: "Recipient Carrier address" })
230
+ .positional("address", { type: "string", demandOption: true, describe: "Recipient Carrier address, or a *.beagles.eth name (bare label ok, e.g. 's3ns')" })
228
231
  .option("hello", { type: "string", describe: "Greeting message" })
229
232
  .option("wait-ms", { type: "number", default: 8000, describe: "Wait time for relay delivery" })
230
233
  .option("config-dir", { type: "string" }), async (argv) => {
@@ -301,12 +301,14 @@ export class ConfigLoader {
301
301
  enabled: false,
302
302
  port: 8888,
303
303
  },
304
- // Auto-accept incoming friend requests by default. The Carrier
305
- // network is already a friend network — if you don't want a peer,
306
- // don't share your address with them. Disable with
307
- // `friends.autoAccept: false` for stricter control.
304
+ // Manual friend approval by default (2026-08-06 security decision).
305
+ // A daemon friend gets an L3 route to this machine, and addresses
306
+ // are now publicly listed (Beagle discover/names directories) — so
307
+ // auto-accept + TUN would let strangers onto your virtual LAN.
308
+ // Infra nodes (doras/exits) that must accept strangers set
309
+ // `friends.autoAccept: true` explicitly.
308
310
  friends: {
309
- autoAccept: true,
311
+ autoAccept: false,
310
312
  },
311
313
  // Dora integration is ON by default and points at the public
312
314
  // canonical dora — `agentnet init` follows up with a one-time
@@ -883,12 +883,14 @@ var ConfigLoader = class {
883
883
  enabled: false,
884
884
  port: 8888
885
885
  },
886
- // Auto-accept incoming friend requests by default. The Carrier
887
- // network is already a friend network — if you don't want a peer,
888
- // don't share your address with them. Disable with
889
- // `friends.autoAccept: false` for stricter control.
886
+ // Manual friend approval by default (2026-08-06 security decision).
887
+ // A daemon friend gets an L3 route to this machine, and addresses
888
+ // are now publicly listed (Beagle discover/names directories) — so
889
+ // auto-accept + TUN would let strangers onto your virtual LAN.
890
+ // Infra nodes (doras/exits) that must accept strangers set
891
+ // `friends.autoAccept: true` explicitly.
890
892
  friends: {
891
- autoAccept: true
893
+ autoAccept: false
892
894
  },
893
895
  // Dora integration is ON by default and points at the public
894
896
  // canonical dora — `agentnet init` follows up with a one-time
@@ -919,7 +919,7 @@ export class DaemonServer {
919
919
  statusMessage: this.config.node.statusMessage ?? "",
920
920
  // Beagle's Profile tab renders the auto-accept toggle only when
921
921
  // the backend reports the current state.
922
- autoAccept: this.config.friends?.autoAccept ?? true,
922
+ autoAccept: this.config.friends?.autoAccept ?? false,
923
923
  },
924
924
  tun: this.tunDevice?.getConfig(),
925
925
  // When Dora is unavailable, config.network.ip can still be the
@@ -1255,7 +1255,10 @@ export class DaemonServer {
1255
1255
  this.ipcEvents.emit("event", { type: "request", userid });
1256
1256
  // Read the flag LIVE (not captured at start) so `agentnet friends
1257
1257
  // autoaccept off` takes effect immediately, no daemon restart.
1258
- if (this.config.friends?.autoAccept ?? true) {
1258
+ // Default flipped to false 2026-08-06: friend = L3 route, and
1259
+ // addresses are public in the Beagle directories now. Infra
1260
+ // (doras/exits) opts back in explicitly in its config.
1261
+ if (this.config.friends?.autoAccept ?? false) {
1259
1262
  this.logger.info(`Friend request from ${who}${hello} — auto-accepting`);
1260
1263
  this.peerManager?.acceptFriendRequest(req.pubkey).catch((err) => {
1261
1264
  this.logger.warn(`Auto-accept failed for ${who}: ${err}`);
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.273";
1
+ window.__DK_UI_VERSION="0.1.275";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.273",
3
+ "version": "0.1.275",
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",