@braedonsaunders/appkit-desk 0.5.1 → 0.7.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 +28 -1
- package/backend.d.ts +43 -1
- package/backend.d.ts.map +1 -1
- package/backend.js +110 -1
- package/backend.js.map +1 -1
- package/events.d.ts +3 -0
- package/events.d.ts.map +1 -1
- package/events.js.map +1 -1
- package/host.d.ts +53 -0
- package/host.d.ts.map +1 -1
- package/host.js +205 -12
- package/host.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/network.d.ts +37 -0
- package/network.d.ts.map +1 -0
- package/network.js +173 -0
- package/network.js.map +1 -0
- package/package.json +1 -1
- package/plan.d.ts +21 -0
- package/plan.d.ts.map +1 -1
- package/plan.js +58 -17
- package/plan.js.map +1 -1
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,QAAQ,CAAA;AACtB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA;AACzB,cAAc,QAAQ,CAAA;AACtB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA","sourcesContent":["export * from './events'\nexport * from './protocol'\nexport * from './plan'\nexport * from './guest-agent'\nexport * from './backend'\nexport * from './host'\nexport * from './frames'\nexport * from './portable-home'\nexport * from './conformance'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,QAAQ,CAAA;AACtB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA;AACzB,cAAc,QAAQ,CAAA;AACtB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA","sourcesContent":["export * from './events'\nexport * from './protocol'\nexport * from './plan'\nexport * from './guest-agent'\nexport * from './backend'\nexport * from './host'\nexport * from './frames'\nexport * from './portable-home'\nexport * from './conformance'\nexport * from './network'\n"]}
|
package/network.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DeskHandle } from './host.js';
|
|
2
|
+
export interface DeskNetworkCommandRunner {
|
|
3
|
+
run(command: string, args: readonly string[]): Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
export interface DeskNetworkOptions {
|
|
6
|
+
gateway: string;
|
|
7
|
+
dns: string;
|
|
8
|
+
egress: {
|
|
9
|
+
mode: 'proxied';
|
|
10
|
+
httpProxyPort: number;
|
|
11
|
+
httpsProxyPort: number;
|
|
12
|
+
} | {
|
|
13
|
+
mode: 'open';
|
|
14
|
+
};
|
|
15
|
+
chainPrefix?: string;
|
|
16
|
+
allowHostPorts?: readonly number[];
|
|
17
|
+
prefixLength?: number;
|
|
18
|
+
runner?: DeskNetworkCommandRunner;
|
|
19
|
+
enableForwarding?: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface DeskNetworkAddress {
|
|
22
|
+
tap: string;
|
|
23
|
+
guest: string;
|
|
24
|
+
}
|
|
25
|
+
/** Deterministic TAP and locally administered MAC for an unsigned desk index. */
|
|
26
|
+
export declare function deskNetworkIdentity(index: number): {
|
|
27
|
+
tap: string;
|
|
28
|
+
macAddress: string;
|
|
29
|
+
};
|
|
30
|
+
/** Point-to-point TAP with either open or transparent-proxy egress. */
|
|
31
|
+
export declare function createDeskNetwork(options: DeskNetworkOptions): {
|
|
32
|
+
ensure(value: DeskNetworkAddress): Promise<void>;
|
|
33
|
+
teardown(value: DeskNetworkAddress): Promise<void>;
|
|
34
|
+
configureGuest(desk: Pick<DeskHandle, "exec">, value: DeskNetworkAddress): Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
export declare function syncDeskGuestClock(desk: Pick<DeskHandle, 'exec'>, now?: number): Promise<void>;
|
|
37
|
+
//# sourceMappingURL=network.d.ts.map
|
package/network.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAExC,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/D;AACD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7F,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,wBAAwB,CAAA;IACjC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AACD,MAAM,WAAW,kBAAkB;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAsBlE,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAItF;AAID,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB;kBAkE3C,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;oBAoBhC,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;yBAYvB,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;EAOjG;AACD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,GAAG,SAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxG"}
|
package/network.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const defaultRunner = {
|
|
7
|
+
async run(command, args) {
|
|
8
|
+
try {
|
|
9
|
+
return (await execFileAsync(command, [...args], { timeout: 15_000 })).stdout;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const failure = error;
|
|
13
|
+
throw new Error(`${command} ${args.join(' ')}: ${(failure.stderr || failure.message || String(error)).trim()}`);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
async function forwarding() {
|
|
18
|
+
const path = '/proc/sys/net/ipv4/ip_forward';
|
|
19
|
+
if ((await readFile(path, 'utf8').catch(() => '')).trim() !== '1')
|
|
20
|
+
await writeFile(path, '1');
|
|
21
|
+
}
|
|
22
|
+
function port(value) {
|
|
23
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535)
|
|
24
|
+
throw new Error('Invalid TCP port');
|
|
25
|
+
}
|
|
26
|
+
function address(value) {
|
|
27
|
+
if (isIP(value) !== 4)
|
|
28
|
+
throw new Error(`Invalid IPv4 address: ${value}`);
|
|
29
|
+
}
|
|
30
|
+
/** Deterministic TAP and locally administered MAC for an unsigned desk index. */
|
|
31
|
+
export function deskNetworkIdentity(index) {
|
|
32
|
+
if (!Number.isInteger(index) || index < 2 || index > 999999999)
|
|
33
|
+
throw new Error('Invalid desk network index');
|
|
34
|
+
const octets = [6, 0, (index >>> 24) & 255, (index >>> 16) & 255, (index >>> 8) & 255, index & 255];
|
|
35
|
+
return { tap: `dsk${index}`, macAddress: octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':') };
|
|
36
|
+
}
|
|
37
|
+
/** Point-to-point TAP with either open or transparent-proxy egress. */
|
|
38
|
+
export function createDeskNetwork(options) {
|
|
39
|
+
address(options.gateway);
|
|
40
|
+
address(options.dns);
|
|
41
|
+
const egress = options.egress;
|
|
42
|
+
if (egress.mode === 'proxied') {
|
|
43
|
+
port(egress.httpProxyPort);
|
|
44
|
+
port(egress.httpsProxyPort);
|
|
45
|
+
}
|
|
46
|
+
else if (egress.mode !== 'open')
|
|
47
|
+
throw new Error('Invalid egress mode');
|
|
48
|
+
const chainPrefix = options.chainPrefix ?? 'desk';
|
|
49
|
+
if (!/^[A-Za-z0-9_-]+$/.test(chainPrefix))
|
|
50
|
+
throw new Error('Invalid iptables chain prefix');
|
|
51
|
+
const hostPorts = [...(options.allowHostPorts ?? [])];
|
|
52
|
+
hostPorts.forEach(port);
|
|
53
|
+
const prefix = options.prefixLength ?? 24;
|
|
54
|
+
if (!Number.isInteger(prefix) || prefix < 1 || prefix > 32)
|
|
55
|
+
throw new Error('Invalid guest prefix length');
|
|
56
|
+
const runner = options.runner ?? defaultRunner;
|
|
57
|
+
const enableForwarding = options.enableForwarding ?? forwarding;
|
|
58
|
+
const work = new Map();
|
|
59
|
+
const run = (command, args) => runner.run(command, args);
|
|
60
|
+
const succeeds = async (command, args) => { try {
|
|
61
|
+
await run(command, args);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
} };
|
|
67
|
+
function validate(value) {
|
|
68
|
+
if (!/^dsk[0-9]{1,9}$/.test(value.tap))
|
|
69
|
+
throw new Error('Invalid TAP name');
|
|
70
|
+
if (`${chainPrefix}-post-${value.tap}`.length > 28)
|
|
71
|
+
throw new Error('iptables chain name exceeds 28 characters');
|
|
72
|
+
address(value.guest);
|
|
73
|
+
if (value.guest === options.gateway)
|
|
74
|
+
throw new Error('Guest and gateway addresses must differ');
|
|
75
|
+
}
|
|
76
|
+
function chains({ tap, guest }) {
|
|
77
|
+
const { gateway, dns } = options;
|
|
78
|
+
const proxyRules = egress.mode === 'proxied' ? [
|
|
79
|
+
['-p', 'tcp', '--dport', '80', '-j', 'DNAT', '--to-destination', `${gateway}:${egress.httpProxyPort}`],
|
|
80
|
+
['-p', 'tcp', '--dport', '443', '-j', 'DNAT', '--to-destination', `${gateway}:${egress.httpsProxyPort}`],
|
|
81
|
+
...['udp', 'tcp'].map((p) => ['-p', p, '--dport', '53', '-j', 'DNAT', '--to-destination', `${dns}:53`]),
|
|
82
|
+
] : [];
|
|
83
|
+
const proxyPorts = egress.mode === 'proxied'
|
|
84
|
+
? [egress.httpProxyPort, egress.httpsProxyPort].map((p) => ['-p', 'tcp', '-d', gateway, '--dport', String(p), '-j', 'ACCEPT'])
|
|
85
|
+
: [];
|
|
86
|
+
const extraPorts = hostPorts.flatMap((p) => ['tcp', 'udp'].map((protocol) => ['-p', protocol, '-d', gateway, '--dport', String(p), '-j', 'ACCEPT']));
|
|
87
|
+
const forwardRules = egress.mode === 'proxied' ? [
|
|
88
|
+
['-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'],
|
|
89
|
+
...['udp', 'tcp'].map((p) => ['-i', tap, '-p', p, '-d', dns, '--dport', '53', '-j', 'ACCEPT']),
|
|
90
|
+
['-j', 'DROP'],
|
|
91
|
+
] : [
|
|
92
|
+
['-o', tap, '-d', `${guest}/32`, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'],
|
|
93
|
+
['-i', tap, '-s', `${guest}/32`, '-j', 'ACCEPT'],
|
|
94
|
+
['-j', 'DROP'],
|
|
95
|
+
];
|
|
96
|
+
return [
|
|
97
|
+
{ table: 'nat', parent: 'PREROUTING', chain: `${chainPrefix}-pre-${tap}`, jumps: [['-i', tap]], rules: proxyRules },
|
|
98
|
+
{ table: 'nat', parent: 'POSTROUTING', chain: `${chainPrefix}-post-${tap}`, jumps: [['-s', `${guest}/32`]], rules: [['-j', 'MASQUERADE']] },
|
|
99
|
+
{ table: 'filter', parent: 'INPUT', chain: `${chainPrefix}-in-${tap}`, jumps: [['-i', tap]], rules: [
|
|
100
|
+
['-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'],
|
|
101
|
+
...proxyPorts, ...extraPorts, ['-j', 'DROP'],
|
|
102
|
+
] },
|
|
103
|
+
{ table: 'filter', parent: 'FORWARD', chain: `${chainPrefix}-fwd-${tap}`, jumps: [['-i', tap], ['-o', tap]], rules: forwardRules },
|
|
104
|
+
...(egress.mode === 'open' ? [
|
|
105
|
+
{ command: 'ip6tables', table: 'filter', parent: 'INPUT', chain: `${chainPrefix}-6in-${tap}`, jumps: [['-i', tap]], rules: [['-j', 'DROP']] },
|
|
106
|
+
{ command: 'ip6tables', table: 'filter', parent: 'FORWARD', chain: `${chainPrefix}-6fwd-${tap}`, jumps: [['-i', tap], ['-o', tap]], rules: [['-j', 'DROP']] },
|
|
107
|
+
] : []),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
function serialize(tap, fn) {
|
|
111
|
+
const next = (work.get(tap) ?? Promise.resolve()).then(fn);
|
|
112
|
+
const settled = next.then(() => undefined, () => undefined);
|
|
113
|
+
work.set(tap, settled);
|
|
114
|
+
void settled.then(() => { if (work.get(tap) === settled)
|
|
115
|
+
work.delete(tap); });
|
|
116
|
+
return next;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ensure(value) {
|
|
120
|
+
validate(value);
|
|
121
|
+
return serialize(value.tap, async () => {
|
|
122
|
+
const { tap, guest } = value;
|
|
123
|
+
if (!(await succeeds('ip', ['link', 'show', 'dev', tap])))
|
|
124
|
+
await run('ip', ['tuntap', 'add', 'dev', tap, 'mode', 'tap']);
|
|
125
|
+
await run('ip', ['addr', 'replace', `${options.gateway}/32`, 'dev', tap]);
|
|
126
|
+
await run('ip', ['link', 'set', 'dev', tap, 'up']);
|
|
127
|
+
await run('ip', ['route', 'replace', `${guest}/32`, 'dev', tap]);
|
|
128
|
+
await enableForwarding();
|
|
129
|
+
for (const { table, parent, chain, jumps, rules, command = 'iptables' } of chains(value)) {
|
|
130
|
+
await succeeds(command, ['-t', table, '-N', chain]);
|
|
131
|
+
await run(command, ['-t', table, '-F', chain]);
|
|
132
|
+
for (const rule of rules)
|
|
133
|
+
await run(command, ['-t', table, '-A', chain, ...rule]);
|
|
134
|
+
for (const jump of jumps) {
|
|
135
|
+
const target = [...jump, '-j', chain];
|
|
136
|
+
if (!(await succeeds(command, ['-t', table, '-C', parent, ...target])))
|
|
137
|
+
await run(command, ['-t', table, '-I', parent, '1', ...target]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
teardown(value) {
|
|
143
|
+
validate(value);
|
|
144
|
+
return serialize(value.tap, async () => {
|
|
145
|
+
for (const { table, parent, chain, jumps, command = 'iptables' } of chains(value)) {
|
|
146
|
+
for (const jump of jumps)
|
|
147
|
+
while (await succeeds(command, ['-t', table, '-C', parent, ...jump, '-j', chain])) {
|
|
148
|
+
if (!(await succeeds(command, ['-t', table, '-D', parent, ...jump, '-j', chain])))
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
await succeeds(command, ['-t', table, '-F', chain]);
|
|
152
|
+
await succeeds(command, ['-t', table, '-X', chain]);
|
|
153
|
+
}
|
|
154
|
+
await succeeds('ip', ['link', 'del', 'dev', value.tap]);
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
async configureGuest(desk, value) {
|
|
158
|
+
validate(value);
|
|
159
|
+
const script = ['set -e', 'dev=$(ls /sys/class/net | grep -E "^(en|eth)" | head -n 1)', 'test -n "$dev"', `ip addr replace ${value.guest}/${prefix} dev "$dev"`, 'ip link set dev "$dev" up', `ip route replace default via ${options.gateway} dev "$dev"`, 'rm -f /etc/resolv.conf', `printf 'nameserver %s\\n' ${options.dns} > /etc/resolv.conf`].join('\n');
|
|
160
|
+
const result = await desk.exec({ command: '/bin/sh', args: ['-c', script], timeoutMs: 20_000 });
|
|
161
|
+
if (result.exitCode !== 0)
|
|
162
|
+
throw new Error(`The guest's network could not be configured: ${result.stderr.trim() || 'no reason given'}`);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export async function syncDeskGuestClock(desk, now = Date.now()) {
|
|
167
|
+
if (!Number.isFinite(now) || now < 0)
|
|
168
|
+
throw new Error('Invalid clock time');
|
|
169
|
+
const result = await desk.exec({ command: '/bin/sh', args: ['-c', `date -u -s @${(now / 1000).toFixed(3)} > /dev/null`], timeoutMs: 5_000 });
|
|
170
|
+
if (result.exitCode !== 0)
|
|
171
|
+
throw new Error(`The guest's clock could not be set: ${result.stderr.trim() || 'no reason given'}`);
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=network.js.map
|
package/network.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from 'node:child_process'\nimport { readFile, writeFile } from 'node:fs/promises'\nimport { isIP } from 'node:net'\nimport { promisify } from 'node:util'\nimport type { DeskHandle } from './host'\n\nexport interface DeskNetworkCommandRunner {\n run(command: string, args: readonly string[]): Promise<string>\n}\nexport interface DeskNetworkOptions {\n gateway: string\n dns: string\n egress: { mode: 'proxied'; httpProxyPort: number; httpsProxyPort: number } | { mode: 'open' }\n chainPrefix?: string\n allowHostPorts?: readonly number[]\n prefixLength?: number\n runner?: DeskNetworkCommandRunner\n enableForwarding?: () => Promise<void>\n}\nexport interface DeskNetworkAddress { tap: string; guest: string }\nconst execFileAsync = promisify(execFile)\nconst defaultRunner: DeskNetworkCommandRunner = {\n async run(command, args) {\n try { return (await execFileAsync(command, [...args], { timeout: 15_000 })).stdout }\n catch (error) {\n const failure = error as { stderr?: string; message?: string }\n throw new Error(`${command} ${args.join(' ')}: ${(failure.stderr || failure.message || String(error)).trim()}`)\n }\n },\n}\nasync function forwarding(): Promise<void> {\n const path = '/proc/sys/net/ipv4/ip_forward'\n if ((await readFile(path, 'utf8').catch(() => '')).trim() !== '1') await writeFile(path, '1')\n}\nfunction port(value: number): void {\n if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error('Invalid TCP port')\n}\nfunction address(value: string): void {\n if (isIP(value) !== 4) throw new Error(`Invalid IPv4 address: ${value}`)\n}\n\n/** Deterministic TAP and locally administered MAC for an unsigned desk index. */\nexport function deskNetworkIdentity(index: number): { tap: string; macAddress: string } {\n if (!Number.isInteger(index) || index < 2 || index > 999999999) throw new Error('Invalid desk network index')\n const octets = [6, 0, (index >>> 24) & 255, (index >>> 16) & 255, (index >>> 8) & 255, index & 255]\n return { tap: `dsk${index}`, macAddress: octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':') }\n}\n\ntype Rule = readonly string[]\ninterface Chain { table: 'nat' | 'filter'; parent: string; chain: string; jumps: Rule[]; rules: Rule[]; command?: 'ip6tables' }\n/** Point-to-point TAP with either open or transparent-proxy egress. */\nexport function createDeskNetwork(options: DeskNetworkOptions) {\n address(options.gateway); address(options.dns)\n const egress = options.egress\n if (egress.mode === 'proxied') { port(egress.httpProxyPort); port(egress.httpsProxyPort) }\n else if (egress.mode !== 'open') throw new Error('Invalid egress mode')\n const chainPrefix = options.chainPrefix ?? 'desk'\n if (!/^[A-Za-z0-9_-]+$/.test(chainPrefix)) throw new Error('Invalid iptables chain prefix')\n const hostPorts = [...(options.allowHostPorts ?? [])]\n hostPorts.forEach(port)\n const prefix = options.prefixLength ?? 24\n if (!Number.isInteger(prefix) || prefix < 1 || prefix > 32) throw new Error('Invalid guest prefix length')\n const runner = options.runner ?? defaultRunner\n const enableForwarding = options.enableForwarding ?? forwarding\n const work = new Map<string, Promise<void>>()\n const run = (command: string, args: readonly string[]) => runner.run(command, args)\n const succeeds = async (command: string, args: readonly string[]) => { try { await run(command, args); return true } catch { return false } }\n function validate(value: DeskNetworkAddress) {\n if (!/^dsk[0-9]{1,9}$/.test(value.tap)) throw new Error('Invalid TAP name')\n if (`${chainPrefix}-post-${value.tap}`.length > 28) throw new Error('iptables chain name exceeds 28 characters')\n address(value.guest)\n if (value.guest === options.gateway) throw new Error('Guest and gateway addresses must differ')\n }\n function chains({ tap, guest }: DeskNetworkAddress): Chain[] {\n const { gateway, dns } = options\n const proxyRules: Rule[] = egress.mode === 'proxied' ? [\n ['-p', 'tcp', '--dport', '80', '-j', 'DNAT', '--to-destination', `${gateway}:${egress.httpProxyPort}`],\n ['-p', 'tcp', '--dport', '443', '-j', 'DNAT', '--to-destination', `${gateway}:${egress.httpsProxyPort}`],\n ...(['udp', 'tcp'] as const).map((p) => ['-p', p, '--dport', '53', '-j', 'DNAT', '--to-destination', `${dns}:53`]),\n ] : []\n const proxyPorts: Rule[] = egress.mode === 'proxied'\n ? [egress.httpProxyPort, egress.httpsProxyPort].map((p) => ['-p', 'tcp', '-d', gateway, '--dport', String(p), '-j', 'ACCEPT'])\n : []\n const extraPorts: Rule[] = hostPorts.flatMap((p) => (['tcp', 'udp'] as const).map((protocol) =>\n ['-p', protocol, '-d', gateway, '--dport', String(p), '-j', 'ACCEPT']))\n const forwardRules: Rule[] = egress.mode === 'proxied' ? [\n ['-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'],\n ...(['udp', 'tcp'] as const).map((p) => ['-i', tap, '-p', p, '-d', dns, '--dport', '53', '-j', 'ACCEPT']),\n ['-j', 'DROP'],\n ] : [\n ['-o', tap, '-d', `${guest}/32`, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'],\n ['-i', tap, '-s', `${guest}/32`, '-j', 'ACCEPT'],\n ['-j', 'DROP'],\n ]\n return [\n { table: 'nat', parent: 'PREROUTING', chain: `${chainPrefix}-pre-${tap}`, jumps: [['-i', tap]], rules: proxyRules },\n { table: 'nat', parent: 'POSTROUTING', chain: `${chainPrefix}-post-${tap}`, jumps: [['-s', `${guest}/32`]], rules: [['-j', 'MASQUERADE']] },\n { table: 'filter', parent: 'INPUT', chain: `${chainPrefix}-in-${tap}`, jumps: [['-i', tap]], rules: [\n ['-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'],\n ...proxyPorts, ...extraPorts, ['-j', 'DROP'],\n ] },\n { table: 'filter', parent: 'FORWARD', chain: `${chainPrefix}-fwd-${tap}`, jumps: [['-i', tap], ['-o', tap]], rules: forwardRules },\n ...(egress.mode === 'open' ? [\n { command: 'ip6tables' as const, table: 'filter' as const, parent: 'INPUT', chain: `${chainPrefix}-6in-${tap}`, jumps: [['-i', tap]], rules: [['-j', 'DROP']] },\n { command: 'ip6tables' as const, table: 'filter' as const, parent: 'FORWARD', chain: `${chainPrefix}-6fwd-${tap}`, jumps: [['-i', tap], ['-o', tap]], rules: [['-j', 'DROP']] },\n ] : []),\n ]\n }\n\n function serialize(tap: string, fn: () => Promise<void>): Promise<void> {\n const next = (work.get(tap) ?? Promise.resolve()).then(fn)\n const settled = next.then(() => undefined, () => undefined)\n work.set(tap, settled)\n void settled.then(() => { if (work.get(tap) === settled) work.delete(tap) })\n return next\n }\n return {\n ensure(value: DeskNetworkAddress): Promise<void> {\n validate(value)\n return serialize(value.tap, async () => {\n const { tap, guest } = value\n if (!(await succeeds('ip', ['link', 'show', 'dev', tap]))) await run('ip', ['tuntap', 'add', 'dev', tap, 'mode', 'tap'])\n await run('ip', ['addr', 'replace', `${options.gateway}/32`, 'dev', tap])\n await run('ip', ['link', 'set', 'dev', tap, 'up'])\n await run('ip', ['route', 'replace', `${guest}/32`, 'dev', tap])\n await enableForwarding()\n for (const { table, parent, chain, jumps, rules, command = 'iptables' } of chains(value)) {\n await succeeds(command, ['-t', table, '-N', chain])\n await run(command, ['-t', table, '-F', chain])\n for (const rule of rules) await run(command, ['-t', table, '-A', chain, ...rule])\n for (const jump of jumps) {\n const target = [...jump, '-j', chain]\n if (!(await succeeds(command, ['-t', table, '-C', parent, ...target]))) await run(command, ['-t', table, '-I', parent, '1', ...target])\n }\n }\n })\n },\n teardown(value: DeskNetworkAddress): Promise<void> {\n validate(value)\n return serialize(value.tap, async () => {\n for (const { table, parent, chain, jumps, command = 'iptables' } of chains(value)) {\n for (const jump of jumps) while (await succeeds(command, ['-t', table, '-C', parent, ...jump, '-j', chain])) {\n if (!(await succeeds(command, ['-t', table, '-D', parent, ...jump, '-j', chain]))) break\n }\n await succeeds(command, ['-t', table, '-F', chain]); await succeeds(command, ['-t', table, '-X', chain])\n }\n await succeeds('ip', ['link', 'del', 'dev', value.tap])\n })\n },\n async configureGuest(desk: Pick<DeskHandle, 'exec'>, value: DeskNetworkAddress): Promise<void> {\n validate(value)\n const script = ['set -e', 'dev=$(ls /sys/class/net | grep -E \"^(en|eth)\" | head -n 1)', 'test -n \"$dev\"', `ip addr replace ${value.guest}/${prefix} dev \"$dev\"`, 'ip link set dev \"$dev\" up', `ip route replace default via ${options.gateway} dev \"$dev\"`, 'rm -f /etc/resolv.conf', `printf 'nameserver %s\\\\n' ${options.dns} > /etc/resolv.conf`].join('\\n')\n const result = await desk.exec({ command: '/bin/sh', args: ['-c', script], timeoutMs: 20_000 })\n if (result.exitCode !== 0) throw new Error(`The guest's network could not be configured: ${result.stderr.trim() || 'no reason given'}`)\n },\n }\n}\nexport async function syncDeskGuestClock(desk: Pick<DeskHandle, 'exec'>, now = Date.now()): Promise<void> {\n if (!Number.isFinite(now) || now < 0) throw new Error('Invalid clock time')\n const result = await desk.exec({ command: '/bin/sh', args: ['-c', `date -u -s @${(now / 1000).toFixed(3)} > /dev/null`], timeoutMs: 5_000 })\n if (result.exitCode !== 0) throw new Error(`The guest's clock could not be set: ${result.stderr.trim() || 'no reason given'}`)\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@braedonsaunders/appkit-desk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Per-agent Debian microVMs under Cloud Hypervisor — headless machines with an on-demand screen, a framed vsock guest-agent protocol, leases with idle suspend and a bounded queue, and load-bearing handover masking behind consumer-supplied policy and recording ports.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
package/plan.d.ts
CHANGED
|
@@ -50,6 +50,22 @@ export interface DeskLaunchPlanOptions {
|
|
|
50
50
|
macAddress?: string;
|
|
51
51
|
kernelCmdline?: string;
|
|
52
52
|
launcherIdentity?: DeskLauncherIdentity;
|
|
53
|
+
/**
|
|
54
|
+
* Where this desk's memory snapshot lives. Defaults beside the overlay, which
|
|
55
|
+
* is the volume sized for desk state — a snapshot is roughly `memoryMb` of
|
|
56
|
+
* guest RAM, so it does not belong on a runtime tmpfs.
|
|
57
|
+
*/
|
|
58
|
+
snapshotDir?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Resume from that snapshot instead of booting the kernel.
|
|
61
|
+
*
|
|
62
|
+
* A restored VM continues with its process tree intact, which is the whole
|
|
63
|
+
* point of parking with memory rather than shutting down: an agent's in-guest
|
|
64
|
+
* daemon survives. Refused when the snapshot is not there, rather than falling
|
|
65
|
+
* back to a cold boot — a silent cold boot is how a desk loses the state it
|
|
66
|
+
* was promised to have kept.
|
|
67
|
+
*/
|
|
68
|
+
restore?: boolean;
|
|
53
69
|
}
|
|
54
70
|
/**
|
|
55
71
|
* The complete Cloud Hypervisor invocation as inspectable data. Nothing here
|
|
@@ -82,6 +98,11 @@ export interface DeskLaunchPlan {
|
|
|
82
98
|
device: string;
|
|
83
99
|
mac: string;
|
|
84
100
|
};
|
|
101
|
+
/** Where a memory snapshot of this desk is written, and read back from. */
|
|
102
|
+
snapshot: {
|
|
103
|
+
dir: string;
|
|
104
|
+
restoring: boolean;
|
|
105
|
+
};
|
|
85
106
|
memoryMb: number;
|
|
86
107
|
vcpus: number;
|
|
87
108
|
kernelPath: string;
|
package/plan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB,8BAA8B,CAAA;AAC3D,eAAO,MAAM,gBAAgB,aAAa,CAAA;AAC1C,eAAO,MAAM,qBAAqB,sBAAsB,CAAA;AACxD,eAAO,MAAM,mBAAmB,qBAAqB,CAAA;AACrD,eAAO,MAAM,iBAAiB,MAAM,CAAA;AACpC,eAAO,MAAM,aAAa,IAAI,CAAA;AAC9B,eAAO,MAAM,sBAAsB,2BAA2B,CAAA;AAS9D;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,SAAkB,IAAI,EAAE,MAAM,CAAc;CAC7C;AAED,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAA;IACX,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAA;IACrB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"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"}
|
package/plan.js
CHANGED
|
@@ -89,6 +89,19 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
|
|
|
89
89
|
const launcherIdentity = cleanLauncherIdentity(options.launcherIdentity);
|
|
90
90
|
const vsockSocketPath = join(runtimeDir, `desk-${deskId}.vsock`);
|
|
91
91
|
const apiSocketPath = join(runtimeDir, `desk-${deskId}.api.sock`);
|
|
92
|
+
// Beside the overlay by default: a snapshot is the guest's RAM, so it belongs
|
|
93
|
+
// on the volume provisioned for desk state rather than on a runtime tmpfs.
|
|
94
|
+
const snapshotDir = options.snapshotDir
|
|
95
|
+
? absolutePath(options.snapshotDir, 'snapshotDir')
|
|
96
|
+
: join(overlayDirectory, `${deskId}.snapshot`);
|
|
97
|
+
const restoring = options.restore === true;
|
|
98
|
+
if (restoring && !pathExists(snapshotDir)) {
|
|
99
|
+
// Refused rather than silently cold-booted. A caller asking to restore has
|
|
100
|
+
// been told this desk kept its memory; quietly booting a fresh kernel would
|
|
101
|
+
// lose the process tree it was relying on and look like a crash inside the
|
|
102
|
+
// guest instead of a missing snapshot out here.
|
|
103
|
+
throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`);
|
|
104
|
+
}
|
|
92
105
|
const create = pathExists(overlayPath)
|
|
93
106
|
? null
|
|
94
107
|
: {
|
|
@@ -101,23 +114,50 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
|
|
|
101
114
|
command: 'cp',
|
|
102
115
|
args: ['--reflink=auto', baseImagePath, overlayPath],
|
|
103
116
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// A restoring VMM is handed the snapshot and nothing else.
|
|
118
|
+
//
|
|
119
|
+
// The snapshot carries the whole machine config — memory size, cpus, disk,
|
|
120
|
+
// vsock, net — so passing `--kernel`/`--disk` alongside `--restore` is not
|
|
121
|
+
// redundant, it is rejected.
|
|
122
|
+
//
|
|
123
|
+
// `resume=false` is deliberate, and it is the difference between this being
|
|
124
|
+
// safe and being a data-loss bug. A memory snapshot is only valid against the
|
|
125
|
+
// disk as it stood when the snapshot was taken. Resume the guest in this same
|
|
126
|
+
// spawn and it starts writing immediately, so for as long as the snapshot
|
|
127
|
+
// still exists there are two valid-looking pasts for one desk — and a host
|
|
128
|
+
// that died in that window would, on the next start, replay this memory
|
|
129
|
+
// against a disk that had moved on. That is corruption, not a stale cache.
|
|
130
|
+
//
|
|
131
|
+
// So the guest comes back PAUSED, the caller destroys the snapshot while
|
|
132
|
+
// nothing can write, and only then is the VM resumed.
|
|
133
|
+
//
|
|
134
|
+
// The tap is reopened BY NAME from the snapshotted config. That is the detail
|
|
135
|
+
// that makes this possible at all: Cloud Hypervisor's `net_fds` restore
|
|
136
|
+
// parameter exists for VMs whose tap was passed as a file descriptor, and
|
|
137
|
+
// producing one from Node would mean an ioctl(TUNSETIFF) it cannot make.
|
|
138
|
+
// Because `deskPlan` configures `--net tap=<name>`, there is nothing to pass.
|
|
139
|
+
const args = restoring
|
|
140
|
+
? [
|
|
141
|
+
'--api-socket', apiSocketPath,
|
|
142
|
+
'--restore', `source_url=file://${snapshotDir},resume=false`,
|
|
143
|
+
]
|
|
144
|
+
: [
|
|
145
|
+
'--api-socket', apiSocketPath,
|
|
146
|
+
'--kernel', kernelPath,
|
|
147
|
+
// A modular distro kernel needs its initramfs to bring up virtio_blk and
|
|
148
|
+
// mount root; omitted only for a kernel with those drivers built in.
|
|
149
|
+
...(initramfsPath === null ? [] : ['--initramfs', initramfsPath]),
|
|
150
|
+
'--cmdline', kernelCmdline,
|
|
151
|
+
// Be explicit that the disk is raw: CH deprecated image-format auto-detection
|
|
152
|
+
// and warns without image_type; there is never a qcow2 overlay here.
|
|
153
|
+
'--disk', `path=${overlayPath},image_type=raw`,
|
|
154
|
+
'--memory', `size=${memoryMb}M`,
|
|
155
|
+
'--cpus', `boot=${vcpus}`,
|
|
156
|
+
'--vsock', `cid=${vsockCid},socket=${vsockSocketPath}`,
|
|
157
|
+
'--net', `tap=${tapDevice},mac=${macAddress}`,
|
|
158
|
+
'--serial', 'off',
|
|
159
|
+
'--console', 'off',
|
|
160
|
+
];
|
|
121
161
|
return {
|
|
122
162
|
deskId,
|
|
123
163
|
vmm: { command: vmmPath, args },
|
|
@@ -125,6 +165,7 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
|
|
|
125
165
|
vsock: { cid: vsockCid, socketPath: vsockSocketPath },
|
|
126
166
|
api: { socketPath: apiSocketPath },
|
|
127
167
|
tap: { device: tapDevice, mac: macAddress },
|
|
168
|
+
snapshot: { dir: snapshotDir, restoring },
|
|
128
169
|
memoryMb,
|
|
129
170
|
vcpus,
|
|
130
171
|
kernelPath,
|
package/plan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAE9D,MAAM,CAAC,MAAM,gBAAgB,GAAG,2BAA2B,CAAA;AAC3D,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAA;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,mBAAmB,CAAA;AACxD,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AACrD,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAA;AAC9B,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAA;AAE9D,MAAM,eAAe,GAAG,mCAAmC,CAAA;AAC3D,MAAM,kBAAkB,GAAG,kCAAkC,CAAA;AAC7D,MAAM,mBAAmB,GAAG,mCAAmC,CAAA;AAC/D,gEAAgE;AAChE,MAAM,aAAa,GAAG,CAAC,CAAA;AACvB,MAAM,aAAa,GAAG,WAAW,CAAA;AAEjC;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAChB,IAAI,GAAW,WAAW,CAAA;CAC7C;AAmED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,UAK5B,EAAE;IACJ,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAA;IACrD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,UAAU,CAAA;IACnD,OAAO,CACL,QAAQ,KAAK,OAAO;WACjB,UAAU,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;WAC/C,UAAU,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC,CACnD,CAAA;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CACjB,sEAAsE,CACvE,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAA8B,EAC9B,eAGI,EAAE;IAEN,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,IAAI,UAAU,CAAA;IACxD,MAAM,YAAY,GAAG,YAAY,CAAC,YAAY,IAAI,UAAU,CAAA;IAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1C,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,EAAE,SAAS,CAAC,CAAA;IAC5E,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,SAAS,CACjB,2BAA2B,OAAO,2CAA2C;cACzE,0CAA0C,CAC/C,CAAA;IACH,CAAC;IACD,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,EAAE,SAAS,CAAC,CAAA;IAC5E,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CACjB,qCAAqC,OAAO,kDAAkD,CAC/F,CAAA;IACH,CAAC;IACD,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;IACjE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,SAAS,CAAC,gCAAgC,UAAU,EAAE,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,aAAa,GACjB,OAAO,CAAC,aAAa,KAAK,SAAS;QACjC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;IAC1D,IAAI,aAAa,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,SAAS,CAAC,6BAA6B,aAAa,EAAE,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,aAAa,GAAG,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;IAC1E,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,SAAS,CAAC,8BAA8B,aAAa,EAAE,CAAC,CAAA;IACpE,CAAC;IACD,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;IACpE,MAAM,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7C,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,SAAS,CAAC,qCAAqC,gBAAgB,EAAE,CAAC,CAAA;IAC9E,CAAC;IACD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,IAAI,iBAAiB,EAAE,UAAU,CAAC,CAAA;IACnF,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,IAAI,aAAa,EAAE,OAAO,CAAC,CAAA;IACtE,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC3C,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,IAAI,mBAAmB,EAAE,YAAY,CAAC,CAAA;IACxF,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAA;IACjF,MAAM,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,UAAU,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAA;IACrF,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAA;IACrE,MAAM,gBAAgB,GAAG,qBAAqB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACxE,MAAM,eAAe,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,MAAM,QAAQ,CAAC,CAAA;IAChE,MAAM,aAAa,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,MAAM,WAAW,CAAC,CAAA;IAEjE,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC;QACpC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC;YACE,sEAAsE;YACtE,8DAA8D;YAC9D,sEAAsE;YACtE,yEAAyE;YACzE,wEAAwE;YACxE,0EAA0E;YAC1E,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,CAAC,gBAAgB,EAAE,aAAa,EAAE,WAAW,CAAC;SACrD,CAAA;IAEL,MAAM,IAAI,GAAG;QACX,cAAc,EAAE,aAAa;QAC7B,UAAU,EAAE,UAAU;QACtB,yEAAyE;QACzE,qEAAqE;QACrE,GAAG,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QACjE,WAAW,EAAE,aAAa;QAC1B,8EAA8E;QAC9E,qEAAqE;QACrE,QAAQ,EAAE,QAAQ,WAAW,iBAAiB;QAC9C,UAAU,EAAE,QAAQ,QAAQ,GAAG;QAC/B,QAAQ,EAAE,QAAQ,KAAK,EAAE;QACzB,SAAS,EAAE,OAAO,QAAQ,WAAW,eAAe,EAAE;QACtD,OAAO,EAAE,OAAO,SAAS,QAAQ,UAAU,EAAE;QAC7C,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,KAAK;KACnB,CAAA;IAED,OAAO;QACL,MAAM;QACN,GAAG,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;QAC/B,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE;QAC/D,KAAK,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE;QACrD,GAAG,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE;QAClC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE;QAC3C,QAAQ;QACR,KAAK;QACL,UAAU;QACV,aAAa;QACb,aAAa;QACb,gBAAgB;KACjB,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,MAAM,GAAG,EAAE,CAAA;AACpB,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAW;IACpC,6EAA6E;IAC7E,2EAA2E;IAC3E,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC,CAAA;IACrG,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC7E,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa;IAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,aAAa,IAAI,KAAK,GAAG,aAAa,EAAE,CAAC;QAC/E,MAAM,IAAI,SAAS,CACjB,uCAAuC,aAAa,QAAQ,aAAa,GAAG,CAC7E,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,iFAAiF,CAAC,CAAA;IACxG,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,SAAS,CAAC,0CAA0C,KAAK,EAAE,CAAC,CAAA;IACxE,CAAC;IACD,OAAO,KAAK,CAAC,WAAW,EAAE,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,QAA0C;IAE1C,IAAI,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC/B,MAAM,GAAG,GAAG,aAAa,CAAA;IACzB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;QAC9E,MAAM,IAAI,SAAS,CAAC,yDAAyD,GAAG,GAAG,CAAC,CAAA;IACtF,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;QAC9E,MAAM,IAAI,SAAS,CAAC,yDAAyD,GAAG,GAAG,CAAC,CAAA;IACtF,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAA;AACjD,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,KAAa;IAChD,IAAI,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,4BAA4B,CAAC,CAAA;IAC3D,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAA;AACvB,CAAC;AAED,SAAS,eAAe,CAAC,KAAa,EAAE,KAAa;IACnD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,8BAA8B,CAAC,CAAA;IAC7D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { existsSync } from 'node:fs'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\n\nexport const DEFAULT_VMM_PATH = '/usr/bin/cloud-hypervisor'\nexport const DEFAULT_KVM_PATH = '/dev/kvm'\nexport const DEFAULT_QEMU_IMG_PATH = '/usr/bin/qemu-img'\nexport const DEFAULT_RUNTIME_DIR = '/run/appkit-desk'\nexport const DEFAULT_MEMORY_MB = 384\nexport const DEFAULT_VCPUS = 2\nexport const DEFAULT_KERNEL_CMDLINE = 'root=/dev/vda rw quiet'\n\nconst DESK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/\nconst TAP_DEVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$/\nconst MAC_ADDRESS_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i\n/** vsock CIDs 0–2 are reserved (hypervisor, loopback, host). */\nconst MIN_GUEST_CID = 3\nconst MAX_GUEST_CID = 0xffff_fffe\n\n/**\n * Every failure this package raises. The `name` is annotated as `string`\n * rather than left to infer its literal so a subclass can narrow the meaning\n * of a failure — see `DeskRequestFateUnknownError` — without callers losing\n * the one `instanceof DeskError` they already check.\n */\nexport class DeskError extends Error {\n override readonly name: string = 'DeskError'\n}\n\nexport interface DeskLauncherIdentity {\n /** Host/container UID used to invoke the VMM and image tooling. */\n uid: number\n /** Host/container GID used to invoke the VMM and image tooling. */\n gid: number\n}\n\nexport interface DeskLaunchPlanOptions {\n deskId: string\n vmmPath?: string\n kvmPath?: string\n qemuImgPath?: string\n /** Absolute path of the kernel image the microVM boots. */\n kernelPath: string\n /**\n * Absolute path of an initramfs. Required whenever the kernel loads its\n * root-filesystem or virtio drivers as modules — a stock distro cloud kernel\n * panics with \"VFS: Unable to mount root fs\" if it boots without one. Omit\n * only for a kernel with virtio_blk/ext4 built in.\n */\n initramfsPath?: string\n /** Absolute path of the golden raw base image every desk clones. */\n baseImagePath: string\n /** Absolute path of this desk's copy-on-write overlay (a raw reflink clone). */\n overlayPath: string\n memoryMb?: number\n vcpus?: number\n /** Guest vsock context ID; must be unique among resident desks. */\n vsockCid: number\n /** Directory holding the vsock and API control sockets. */\n runtimeDir?: string\n /** Host TAP device name; derived from the CID when omitted. */\n tapDevice?: string\n /** Guest MAC address; derived from the CID when omitted. */\n macAddress?: string\n kernelCmdline?: string\n launcherIdentity?: DeskLauncherIdentity\n}\n\n/**\n * The complete Cloud Hypervisor invocation as inspectable data. Nothing here\n * has been spawned: tests, audit logs, and readiness screens can assert on the\n * exact argv a desk would boot with.\n */\nexport interface DeskLaunchPlan {\n deskId: string\n vmm: { command: string; args: string[] }\n overlay: {\n path: string\n basePath: string\n /** The overlay-creation step, or `null` when the overlay already exists. */\n create: { command: string; args: string[] } | null\n }\n vsock: { cid: number; socketPath: string }\n api: { socketPath: string }\n tap: { device: string; mac: string }\n memoryMb: number\n vcpus: number\n kernelPath: string\n /** Resolved initramfs path, or `null` when the kernel needs none. */\n initramfsPath: string | null\n kernelCmdline: string\n launcherIdentity?: DeskLauncherIdentity\n}\n\n/**\n * Whether this host can run desks at all: Linux, a usable `/dev/kvm`, and the\n * Cloud Hypervisor binary. All three checks are injectable and the answer is\n * fail-closed — a host missing any of them gets no desk capability rather\n * than a degraded one.\n */\nexport function isDeskSupported(options: {\n platform?: NodeJS.Platform\n kvmPath?: string\n vmmPath?: string\n pathExists?: (path: string) => boolean\n} = {}): boolean {\n const platform = options.platform ?? process.platform\n const pathExists = options.pathExists ?? existsSync\n return (\n platform === 'linux'\n && pathExists(options.kvmPath ?? DEFAULT_KVM_PATH)\n && pathExists(options.vmmPath ?? DEFAULT_VMM_PATH)\n )\n}\n\n/** Validate a desk identifier, which is used in socket paths and device names. */\nexport function cleanDeskId(value: string): string {\n if (!DESK_ID_PATTERN.test(value)) {\n throw new DeskError(\n 'deskId must be 1–64 letters, numbers, dots, underscores, or hyphens.',\n )\n }\n return value\n}\n\n/**\n * Build the deterministic Cloud Hypervisor launch plan without spawning\n * anything. Fail closed: a missing `/dev/kvm`, VMM binary, kernel, base image,\n * or overlay directory throws instead of producing a plan that cannot boot.\n */\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions,\n dependencies: {\n pathExists?: (path: string) => boolean\n deviceExists?: (path: string) => boolean\n } = {},\n): DeskLaunchPlan {\n const pathExists = dependencies.pathExists ?? existsSync\n const deviceExists = dependencies.deviceExists ?? existsSync\n const deskId = cleanDeskId(options.deskId)\n const kvmPath = absolutePath(options.kvmPath ?? DEFAULT_KVM_PATH, 'kvmPath')\n if (!deviceExists(kvmPath)) {\n throw new DeskError(\n `KVM is not available at ${kvmPath}. Desks require hardware virtualization; `\n + 'there is no software-emulation fallback.',\n )\n }\n const vmmPath = absolutePath(options.vmmPath ?? DEFAULT_VMM_PATH, 'vmmPath')\n if (!pathExists(vmmPath)) {\n throw new DeskError(\n `Cloud Hypervisor was not found at ${vmmPath}. Install cloud-hypervisor or configure vmmPath.`,\n )\n }\n const kernelPath = absolutePath(options.kernelPath, 'kernelPath')\n if (!pathExists(kernelPath)) {\n throw new DeskError(`Guest kernel does not exist: ${kernelPath}`)\n }\n const initramfsPath =\n options.initramfsPath === undefined\n ? null\n : absolutePath(options.initramfsPath, 'initramfsPath')\n if (initramfsPath !== null && !pathExists(initramfsPath)) {\n throw new DeskError(`Initramfs does not exist: ${initramfsPath}`)\n }\n const baseImagePath = absolutePath(options.baseImagePath, 'baseImagePath')\n if (!pathExists(baseImagePath)) {\n throw new DeskError(`Base image does not exist: ${baseImagePath}`)\n }\n const overlayPath = absolutePath(options.overlayPath, 'overlayPath')\n const overlayDirectory = dirname(overlayPath)\n if (!pathExists(overlayDirectory)) {\n throw new DeskError(`Overlay directory does not exist: ${overlayDirectory}`)\n }\n const memoryMb = positiveInteger(options.memoryMb ?? DEFAULT_MEMORY_MB, 'memoryMb')\n const vcpus = positiveInteger(options.vcpus ?? DEFAULT_VCPUS, 'vcpus')\n const vsockCid = cleanCid(options.vsockCid)\n const runtimeDir = absolutePath(options.runtimeDir ?? DEFAULT_RUNTIME_DIR, 'runtimeDir')\n const tapDevice = cleanTapDevice(options.tapDevice ?? defaultTapDevice(vsockCid))\n const macAddress = cleanMacAddress(options.macAddress ?? defaultMacAddress(vsockCid))\n const kernelCmdline = options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE\n const launcherIdentity = cleanLauncherIdentity(options.launcherIdentity)\n const vsockSocketPath = join(runtimeDir, `desk-${deskId}.vsock`)\n const apiSocketPath = join(runtimeDir, `desk-${deskId}.api.sock`)\n\n const create = pathExists(overlayPath)\n ? null\n : {\n // Cloud Hypervisor cannot follow disk backing chains: a qcow2 overlay\n // over a qcow2/raw base is rejected with UnsupportedFeature /\n // MaxNestingDepthExceeded. So the overlay is a plain raw file, cloned\n // from the raw base with `cp --reflink=auto` — an instant, block-sharing\n // copy-on-write clone on XFS/Btrfs and a graceful full-copy fallback on\n // ext4. Either way CH boots the resulting raw file with no backing chain.\n command: 'cp',\n args: ['--reflink=auto', baseImagePath, overlayPath],\n }\n\n const args = [\n '--api-socket', apiSocketPath,\n '--kernel', kernelPath,\n // A modular distro kernel needs its initramfs to bring up virtio_blk and\n // mount root; omitted only for a kernel with those drivers built in.\n ...(initramfsPath === null ? [] : ['--initramfs', initramfsPath]),\n '--cmdline', kernelCmdline,\n // Be explicit that the disk is raw: CH deprecated image-format auto-detection\n // and warns without image_type; there is never a qcow2 overlay here.\n '--disk', `path=${overlayPath},image_type=raw`,\n '--memory', `size=${memoryMb}M`,\n '--cpus', `boot=${vcpus}`,\n '--vsock', `cid=${vsockCid},socket=${vsockSocketPath}`,\n '--net', `tap=${tapDevice},mac=${macAddress}`,\n '--serial', 'off',\n '--console', 'off',\n ]\n\n return {\n deskId,\n vmm: { command: vmmPath, args },\n overlay: { path: overlayPath, basePath: baseImagePath, create },\n vsock: { cid: vsockCid, socketPath: vsockSocketPath },\n api: { socketPath: apiSocketPath },\n tap: { device: tapDevice, mac: macAddress },\n memoryMb,\n vcpus,\n kernelPath,\n initramfsPath,\n kernelCmdline,\n launcherIdentity,\n }\n}\n\nfunction defaultTapDevice(cid: number): string {\n return `dsk${cid}`\n}\n\nfunction defaultMacAddress(cid: number): string {\n // Locally administered, unicast prefix; the low four octets carry the CID so\n // the address is stable per desk and never collides across resident desks.\n const octets = [0x06, 0x00, (cid >>> 24) & 0xff, (cid >>> 16) & 0xff, (cid >>> 8) & 0xff, cid & 0xff]\n return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':')\n}\n\nfunction cleanCid(value: number): number {\n if (!Number.isInteger(value) || value < MIN_GUEST_CID || value > MAX_GUEST_CID) {\n throw new DeskError(\n `vsockCid must be an integer between ${MIN_GUEST_CID} and ${MAX_GUEST_CID}.`,\n )\n }\n return value\n}\n\nfunction cleanTapDevice(value: string): string {\n if (!TAP_DEVICE_PATTERN.test(value)) {\n throw new DeskError('tapDevice must be 1–15 characters of letters, numbers, underscores, or hyphens.')\n }\n return value\n}\n\nfunction cleanMacAddress(value: string): string {\n if (!MAC_ADDRESS_PATTERN.test(value)) {\n throw new DeskError(`macAddress is not a valid MAC address: ${value}`)\n }\n return value.toLowerCase()\n}\n\nexport function cleanLauncherIdentity(\n identity: DeskLauncherIdentity | undefined,\n): DeskLauncherIdentity | undefined {\n if (!identity) return undefined\n const max = 2_147_483_647\n if (!Number.isInteger(identity.uid) || identity.uid < 1 || identity.uid > max) {\n throw new DeskError(`launcherIdentity.uid must be an integer between 1 and ${max}.`)\n }\n if (!Number.isInteger(identity.gid) || identity.gid < 0 || identity.gid > max) {\n throw new DeskError(`launcherIdentity.gid must be an integer between 0 and ${max}.`)\n }\n return { uid: identity.uid, gid: identity.gid }\n}\n\nfunction absolutePath(value: string, field: string): string {\n if (!value || !isAbsolute(value)) {\n throw new DeskError(`${field} must be an absolute path.`)\n }\n return resolve(value)\n}\n\nfunction positiveInteger(value: number, field: string): number {\n if (!Number.isInteger(value) || value < 1) {\n throw new DeskError(`${field} must be a positive integer.`)\n }\n return value\n}\n"]}
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs'\nimport { dirname, isAbsolute, join, resolve } from 'node:path'\n\nexport const DEFAULT_VMM_PATH = '/usr/bin/cloud-hypervisor'\nexport const DEFAULT_KVM_PATH = '/dev/kvm'\nexport const DEFAULT_QEMU_IMG_PATH = '/usr/bin/qemu-img'\nexport const DEFAULT_RUNTIME_DIR = '/run/appkit-desk'\nexport const DEFAULT_MEMORY_MB = 384\nexport const DEFAULT_VCPUS = 2\nexport const DEFAULT_KERNEL_CMDLINE = 'root=/dev/vda rw quiet'\n\nconst DESK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/\nconst TAP_DEVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$/\nconst MAC_ADDRESS_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i\n/** vsock CIDs 0–2 are reserved (hypervisor, loopback, host). */\nconst MIN_GUEST_CID = 3\nconst MAX_GUEST_CID = 0xffff_fffe\n\n/**\n * Every failure this package raises. The `name` is annotated as `string`\n * rather than left to infer its literal so a subclass can narrow the meaning\n * of a failure — see `DeskRequestFateUnknownError` — without callers losing\n * the one `instanceof DeskError` they already check.\n */\nexport class DeskError extends Error {\n override readonly name: string = 'DeskError'\n}\n\nexport interface DeskLauncherIdentity {\n /** Host/container UID used to invoke the VMM and image tooling. */\n uid: number\n /** Host/container GID used to invoke the VMM and image tooling. */\n gid: number\n}\n\nexport interface DeskLaunchPlanOptions {\n deskId: string\n vmmPath?: string\n kvmPath?: string\n qemuImgPath?: string\n /** Absolute path of the kernel image the microVM boots. */\n kernelPath: string\n /**\n * Absolute path of an initramfs. Required whenever the kernel loads its\n * root-filesystem or virtio drivers as modules — a stock distro cloud kernel\n * panics with \"VFS: Unable to mount root fs\" if it boots without one. Omit\n * only for a kernel with virtio_blk/ext4 built in.\n */\n initramfsPath?: string\n /** Absolute path of the golden raw base image every desk clones. */\n baseImagePath: string\n /** Absolute path of this desk's copy-on-write overlay (a raw reflink clone). */\n overlayPath: string\n memoryMb?: number\n vcpus?: number\n /** Guest vsock context ID; must be unique among resident desks. */\n vsockCid: number\n /** Directory holding the vsock and API control sockets. */\n runtimeDir?: string\n /** Host TAP device name; derived from the CID when omitted. */\n tapDevice?: string\n /** Guest MAC address; derived from the CID when omitted. */\n macAddress?: string\n kernelCmdline?: string\n launcherIdentity?: DeskLauncherIdentity\n /**\n * Where this desk's memory snapshot lives. Defaults beside the overlay, which\n * is the volume sized for desk state — a snapshot is roughly `memoryMb` of\n * guest RAM, so it does not belong on a runtime tmpfs.\n */\n snapshotDir?: string\n /**\n * Resume from that snapshot instead of booting the kernel.\n *\n * A restored VM continues with its process tree intact, which is the whole\n * point of parking with memory rather than shutting down: an agent's in-guest\n * daemon survives. Refused when the snapshot is not there, rather than falling\n * back to a cold boot — a silent cold boot is how a desk loses the state it\n * was promised to have kept.\n */\n restore?: boolean\n}\n\n/**\n * The complete Cloud Hypervisor invocation as inspectable data. Nothing here\n * has been spawned: tests, audit logs, and readiness screens can assert on the\n * exact argv a desk would boot with.\n */\nexport interface DeskLaunchPlan {\n deskId: string\n vmm: { command: string; args: string[] }\n overlay: {\n path: string\n basePath: string\n /** The overlay-creation step, or `null` when the overlay already exists. */\n create: { command: string; args: string[] } | null\n }\n vsock: { cid: number; socketPath: string }\n api: { socketPath: string }\n tap: { device: string; mac: string }\n /** Where a memory snapshot of this desk is written, and read back from. */\n snapshot: { dir: string; restoring: boolean }\n memoryMb: number\n vcpus: number\n kernelPath: string\n /** Resolved initramfs path, or `null` when the kernel needs none. */\n initramfsPath: string | null\n kernelCmdline: string\n launcherIdentity?: DeskLauncherIdentity\n}\n\n/**\n * Whether this host can run desks at all: Linux, a usable `/dev/kvm`, and the\n * Cloud Hypervisor binary. All three checks are injectable and the answer is\n * fail-closed — a host missing any of them gets no desk capability rather\n * than a degraded one.\n */\nexport function isDeskSupported(options: {\n platform?: NodeJS.Platform\n kvmPath?: string\n vmmPath?: string\n pathExists?: (path: string) => boolean\n} = {}): boolean {\n const platform = options.platform ?? process.platform\n const pathExists = options.pathExists ?? existsSync\n return (\n platform === 'linux'\n && pathExists(options.kvmPath ?? DEFAULT_KVM_PATH)\n && pathExists(options.vmmPath ?? DEFAULT_VMM_PATH)\n )\n}\n\n/** Validate a desk identifier, which is used in socket paths and device names. */\nexport function cleanDeskId(value: string): string {\n if (!DESK_ID_PATTERN.test(value)) {\n throw new DeskError(\n 'deskId must be 1–64 letters, numbers, dots, underscores, or hyphens.',\n )\n }\n return value\n}\n\n/**\n * Build the deterministic Cloud Hypervisor launch plan without spawning\n * anything. Fail closed: a missing `/dev/kvm`, VMM binary, kernel, base image,\n * or overlay directory throws instead of producing a plan that cannot boot.\n */\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions,\n dependencies: {\n pathExists?: (path: string) => boolean\n deviceExists?: (path: string) => boolean\n } = {},\n): DeskLaunchPlan {\n const pathExists = dependencies.pathExists ?? existsSync\n const deviceExists = dependencies.deviceExists ?? existsSync\n const deskId = cleanDeskId(options.deskId)\n const kvmPath = absolutePath(options.kvmPath ?? DEFAULT_KVM_PATH, 'kvmPath')\n if (!deviceExists(kvmPath)) {\n throw new DeskError(\n `KVM is not available at ${kvmPath}. Desks require hardware virtualization; `\n + 'there is no software-emulation fallback.',\n )\n }\n const vmmPath = absolutePath(options.vmmPath ?? DEFAULT_VMM_PATH, 'vmmPath')\n if (!pathExists(vmmPath)) {\n throw new DeskError(\n `Cloud Hypervisor was not found at ${vmmPath}. Install cloud-hypervisor or configure vmmPath.`,\n )\n }\n const kernelPath = absolutePath(options.kernelPath, 'kernelPath')\n if (!pathExists(kernelPath)) {\n throw new DeskError(`Guest kernel does not exist: ${kernelPath}`)\n }\n const initramfsPath =\n options.initramfsPath === undefined\n ? null\n : absolutePath(options.initramfsPath, 'initramfsPath')\n if (initramfsPath !== null && !pathExists(initramfsPath)) {\n throw new DeskError(`Initramfs does not exist: ${initramfsPath}`)\n }\n const baseImagePath = absolutePath(options.baseImagePath, 'baseImagePath')\n if (!pathExists(baseImagePath)) {\n throw new DeskError(`Base image does not exist: ${baseImagePath}`)\n }\n const overlayPath = absolutePath(options.overlayPath, 'overlayPath')\n const overlayDirectory = dirname(overlayPath)\n if (!pathExists(overlayDirectory)) {\n throw new DeskError(`Overlay directory does not exist: ${overlayDirectory}`)\n }\n const memoryMb = positiveInteger(options.memoryMb ?? DEFAULT_MEMORY_MB, 'memoryMb')\n const vcpus = positiveInteger(options.vcpus ?? DEFAULT_VCPUS, 'vcpus')\n const vsockCid = cleanCid(options.vsockCid)\n const runtimeDir = absolutePath(options.runtimeDir ?? DEFAULT_RUNTIME_DIR, 'runtimeDir')\n const tapDevice = cleanTapDevice(options.tapDevice ?? defaultTapDevice(vsockCid))\n const macAddress = cleanMacAddress(options.macAddress ?? defaultMacAddress(vsockCid))\n const kernelCmdline = options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE\n const launcherIdentity = cleanLauncherIdentity(options.launcherIdentity)\n const vsockSocketPath = join(runtimeDir, `desk-${deskId}.vsock`)\n const apiSocketPath = join(runtimeDir, `desk-${deskId}.api.sock`)\n // Beside the overlay by default: a snapshot is the guest's RAM, so it belongs\n // on the volume provisioned for desk state rather than on a runtime tmpfs.\n const snapshotDir = options.snapshotDir\n ? absolutePath(options.snapshotDir, 'snapshotDir')\n : join(overlayDirectory, `${deskId}.snapshot`)\n const restoring = options.restore === true\n if (restoring && !pathExists(snapshotDir)) {\n // Refused rather than silently cold-booted. A caller asking to restore has\n // been told this desk kept its memory; quietly booting a fresh kernel would\n // lose the process tree it was relying on and look like a crash inside the\n // guest instead of a missing snapshot out here.\n throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`)\n }\n\n const create = pathExists(overlayPath)\n ? null\n : {\n // Cloud Hypervisor cannot follow disk backing chains: a qcow2 overlay\n // over a qcow2/raw base is rejected with UnsupportedFeature /\n // MaxNestingDepthExceeded. So the overlay is a plain raw file, cloned\n // from the raw base with `cp --reflink=auto` — an instant, block-sharing\n // copy-on-write clone on XFS/Btrfs and a graceful full-copy fallback on\n // ext4. Either way CH boots the resulting raw file with no backing chain.\n command: 'cp',\n args: ['--reflink=auto', baseImagePath, overlayPath],\n }\n\n // A restoring VMM is handed the snapshot and nothing else.\n //\n // The snapshot carries the whole machine config — memory size, cpus, disk,\n // vsock, net — so passing `--kernel`/`--disk` alongside `--restore` is not\n // redundant, it is rejected.\n //\n // `resume=false` is deliberate, and it is the difference between this being\n // safe and being a data-loss bug. A memory snapshot is only valid against the\n // disk as it stood when the snapshot was taken. Resume the guest in this same\n // spawn and it starts writing immediately, so for as long as the snapshot\n // still exists there are two valid-looking pasts for one desk — and a host\n // that died in that window would, on the next start, replay this memory\n // against a disk that had moved on. That is corruption, not a stale cache.\n //\n // So the guest comes back PAUSED, the caller destroys the snapshot while\n // nothing can write, and only then is the VM resumed.\n //\n // The tap is reopened BY NAME from the snapshotted config. That is the detail\n // that makes this possible at all: Cloud Hypervisor's `net_fds` restore\n // parameter exists for VMs whose tap was passed as a file descriptor, and\n // producing one from Node would mean an ioctl(TUNSETIFF) it cannot make.\n // Because `deskPlan` configures `--net tap=<name>`, there is nothing to pass.\n const args = restoring\n ? [\n '--api-socket', apiSocketPath,\n '--restore', `source_url=file://${snapshotDir},resume=false`,\n ]\n : [\n '--api-socket', apiSocketPath,\n '--kernel', kernelPath,\n // A modular distro kernel needs its initramfs to bring up virtio_blk and\n // mount root; omitted only for a kernel with those drivers built in.\n ...(initramfsPath === null ? [] : ['--initramfs', initramfsPath]),\n '--cmdline', kernelCmdline,\n // Be explicit that the disk is raw: CH deprecated image-format auto-detection\n // and warns without image_type; there is never a qcow2 overlay here.\n '--disk', `path=${overlayPath},image_type=raw`,\n '--memory', `size=${memoryMb}M`,\n '--cpus', `boot=${vcpus}`,\n '--vsock', `cid=${vsockCid},socket=${vsockSocketPath}`,\n '--net', `tap=${tapDevice},mac=${macAddress}`,\n '--serial', 'off',\n '--console', 'off',\n ]\n\n return {\n deskId,\n vmm: { command: vmmPath, args },\n overlay: { path: overlayPath, basePath: baseImagePath, create },\n vsock: { cid: vsockCid, socketPath: vsockSocketPath },\n api: { socketPath: apiSocketPath },\n tap: { device: tapDevice, mac: macAddress },\n snapshot: { dir: snapshotDir, restoring },\n memoryMb,\n vcpus,\n kernelPath,\n initramfsPath,\n kernelCmdline,\n launcherIdentity,\n }\n}\n\nfunction defaultTapDevice(cid: number): string {\n return `dsk${cid}`\n}\n\nfunction defaultMacAddress(cid: number): string {\n // Locally administered, unicast prefix; the low four octets carry the CID so\n // the address is stable per desk and never collides across resident desks.\n const octets = [0x06, 0x00, (cid >>> 24) & 0xff, (cid >>> 16) & 0xff, (cid >>> 8) & 0xff, cid & 0xff]\n return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':')\n}\n\nfunction cleanCid(value: number): number {\n if (!Number.isInteger(value) || value < MIN_GUEST_CID || value > MAX_GUEST_CID) {\n throw new DeskError(\n `vsockCid must be an integer between ${MIN_GUEST_CID} and ${MAX_GUEST_CID}.`,\n )\n }\n return value\n}\n\nfunction cleanTapDevice(value: string): string {\n if (!TAP_DEVICE_PATTERN.test(value)) {\n throw new DeskError('tapDevice must be 1–15 characters of letters, numbers, underscores, or hyphens.')\n }\n return value\n}\n\nfunction cleanMacAddress(value: string): string {\n if (!MAC_ADDRESS_PATTERN.test(value)) {\n throw new DeskError(`macAddress is not a valid MAC address: ${value}`)\n }\n return value.toLowerCase()\n}\n\nexport function cleanLauncherIdentity(\n identity: DeskLauncherIdentity | undefined,\n): DeskLauncherIdentity | undefined {\n if (!identity) return undefined\n const max = 2_147_483_647\n if (!Number.isInteger(identity.uid) || identity.uid < 1 || identity.uid > max) {\n throw new DeskError(`launcherIdentity.uid must be an integer between 1 and ${max}.`)\n }\n if (!Number.isInteger(identity.gid) || identity.gid < 0 || identity.gid > max) {\n throw new DeskError(`launcherIdentity.gid must be an integer between 0 and ${max}.`)\n }\n return { uid: identity.uid, gid: identity.gid }\n}\n\nfunction absolutePath(value: string, field: string): string {\n if (!value || !isAbsolute(value)) {\n throw new DeskError(`${field} must be an absolute path.`)\n }\n return resolve(value)\n}\n\nfunction positiveInteger(value: number, field: string): number {\n if (!Number.isInteger(value) || value < 1) {\n throw new DeskError(`${field} must be a positive integer.`)\n }\n return value\n}\n"]}
|