@decentnetwork/lan 0.1.273 → 0.1.274

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.
@@ -486,6 +486,24 @@ export declare function windowsServiceLauncherScript(args: {
486
486
  configDir: string;
487
487
  logPath: string;
488
488
  }): string;
489
+ /**
490
+ * Key-only SSH for connections arriving over the AgentNet subnet.
491
+ *
492
+ * Runs automatically whenever the user turns on the TUN / virtual LAN
493
+ * (`agentnet up --real-tun`, `agentnet service install`) — a daemon friend
494
+ * gets an L3 route to this machine, and password-guessable SSH over that
495
+ * route is the #1 exposure (docs/SECURITY-L3-HARDENING.md).
496
+ *
497
+ * STRICTLY SCOPED (product rule 2026-08-06): appends a
498
+ * `Match Address 10.86.0.0/16` block to the END of sshd_config — the
499
+ * user's existing physical-LAN/WAN login policy is untouched, and we
500
+ * never rewrite anything they configured. Idempotent via a marker line;
501
+ * original config backed up once at sshd_config.bak.agentnet; validated
502
+ * with `sshd -t` and rolled back if the daemon rejects it.
503
+ */
504
+ export declare function cmdHardenSsh(args?: {
505
+ quiet?: boolean;
506
+ }): Promise<void>;
489
507
  export declare function cmdServiceInstall(args: {
490
508
  uninstall?: boolean;
491
509
  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,
@@ -867,7 +878,7 @@ export async function cmdFriendsAutoAccept(args) {
867
878
  const configPath = resolve(dir, "config.yaml");
868
879
  const config = await ConfigLoader.load(configPath);
869
880
  if (!args.mode) {
870
- const cur = config.friends?.autoAccept ?? true;
881
+ const cur = config.friends?.autoAccept ?? false;
871
882
  console.log(`friends.autoAccept: ${cur ? "on" : "off"}`);
872
883
  console.log(cur
873
884
  ? " Incoming friend-requests are accepted automatically."
@@ -2195,6 +2206,76 @@ while ($true) {
2195
2206
  }
2196
2207
  `;
2197
2208
  }
2209
+ /**
2210
+ * Key-only SSH for connections arriving over the AgentNet subnet.
2211
+ *
2212
+ * Runs automatically whenever the user turns on the TUN / virtual LAN
2213
+ * (`agentnet up --real-tun`, `agentnet service install`) — a daemon friend
2214
+ * gets an L3 route to this machine, and password-guessable SSH over that
2215
+ * route is the #1 exposure (docs/SECURITY-L3-HARDENING.md).
2216
+ *
2217
+ * STRICTLY SCOPED (product rule 2026-08-06): appends a
2218
+ * `Match Address 10.86.0.0/16` block to the END of sshd_config — the
2219
+ * user's existing physical-LAN/WAN login policy is untouched, and we
2220
+ * never rewrite anything they configured. Idempotent via a marker line;
2221
+ * original config backed up once at sshd_config.bak.agentnet; validated
2222
+ * with `sshd -t` and rolled back if the daemon rejects it.
2223
+ */
2224
+ export async function cmdHardenSsh(args = {}) {
2225
+ const MARK = "agentnet L3 hardening";
2226
+ const say = (m) => { if (!args.quiet)
2227
+ console.log(m); };
2228
+ if (process.platform === "win32") {
2229
+ say("[harden-ssh] Windows: append to C:\\ProgramData\\ssh\\sshd_config:\n" +
2230
+ " Match Address 10.86.0.0/16\n PasswordAuthentication no\n" +
2231
+ "then Restart-Service sshd (as Administrator).");
2232
+ return;
2233
+ }
2234
+ const cfg = "/etc/ssh/sshd_config";
2235
+ if (!existsSync(cfg)) {
2236
+ say("[harden-ssh] no /etc/ssh/sshd_config (no OpenSSH server) — nothing to do");
2237
+ return;
2238
+ }
2239
+ if (readFileSync(cfg, "utf-8").includes(MARK)) {
2240
+ say("[harden-ssh] already hardened — nothing to do");
2241
+ return;
2242
+ }
2243
+ if (typeof process.getuid === "function" && process.getuid() !== 0) {
2244
+ // Not fatal: the caller may be a non-root `up` on a mock TUN box.
2245
+ console.log("[harden-ssh] needs root — run: sudo agentnet harden-ssh");
2246
+ return;
2247
+ }
2248
+ const { execSync } = await import("child_process");
2249
+ const { copyFileSync, appendFileSync } = await import("fs");
2250
+ const bak = `${cfg}.bak.agentnet`;
2251
+ if (!existsSync(bak))
2252
+ copyFileSync(cfg, bak);
2253
+ appendFileSync(cfg, `\n# ${MARK}: key-only SSH from the virtual net (10.86/16 only)\n` +
2254
+ `Match Address 10.86.0.0/16\n PasswordAuthentication no\n KbdInteractiveAuthentication no\n`);
2255
+ try {
2256
+ execSync("/usr/sbin/sshd -t", { stdio: "pipe" });
2257
+ }
2258
+ catch (err) {
2259
+ copyFileSync(bak, cfg);
2260
+ throw new Error(`[harden-ssh] sshd rejected the change — restored original config: ${err.message}`);
2261
+ }
2262
+ if (process.platform === "darwin") {
2263
+ try {
2264
+ execSync("launchctl kickstart -k system/com.openssh.sshd", { stdio: "pipe" });
2265
+ }
2266
+ catch { /* socket-activated: new connections read the config anyway */ }
2267
+ }
2268
+ else {
2269
+ for (const c of ["systemctl reload ssh", "systemctl reload sshd", "service ssh reload"]) {
2270
+ try {
2271
+ execSync(c, { stdio: "pipe" });
2272
+ break;
2273
+ }
2274
+ catch { /* try the next spelling */ }
2275
+ }
2276
+ }
2277
+ console.log(`[harden-ssh] SSH from 10.86.0.0/16 is now key-only. Physical-network logins unchanged; backup: ${bak}`);
2278
+ }
2198
2279
  export async function cmdServiceInstall(args) {
2199
2280
  // Detect sudo's $HOME=/root trap. When `service install` is run via
2200
2281
  // sudo and no --config-dir is provided, derive the home dir from
@@ -2337,6 +2418,12 @@ WantedBy=multi-user.target
2337
2418
  }
2338
2419
  execSync("systemctl daemon-reload");
2339
2420
  execSync("systemctl enable --now agentnet");
2421
+ try {
2422
+ await cmdHardenSsh({ quiet: true });
2423
+ }
2424
+ catch (err) {
2425
+ console.warn(String(err.message ?? err));
2426
+ }
2340
2427
  console.log(`Installed ${unitPath} and started agentnet.service.`);
2341
2428
  console.log(`Logs: journalctl -u agentnet -f`);
2342
2429
  console.log(`Optional — watch China video (CCTV etc.): agentnet proxy trust-ca && agentnet proxy router --hls-accel (docs/CCTV-VIEWING.md)`);
@@ -2414,6 +2501,12 @@ WantedBy=multi-user.target
2414
2501
  console.log(`Note: could not install the newsyslog rotation rule (${err instanceof Error ? err.message : err}). The log is still throttled in-process.`);
2415
2502
  }
2416
2503
  execSync(`launchctl load ${plistPath}`);
2504
+ try {
2505
+ await cmdHardenSsh({ quiet: true });
2506
+ }
2507
+ catch (err) {
2508
+ console.warn(String(err.message ?? err));
2509
+ }
2417
2510
  console.log(`Installed ${plistPath} and started com.decentlan.agentnet.`);
2418
2511
  console.log(`Optional — watch China video (CCTV etc.): agentnet proxy trust-ca && agentnet proxy router --hls-accel (docs/CCTV-VIEWING.md)`);
2419
2512
  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
@@ -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.274";
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.274",
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",