anon-pi 0.10.0 → 0.11.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/README.md +32 -0
- package/dist/anon-pi.d.ts +129 -1
- package/dist/anon-pi.d.ts.map +1 -1
- package/dist/anon-pi.js +240 -0
- package/dist/anon-pi.js.map +1 -1
- package/dist/cli.js +272 -6
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/anon-pi.ts +314 -0
- package/src/cli.ts +315 -7
package/src/cli.ts
CHANGED
|
@@ -55,6 +55,15 @@ import {
|
|
|
55
55
|
resolveDeleteProject,
|
|
56
56
|
parseConfigJson,
|
|
57
57
|
parseLaunchArgs,
|
|
58
|
+
parseForwardArgs,
|
|
59
|
+
parsePortsArgs,
|
|
60
|
+
parsePortArg,
|
|
61
|
+
parseKeptKey,
|
|
62
|
+
keyProject,
|
|
63
|
+
resolveManagedMatches,
|
|
64
|
+
parseNetcagePortsJson,
|
|
65
|
+
forwardablePorts,
|
|
66
|
+
formatPortsHint,
|
|
58
67
|
isHeadlessPiArgs,
|
|
59
68
|
resumeSessionId,
|
|
60
69
|
sessionHeaderCwd,
|
|
@@ -102,6 +111,8 @@ import {
|
|
|
102
111
|
type ModelCandidate,
|
|
103
112
|
type ModelSelection,
|
|
104
113
|
type KeptContainer,
|
|
114
|
+
type ManagedContainer,
|
|
115
|
+
type NetcageListener,
|
|
105
116
|
type LaunchIntent,
|
|
106
117
|
type Machine,
|
|
107
118
|
type MachineConfig,
|
|
@@ -132,7 +143,7 @@ function main(argv: string[]): number {
|
|
|
132
143
|
// and `anon-pi machine --help` show THEIR help, not the global one). Those
|
|
133
144
|
// subcommands route to runInit / runMachine, which print INIT_HELP /
|
|
134
145
|
// MACHINE_HELP respectively.
|
|
135
|
-
const OWN_HELP_SUBCOMMANDS = new Set(['init', 'machine']);
|
|
146
|
+
const OWN_HELP_SUBCOMMANDS = new Set(['init', 'machine', 'forward', 'ports']);
|
|
136
147
|
if (
|
|
137
148
|
(args.includes('--help') || args.includes('-h')) &&
|
|
138
149
|
!OWN_HELP_SUBCOMMANDS.has(args[0] ?? '')
|
|
@@ -166,6 +177,16 @@ function main(argv: string[]): number {
|
|
|
166
177
|
return runInit(args.slice(1));
|
|
167
178
|
}
|
|
168
179
|
|
|
180
|
+
// Host-access verbs (netcage >= 0.9.0). `forward` opens a host->jail port; the
|
|
181
|
+
// `ports` sibling lists a jail's open listeners. Dispatched BEFORE the launch
|
|
182
|
+
// grammar so `forward`/`ports` are never parsed as a project name.
|
|
183
|
+
if (args[0] === 'forward') {
|
|
184
|
+
return runForward(args.slice(1));
|
|
185
|
+
}
|
|
186
|
+
if (args[0] === 'ports') {
|
|
187
|
+
return runPorts(args.slice(1));
|
|
188
|
+
}
|
|
189
|
+
|
|
169
190
|
let parsed: ParsedLaunch;
|
|
170
191
|
try {
|
|
171
192
|
parsed = parseLaunchArgs(args);
|
|
@@ -383,6 +404,13 @@ function executeLaunchPlan(
|
|
|
383
404
|
mkdirSync(intent.mountParent ?? intent.projectsRoot, {recursive: true});
|
|
384
405
|
}
|
|
385
406
|
|
|
407
|
+
// The anon-pi identity key, stamped on EVERY launch (not just --keep) as an
|
|
408
|
+
// additive netcage label. Under --keep it lets a re-entry find + `netcage
|
|
409
|
+
// start` the kept container; on a throwaway --rm run it lets `anon-pi forward`
|
|
410
|
+
// find the RUNNING container while it is up (the label goes away with the
|
|
411
|
+
// container on exit). It touches NO egress flag (the RunPlan owns those).
|
|
412
|
+
const keyed = withKeyLabel(plan.netcageArgs, keptContainerKey(intent));
|
|
413
|
+
|
|
386
414
|
// Run-vs-start: under --keep, ask netcage for its kept managed containers and
|
|
387
415
|
// resume a matching one via `netcage start`; else run the composed argv. A
|
|
388
416
|
// throwaway (`--rm`) launch is always a fresh run (the pure rule never
|
|
@@ -392,14 +420,11 @@ function executeLaunchPlan(
|
|
|
392
420
|
if (decision.action === 'start') {
|
|
393
421
|
return spawnNetcage(['start', '-a', '-i', decision.ref]);
|
|
394
422
|
}
|
|
395
|
-
// A fresh `--keep` run:
|
|
396
|
-
|
|
397
|
-
return spawnNetcage(
|
|
398
|
-
withKeyLabel(plan.netcageArgs, keptContainerKey(intent)),
|
|
399
|
-
);
|
|
423
|
+
// A fresh `--keep` run: the RunPlan already omits --rm so it is left kept.
|
|
424
|
+
return spawnNetcage(keyed);
|
|
400
425
|
}
|
|
401
426
|
|
|
402
|
-
return spawnNetcage(
|
|
427
|
+
return spawnNetcage(keyed);
|
|
403
428
|
}
|
|
404
429
|
|
|
405
430
|
// --- the interactive host-side menu (the ONLY untested I/O) -------------------
|
|
@@ -1981,6 +2006,36 @@ function promptLine(prompt: string): string | undefined {
|
|
|
1981
2006
|
return line === '' ? undefined : line;
|
|
1982
2007
|
}
|
|
1983
2008
|
|
|
2009
|
+
/** The `forward` subcommand help. */
|
|
2010
|
+
const FORWARD_HELP = `anon-pi forward - open a host port onto a running anon-pi container's in-jail server
|
|
2011
|
+
|
|
2012
|
+
USAGE
|
|
2013
|
+
anon-pi forward [<project>] [--port <[hostPort:]jailPort>] [--bind <addr>] [-m <machine>]
|
|
2014
|
+
|
|
2015
|
+
<project> filter to a running container for THIS project (a numeric name is a
|
|
2016
|
+
project, never a port). Omitted => all running anon-pi containers.
|
|
2017
|
+
--port,-p the port to forward, host-first like docker/kubectl: 3001 (host
|
|
2018
|
+
3001 -> jail 3001) or 8080:3001 (host 8080 -> jail 3001). Omit to be
|
|
2019
|
+
shown the container's open ports and prompted (incl. a different
|
|
2020
|
+
host port). An explicit port may be one not open yet.
|
|
2021
|
+
--bind passed through to netcage (127.0.0.1 default, or 0.0.0.0 for LAN).
|
|
2022
|
+
-m the machine the container runs on (else the default machine).
|
|
2023
|
+
|
|
2024
|
+
Wraps \`netcage forward\` (netcage >= 0.9.0). If several containers match, you pick
|
|
2025
|
+
one (each row shows its open in-jail ports). The forward runs until Ctrl-C.
|
|
2026
|
+
`;
|
|
2027
|
+
|
|
2028
|
+
/** The `ports` subcommand help. */
|
|
2029
|
+
const PORTS_HELP = `anon-pi ports - list a running anon-pi container's open in-jail TCP listeners
|
|
2030
|
+
|
|
2031
|
+
USAGE
|
|
2032
|
+
anon-pi ports [<project>] [-m <machine>]
|
|
2033
|
+
|
|
2034
|
+
Wraps \`netcage ports --json\` (netcage >= 0.9.0), which reads the jail's
|
|
2035
|
+
/proc/net/tcp* image-independently (works even with no ss/netstat in the image).
|
|
2036
|
+
Use it to find which port to \`anon-pi forward\`.
|
|
2037
|
+
`;
|
|
2038
|
+
|
|
1984
2039
|
/** The `machine` subcommand help. */
|
|
1985
2040
|
const MACHINE_HELP = `anon-pi machine - manage machines (an image + a persistent host home)
|
|
1986
2041
|
|
|
@@ -2068,6 +2123,259 @@ function queryKeptContainers(): KeptContainer[] {
|
|
|
2068
2123
|
return out;
|
|
2069
2124
|
}
|
|
2070
2125
|
|
|
2126
|
+
// --- `forward` / `ports`: reach an in-jail server from the host --------------
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Query netcage for its RUNNING managed containers (no `-a`, so a stopped kept
|
|
2130
|
+
* container is excluded: forward/ports can only reach a live jail), surfacing
|
|
2131
|
+
* each one's stamped anon-pi key + a display name. Best-effort: [] on any
|
|
2132
|
+
* failure, so the caller reports "nothing running" cleanly.
|
|
2133
|
+
*/
|
|
2134
|
+
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;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Best-effort: the in-jail TCP listeners of a container via `netcage ports
|
|
2165
|
+
* <ref> --json` (netcage >= 0.9.0). [] on any failure (older netcage without
|
|
2166
|
+
* the verb, a parse miss), so the port hint is purely additive and never blocks
|
|
2167
|
+
* a forward.
|
|
2168
|
+
*/
|
|
2169
|
+
function queryNetcagePorts(ref: string): NetcageListener[] {
|
|
2170
|
+
const res = spawnSync('netcage', ['ports', ref, '--json'], {
|
|
2171
|
+
encoding: 'utf8',
|
|
2172
|
+
});
|
|
2173
|
+
if (res.error || res.status !== 0 || !res.stdout) return [];
|
|
2174
|
+
return parseNetcagePortsJson(res.stdout);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* Resolve the ONE running anon-pi container a forward/ports should act on:
|
|
2179
|
+
* filter the running managed containers by machine (+ project if given), then
|
|
2180
|
+
* 0 => error (nothing running), 1 => it, many => an arrow-key picker annotated
|
|
2181
|
+
* with each container's open in-jail ports. Returns undefined on no-match or a
|
|
2182
|
+
* cancelled pick (with the reason printed). TTY is required only for the picker.
|
|
2183
|
+
*/
|
|
2184
|
+
function resolveForwardTarget(
|
|
2185
|
+
machine: string,
|
|
2186
|
+
project: string | undefined,
|
|
2187
|
+
verb: string,
|
|
2188
|
+
): ManagedContainer | undefined {
|
|
2189
|
+
const running = queryRunningContainers();
|
|
2190
|
+
const matches = resolveManagedMatches({
|
|
2191
|
+
containers: running,
|
|
2192
|
+
machine,
|
|
2193
|
+
project,
|
|
2194
|
+
});
|
|
2195
|
+
if (matches.length === 0) {
|
|
2196
|
+
const scope =
|
|
2197
|
+
project !== undefined
|
|
2198
|
+
? `project ${JSON.stringify(project)} on machine ${JSON.stringify(machine)}`
|
|
2199
|
+
: `machine ${JSON.stringify(machine)}`;
|
|
2200
|
+
process.stderr.write(
|
|
2201
|
+
`anon-pi: no running anon-pi container for ${scope}. ` +
|
|
2202
|
+
`Start one first (e.g. \`anon-pi ${project ?? '<project>'}\`) and run its ` +
|
|
2203
|
+
`server, then \`anon-pi ${verb}\` again.\n`,
|
|
2204
|
+
);
|
|
2205
|
+
return undefined;
|
|
2206
|
+
}
|
|
2207
|
+
if (matches.length === 1) return matches[0];
|
|
2208
|
+
|
|
2209
|
+
// Many: pick one. Each row is annotated with the container's open ports (a
|
|
2210
|
+
// best-effort hint), so the user can tell the sessions apart.
|
|
2211
|
+
if (!process.stdin.isTTY) {
|
|
2212
|
+
process.stderr.write(
|
|
2213
|
+
`anon-pi: ${matches.length} running containers match; a terminal is needed to ` +
|
|
2214
|
+
`pick one. Narrow with a project (\`anon-pi ${verb} <project>\`) or -m <machine>.\n`,
|
|
2215
|
+
);
|
|
2216
|
+
return undefined;
|
|
2217
|
+
}
|
|
2218
|
+
const entries: MenuEntry[] = matches.map((c) => {
|
|
2219
|
+
const proj = keyProject(parseKeptKey(c.key));
|
|
2220
|
+
const label = proj === '' ? '(shell)' : proj;
|
|
2221
|
+
const hint = formatPortsHint(queryNetcagePorts(c.ref));
|
|
2222
|
+
return {
|
|
2223
|
+
kind: 'project',
|
|
2224
|
+
project: c.ref,
|
|
2225
|
+
label: `${label} [${c.name}] ${hint}`,
|
|
2226
|
+
};
|
|
2227
|
+
});
|
|
2228
|
+
const picked = select(entries, {
|
|
2229
|
+
header: `anon-pi: pick a container to ${verb} (\u2191/\u2193 move, Enter select, Ctrl-C quit)`,
|
|
2230
|
+
});
|
|
2231
|
+
if (picked === undefined) {
|
|
2232
|
+
process.stderr.write('anon-pi: cancelled; nothing forwarded.\n');
|
|
2233
|
+
return undefined;
|
|
2234
|
+
}
|
|
2235
|
+
return matches.find((c) => c.ref === picked.project);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* `anon-pi forward [<project>] [--port <[hostPort:]jailPort>] [--bind <addr>]
|
|
2240
|
+
* [-m <machine>]`: open a host->jail port on a running anon-pi container. Wraps
|
|
2241
|
+
* `netcage forward`. When --port is omitted, it lists the container's open
|
|
2242
|
+
* in-jail ports and prompts for the jail port + an optional different host port.
|
|
2243
|
+
*/
|
|
2244
|
+
function runForward(forwardArgs: string[]): number {
|
|
2245
|
+
if (forwardArgs.includes('--help') || forwardArgs.includes('-h')) {
|
|
2246
|
+
process.stdout.write(FORWARD_HELP);
|
|
2247
|
+
return 0;
|
|
2248
|
+
}
|
|
2249
|
+
if (!hasNetcage()) return netcageMissing();
|
|
2250
|
+
|
|
2251
|
+
let cmd;
|
|
2252
|
+
try {
|
|
2253
|
+
cmd = parseForwardArgs(forwardArgs);
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
return reportAnonPiError(e);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
const machine = resolveVerbMachine(cmd.machine, cmd.machineExplicit);
|
|
2259
|
+
const target = resolveForwardTarget(machine, cmd.project, 'forward');
|
|
2260
|
+
if (target === undefined) return 1;
|
|
2261
|
+
|
|
2262
|
+
// Resolve the port: --port wins; else prompt from the container's listeners.
|
|
2263
|
+
let portRaw: string;
|
|
2264
|
+
if (cmd.port !== undefined) {
|
|
2265
|
+
portRaw = cmd.port.raw;
|
|
2266
|
+
} else {
|
|
2267
|
+
const prompted = promptForwardPort(target.ref);
|
|
2268
|
+
if (prompted === undefined) return 1;
|
|
2269
|
+
portRaw = prompted;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
const argv = ['forward'];
|
|
2273
|
+
if (cmd.bind !== undefined) argv.push('--bind', cmd.bind);
|
|
2274
|
+
argv.push(target.ref, portRaw);
|
|
2275
|
+
process.stderr.write(
|
|
2276
|
+
`anon-pi: forwarding to ${target.name} (${portRaw}); Ctrl-C to stop\u2026\n`,
|
|
2277
|
+
);
|
|
2278
|
+
return spawnNetcage(argv);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Interactive port resolution when `--port` is omitted: show the container's
|
|
2283
|
+
* open in-jail listeners, prompt for the jail port (defaulting to the sole
|
|
2284
|
+
* obvious one), then ask whether to bind it on a DIFFERENT host port and let the
|
|
2285
|
+
* user type it. Returns the netcage port token (`<jail>` or `<host>:<jail>`), or
|
|
2286
|
+
* undefined on cancel / a bad entry (reported). Requires a TTY.
|
|
2287
|
+
*/
|
|
2288
|
+
function promptForwardPort(ref: string): string | undefined {
|
|
2289
|
+
if (!process.stdin.isTTY) {
|
|
2290
|
+
process.stderr.write(
|
|
2291
|
+
'anon-pi: no TTY. Pass the port explicitly, e.g. `anon-pi forward --port 3001`.\n',
|
|
2292
|
+
);
|
|
2293
|
+
return undefined;
|
|
2294
|
+
}
|
|
2295
|
+
const listeners = queryNetcagePorts(ref);
|
|
2296
|
+
const open = forwardablePorts(listeners);
|
|
2297
|
+
process.stderr.write(` in-jail listeners: ${formatPortsHint(listeners)}\n`);
|
|
2298
|
+
|
|
2299
|
+
const def = open.length === 1 ? String(open[0]) : undefined;
|
|
2300
|
+
const jailAns = promptLine(
|
|
2301
|
+
` jail port to forward${def ? ` [${def}]` : ''}: `,
|
|
2302
|
+
);
|
|
2303
|
+
const jailStr =
|
|
2304
|
+
jailAns === undefined || jailAns.trim() === '' ? def : jailAns.trim();
|
|
2305
|
+
if (jailStr === undefined) {
|
|
2306
|
+
process.stderr.write('anon-pi: no port given; nothing forwarded.\n');
|
|
2307
|
+
return undefined;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
const hostAns = promptLine(
|
|
2311
|
+
` host port (Enter for same as jail, or type a different one): `,
|
|
2312
|
+
);
|
|
2313
|
+
const hostStr = hostAns === undefined ? '' : hostAns.trim();
|
|
2314
|
+
const token = hostStr === '' ? jailStr : `${hostStr}:${jailStr}`;
|
|
2315
|
+
try {
|
|
2316
|
+
return parsePortArg(token).raw; // validate 1..65535 + shape
|
|
2317
|
+
} catch (e) {
|
|
2318
|
+
reportAnonPiError(e);
|
|
2319
|
+
return undefined;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* `anon-pi ports [<project>] [-m <machine>]`: list a running anon-pi container's
|
|
2325
|
+
* open in-jail TCP listeners (via `netcage ports --json`), image-independent.
|
|
2326
|
+
* Disambiguates the same way as forward.
|
|
2327
|
+
*/
|
|
2328
|
+
function runPorts(portsArgs: string[]): number {
|
|
2329
|
+
if (portsArgs.includes('--help') || portsArgs.includes('-h')) {
|
|
2330
|
+
process.stdout.write(PORTS_HELP);
|
|
2331
|
+
return 0;
|
|
2332
|
+
}
|
|
2333
|
+
if (!hasNetcage()) return netcageMissing();
|
|
2334
|
+
|
|
2335
|
+
let cmd;
|
|
2336
|
+
try {
|
|
2337
|
+
cmd = parsePortsArgs(portsArgs);
|
|
2338
|
+
} catch (e) {
|
|
2339
|
+
return reportAnonPiError(e);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
const machine = resolveVerbMachine(cmd.machine, cmd.machineExplicit);
|
|
2343
|
+
const target = resolveForwardTarget(machine, cmd.project, 'ports');
|
|
2344
|
+
if (target === undefined) return 1;
|
|
2345
|
+
|
|
2346
|
+
const listeners = queryNetcagePorts(target.ref);
|
|
2347
|
+
if (listeners.length === 0) {
|
|
2348
|
+
process.stdout.write(
|
|
2349
|
+
`${target.name}: no in-jail TCP listeners detected ` +
|
|
2350
|
+
`(the server may not be up yet, or netcage < 0.9.0).\n`,
|
|
2351
|
+
);
|
|
2352
|
+
return 0;
|
|
2353
|
+
}
|
|
2354
|
+
process.stdout.write(`${target.name}: in-jail TCP listeners\n`);
|
|
2355
|
+
for (const l of listeners) {
|
|
2356
|
+
const scope = l.loopbackOnly ? 'loopback' : 'all-interfaces';
|
|
2357
|
+
const note = l.port === 53 && l.loopbackOnly ? ' (netcage DNS)' : '';
|
|
2358
|
+
process.stdout.write(` ${l.address}:${l.port}\t${scope}${note}\n`);
|
|
2359
|
+
}
|
|
2360
|
+
return 0;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/** Resolve the machine a verb targets: explicit -m wins, else config.defaultMachine. */
|
|
2364
|
+
function resolveVerbMachine(machine: string, explicit: boolean): string {
|
|
2365
|
+
if (explicit) return machine;
|
|
2366
|
+
const config = readJsonConfig(envFromProcess(process.env));
|
|
2367
|
+
return config.defaultMachine ?? DEFAULT_MACHINE;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/** The shared "netcage not on PATH" error (exit 1). */
|
|
2371
|
+
function netcageMissing(): number {
|
|
2372
|
+
process.stderr.write(
|
|
2373
|
+
'anon-pi: `netcage` not found on PATH. anon-pi is a launcher for netcage; install it first\n' +
|
|
2374
|
+
'(https://github.com/wighawag/netcage). Linux only.\n',
|
|
2375
|
+
);
|
|
2376
|
+
return 1;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2071
2379
|
/**
|
|
2072
2380
|
* Pull the anon-pi key out of a podman `{{.Labels}}` rendering (a
|
|
2073
2381
|
* comma-separated `k=v` list). The key is stamped as `anon-pi.key=<opaque>`;
|