@braedonsaunders/appkit-desk 0.6.0 → 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/index.d.ts CHANGED
@@ -7,4 +7,5 @@ export * from './host.js';
7
7
  export * from './frames.js';
8
8
  export * from './portable-home.js';
9
9
  export * from './conformance.js';
10
+ export * from './network.js';
10
11
  //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","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"}
1
+ {"version":3,"file":"index.d.ts","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"}
package/index.js CHANGED
@@ -7,4 +7,5 @@ export * from './host.js';
7
7
  export * from './frames.js';
8
8
  export * from './portable-home.js';
9
9
  export * from './conformance.js';
10
+ export * from './network.js';
10
11
  //# sourceMappingURL=index.js.map
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
@@ -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.6.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",