anon-pi 0.11.0 → 0.12.0

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/src/cli.ts CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  parseKeptKey,
62
62
  keyProject,
63
63
  resolveManagedMatches,
64
+ parseNetcagePsJson,
64
65
  parseNetcagePortsJson,
65
66
  forwardablePorts,
66
67
  formatPortsHint,
@@ -177,7 +178,7 @@ function main(argv: string[]): number {
177
178
  return runInit(args.slice(1));
178
179
  }
179
180
 
180
- // Host-access verbs (netcage >= 0.9.0). `forward` opens a host->jail port; the
181
+ // Host-access verbs (netcage >= 0.10.0). `forward` opens a host->jail port; the
181
182
  // `ports` sibling lists a jail's open listeners. Dispatched BEFORE the launch
182
183
  // grammar so `forward`/`ports` are never parsed as a project name.
183
184
  if (args[0] === 'forward') {
@@ -418,13 +419,15 @@ function executeLaunchPlan(
418
419
  if (intent.keep) {
419
420
  const decision = resolveRunVsStart(intent, queryKeptContainers());
420
421
  if (decision.action === 'start') {
421
- return spawnNetcage(['start', '-a', '-i', decision.ref]);
422
+ return spawnNetcage(['start', '-a', '-i', decision.ref], {
423
+ enteringJail: true,
424
+ });
422
425
  }
423
426
  // A fresh `--keep` run: the RunPlan already omits --rm so it is left kept.
424
- return spawnNetcage(keyed);
427
+ return spawnNetcage(keyed, {enteringJail: true});
425
428
  }
426
429
 
427
- return spawnNetcage(keyed);
430
+ return spawnNetcage(keyed, {enteringJail: true});
428
431
  }
429
432
 
430
433
  // --- the interactive host-side menu (the ONLY untested I/O) -------------------
@@ -2021,7 +2024,7 @@ USAGE
2021
2024
  --bind passed through to netcage (127.0.0.1 default, or 0.0.0.0 for LAN).
2022
2025
  -m the machine the container runs on (else the default machine).
2023
2026
 
2024
- Wraps \`netcage forward\` (netcage >= 0.9.0). If several containers match, you pick
2027
+ Wraps \`netcage forward\` (netcage >= 0.10.0). If several containers match, you pick
2025
2028
  one (each row shows its open in-jail ports). The forward runs until Ctrl-C.
2026
2029
  `;
2027
2030
 
@@ -2031,7 +2034,7 @@ const PORTS_HELP = `anon-pi ports - list a running anon-pi container's open in-j
2031
2034
  USAGE
2032
2035
  anon-pi ports [<project>] [-m <machine>]
2033
2036
 
2034
- Wraps \`netcage ports --json\` (netcage >= 0.9.0), which reads the jail's
2037
+ Wraps \`netcage ports --json\` (netcage >= 0.10.0), which reads the jail's
2035
2038
  /proc/net/tcp* image-independently (works even with no ss/netstat in the image).
2036
2039
  Use it to find which port to \`anon-pi forward\`.
2037
2040
  `;
@@ -2092,33 +2095,44 @@ function homeFresh(machineHome: string): boolean {
2092
2095
  * fresh `run` (safe: it never wrongly resumes, it just creates a new container).
2093
2096
  */
2094
2097
  function queryKeptContainers(): KeptContainer[] {
2095
- // Ask netcage for its managed containers as JSON, reading back the anon-pi
2096
- // key label. netcage is a podman drop-in, so `ps` accepts the same
2097
- // label-filter + Go-template/JSON format flags.
2098
- const res = spawnSync(
2099
- 'netcage',
2100
- [
2101
- 'ps',
2102
- '-a',
2103
- '--filter',
2104
- 'label=netcage.managed',
2105
- '--format',
2106
- '{{.ID}}\t{{.Labels}}',
2107
- ],
2108
- {encoding: 'utf8'},
2109
- );
2110
- if (res.error || res.status !== 0 || !res.stdout) return [];
2098
+ // Ask netcage for ALL its managed containers as JSON (netcage >= 0.10.0
2099
+ // forwards podman's --format json over its managed scope), then keep the ones
2100
+ // carrying an anon-pi.key label (a sidecar has none) and decode it. -a so a
2101
+ // STOPPED kept container is included (run-vs-start resumes it).
2102
+ return queryManagedContainers({all: true}).map(({key, ref}) => ({key, ref}));
2103
+ }
2104
+
2105
+ /**
2106
+ * Decode a base64 anon-pi.key label back to its identity key (the reverse of
2107
+ * withKeyLabel's encode; keptContainerKey embeds newlines, so it is base64'd to
2108
+ * stay a single safe label value). undefined on a decode error.
2109
+ */
2110
+ function decodeKeyLabel(raw: string): string | undefined {
2111
+ try {
2112
+ return Buffer.from(raw, 'base64').toString('utf8');
2113
+ } catch {
2114
+ return undefined;
2115
+ }
2116
+ }
2111
2117
 
2112
- const out: KeptContainer[] = [];
2113
- for (const line of res.stdout.split('\n')) {
2114
- const trimmed = line.trim();
2115
- if (trimmed === '') continue;
2116
- const tab = trimmed.indexOf('\t');
2117
- if (tab < 0) continue;
2118
- const ref = trimmed.slice(0, tab).trim();
2119
- const labels = trimmed.slice(tab + 1);
2120
- const key = extractKeyLabel(labels);
2121
- if (ref !== '' && key !== undefined) out.push({key, ref});
2118
+ /**
2119
+ * The shared `netcage ps --format json` query: parse the JSON to anon-pi's
2120
+ * containers (pure parseNetcagePsJson keeps only anon-pi.key-labelled entries,
2121
+ * optionally running-only), then base64-DECODE each key. Best-effort: [] on any
2122
+ * failure. `all` => `-a` (include stopped); else running only.
2123
+ */
2124
+ function queryManagedContainers(opts: {
2125
+ all?: boolean;
2126
+ }): {key: string; ref: string; name: string}[] {
2127
+ const args = ['ps'];
2128
+ if (opts.all) args.push('-a');
2129
+ args.push('--filter', 'label=netcage.managed', '--format', 'json');
2130
+ const res = spawnSync('netcage', args, {encoding: 'utf8'});
2131
+ if (res.error || res.status !== 0 || !res.stdout) return [];
2132
+ const out: {key: string; ref: string; name: string}[] = [];
2133
+ for (const e of parseNetcagePsJson(res.stdout, {runningOnly: !opts.all})) {
2134
+ const key = decodeKeyLabel(e.key);
2135
+ if (key !== undefined) out.push({key, ref: e.ref, name: e.name});
2122
2136
  }
2123
2137
  return out;
2124
2138
  }
@@ -2132,32 +2146,7 @@ function queryKeptContainers(): KeptContainer[] {
2132
2146
  * failure, so the caller reports "nothing running" cleanly.
2133
2147
  */
2134
2148
  function queryRunningContainers(): ManagedContainer[] {
2135
- const res = spawnSync(
2136
- 'netcage',
2137
- [
2138
- 'ps',
2139
- '--filter',
2140
- 'label=netcage.managed',
2141
- '--format',
2142
- '{{.ID}}\t{{.Names}}\t{{.Labels}}',
2143
- ],
2144
- {encoding: 'utf8'},
2145
- );
2146
- if (res.error || res.status !== 0 || !res.stdout) return [];
2147
- const out: ManagedContainer[] = [];
2148
- for (const line of res.stdout.split('\n')) {
2149
- const trimmed = line.trim();
2150
- if (trimmed === '') continue;
2151
- const cols = trimmed.split('\t');
2152
- if (cols.length < 3) continue;
2153
- const ref = cols[0].trim();
2154
- const name = cols[1].trim();
2155
- const key = extractKeyLabel(cols.slice(2).join('\t'));
2156
- if (ref !== '' && key !== undefined) {
2157
- out.push({key, ref, name: name || ref});
2158
- }
2159
- }
2160
- return out;
2149
+ return queryManagedContainers({all: false});
2161
2150
  }
2162
2151
 
2163
2152
  /**
@@ -2376,28 +2365,6 @@ function netcageMissing(): number {
2376
2365
  return 1;
2377
2366
  }
2378
2367
 
2379
- /**
2380
- * Pull the anon-pi key out of a podman `{{.Labels}}` rendering (a
2381
- * comma-separated `k=v` list). The key is stamped as `anon-pi.key=<opaque>`;
2382
- * because keptContainerKey embeds newlines, the CLI base64-encodes it when
2383
- * stamping (withKeyLabel) and decodes it here, so a `\n` never breaks the label.
2384
- */
2385
- function extractKeyLabel(labels: string): string | undefined {
2386
- for (const pair of labels.split(',')) {
2387
- const eq = pair.indexOf('=');
2388
- if (eq < 0) continue;
2389
- const k = pair.slice(0, eq).trim();
2390
- if (k !== ANON_PI_KEY_LABEL) continue;
2391
- const v = pair.slice(eq + 1).trim();
2392
- try {
2393
- return Buffer.from(v, 'base64').toString('utf8');
2394
- } catch {
2395
- return undefined;
2396
- }
2397
- }
2398
- return undefined;
2399
- }
2400
-
2401
2368
  /**
2402
2369
  * Insert the anon-pi identity label into a `netcage run` argv (right after
2403
2370
  * `run`), so a kept container can be found on re-entry. The key is base64'd
@@ -2463,14 +2430,20 @@ function firstLine(path: string): string | undefined {
2463
2430
  }
2464
2431
 
2465
2432
  /** Spawn netcage with inherited stdio; propagate its exit code. */
2466
- function spawnNetcage(netcageArgs: string[]): number {
2467
- // Explain the pause: netcage sets up the jail (netns, firewall, DNS, container
2468
- // start) BEFORE pi paints, so without this line the user sees only a blinking
2469
- // cursor. Goes to stderr so it never pollutes any piped stdout, and is
2470
- // transient (pi typically clears the screen when its TUI comes up).
2471
- process.stderr.write(
2472
- 'anon-pi: entering the netcage jail (setting up forced-egress)\u2026\n',
2473
- );
2433
+ function spawnNetcage(
2434
+ netcageArgs: string[],
2435
+ opts: {enteringJail?: boolean} = {},
2436
+ ): number {
2437
+ // Explain the pause on a LAUNCH: netcage sets up the jail (netns, firewall,
2438
+ // DNS, container start) BEFORE pi paints, so without this line the user sees
2439
+ // only a blinking cursor. Goes to stderr (never pollutes piped stdout), and is
2440
+ // transient (pi clears the screen when its TUI comes up). NOT printed for
2441
+ // `forward` (it attaches to an existing jail, and prints its own line).
2442
+ if (opts.enteringJail) {
2443
+ process.stderr.write(
2444
+ 'anon-pi: entering the netcage jail (setting up forced-egress)\u2026\n',
2445
+ );
2446
+ }
2474
2447
  const res = spawnSync('netcage', netcageArgs, {stdio: 'inherit'});
2475
2448
  if (res.error) {
2476
2449
  process.stderr.write(