@braedonsaunders/appkit-desk 0.7.0 → 0.8.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 +55 -0
- package/backend.d.ts +28 -7
- package/backend.d.ts.map +1 -1
- package/backend.js +18 -521
- package/backend.js.map +1 -1
- package/host.d.ts +4 -1
- package/host.d.ts.map +1 -1
- package/host.js +31 -16
- package/host.js.map +1 -1
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/machine.d.ts +145 -0
- package/machine.d.ts.map +1 -0
- package/machine.js +518 -0
- package/machine.js.map +1 -0
- package/package.json +2 -2
- package/plan.d.ts +129 -31
- package/plan.d.ts.map +1 -1
- package/plan.js +165 -9
- package/plan.js.map +1 -1
- package/port-relay.d.ts +13 -0
- package/port-relay.d.ts.map +1 -0
- package/port-relay.js +56 -0
- package/port-relay.js.map +1 -0
- package/virtualization.d.ts +16 -0
- package/virtualization.d.ts.map +1 -0
- package/virtualization.js +148 -0
- package/virtualization.js.map +1 -0
- package/vz/Package.swift +23 -0
- package/vz/Sources/DeskNet/Addresses.swift +106 -0
- package/vz/Sources/DeskNet/DHCP.swift +111 -0
- package/vz/Sources/DeskNet/Fence.swift +57 -0
- package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
- package/vz/Sources/DeskNet/Packets.swift +355 -0
- package/vz/Sources/DeskNet/Plumbing.swift +312 -0
- package/vz/Sources/DeskNet/Sockets.swift +221 -0
- package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
- package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
- package/vz/Sources/appkit-desk-vz/Helper.swift +505 -0
- package/vz/Sources/appkit-desk-vz/main.swift +38 -0
- package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
- package/vz/appkit-desk-vz.entitlements +8 -0
- package/vz/build.sh +23 -0
- package/vz/check/extract-boot.py +221 -0
package/plan.d.ts
CHANGED
|
@@ -5,6 +5,17 @@ export declare const DEFAULT_RUNTIME_DIR = "/run/appkit-desk";
|
|
|
5
5
|
export declare const DEFAULT_MEMORY_MB = 384;
|
|
6
6
|
export declare const DEFAULT_VCPUS = 2;
|
|
7
7
|
export declare const DEFAULT_KERNEL_CMDLINE = "root=/dev/vda rw quiet";
|
|
8
|
+
/**
|
|
9
|
+
* The guest network Virtualization.framework desks get from the helper's NAT:
|
|
10
|
+
* the same addresses QEMU's user networking uses, so they are unsurprising in
|
|
11
|
+
* a guest. Nothing outside the helper ever sees them.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_VZ_GUEST_ADDRESS = "10.0.2.15";
|
|
14
|
+
export declare const DEFAULT_VZ_GATEWAY_ADDRESS = "10.0.2.2";
|
|
15
|
+
export declare const DEFAULT_VZ_PREFIX_LENGTH = 24;
|
|
16
|
+
export declare const DEFAULT_VZ_DNS: readonly string[];
|
|
17
|
+
/** Virtualization.framework always gives the guest vsock context ID 3. */
|
|
18
|
+
export declare const VZ_GUEST_CID = 3;
|
|
8
19
|
/**
|
|
9
20
|
* Every failure this package raises. The `name` is annotated as `string`
|
|
10
21
|
* rather than left to infer its literal so a subclass can narrow the meaning
|
|
@@ -20,9 +31,35 @@ export interface DeskLauncherIdentity {
|
|
|
20
31
|
/** Host/container GID used to invoke the VMM and image tooling. */
|
|
21
32
|
gid: number;
|
|
22
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Which hypervisor a plan is for: Cloud Hypervisor over KVM on Linux, or
|
|
36
|
+
* Apple's Virtualization.framework through the `appkit-desk-vz` helper on an
|
|
37
|
+
* Apple silicon Mac.
|
|
38
|
+
*/
|
|
39
|
+
export type DeskHypervisor = 'cloud-hypervisor' | 'virtualization';
|
|
40
|
+
/**
|
|
41
|
+
* The guest network of a Virtualization.framework desk. The helper runs a
|
|
42
|
+
* userspace NAT: the guest reaches the public internet and nothing else unless
|
|
43
|
+
* `allowLan` is set — no RFC 1918 space, link-local, CGNAT, or the Mac itself.
|
|
44
|
+
*/
|
|
45
|
+
export interface DeskGuestNetworkOptions {
|
|
46
|
+
/** Let the guest reach the Mac's LAN, link-local and CGNAT space, and the Mac. */
|
|
47
|
+
allowLan?: boolean;
|
|
48
|
+
/** The address the helper's DHCP server leases the guest. */
|
|
49
|
+
guestAddress?: string;
|
|
50
|
+
/** The helper's own address on the link: the guest's router. */
|
|
51
|
+
gatewayAddress?: string;
|
|
52
|
+
prefixLength?: number;
|
|
53
|
+
/** Resolvers the guest is handed; the first also answers DNS sent to the gateway. */
|
|
54
|
+
dns?: readonly string[];
|
|
55
|
+
}
|
|
23
56
|
export interface DeskLaunchPlanOptions {
|
|
24
57
|
deskId: string;
|
|
58
|
+
/** Defaults to `cloud-hypervisor`. */
|
|
59
|
+
hypervisor?: DeskHypervisor;
|
|
60
|
+
/** Cloud Hypervisor only: the VMM binary. */
|
|
25
61
|
vmmPath?: string;
|
|
62
|
+
/** Cloud Hypervisor only: the KVM device. */
|
|
26
63
|
kvmPath?: string;
|
|
27
64
|
qemuImgPath?: string;
|
|
28
65
|
/** Absolute path of the kernel image the microVM boots. */
|
|
@@ -40,14 +77,27 @@ export interface DeskLaunchPlanOptions {
|
|
|
40
77
|
overlayPath: string;
|
|
41
78
|
memoryMb?: number;
|
|
42
79
|
vcpus?: number;
|
|
43
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Guest vsock context ID; must be unique among resident desks. Cloud
|
|
82
|
+
* Hypervisor only — a Virtualization.framework guest is always CID 3, and
|
|
83
|
+
* its vsock is private to its helper.
|
|
84
|
+
*/
|
|
44
85
|
vsockCid: number;
|
|
45
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Directory holding the desk's control sockets. Defaults to
|
|
88
|
+
* `/run/appkit-desk` for Cloud Hypervisor and `<os tmpdir>/appkit-desk` for
|
|
89
|
+
* Virtualization.framework.
|
|
90
|
+
*/
|
|
46
91
|
runtimeDir?: string;
|
|
47
|
-
/**
|
|
92
|
+
/** Cloud Hypervisor only: host TAP device name; derived from the CID when omitted. */
|
|
48
93
|
tapDevice?: string;
|
|
49
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Guest MAC address. Derived from the CID for Cloud Hypervisor, and from the
|
|
96
|
+
* desk ID for Virtualization.framework, whose restore needs the same NIC.
|
|
97
|
+
*/
|
|
50
98
|
macAddress?: string;
|
|
99
|
+
/** Virtualization.framework only: the helper's NAT network and fence. */
|
|
100
|
+
guestNetwork?: DeskGuestNetworkOptions;
|
|
51
101
|
kernelCmdline?: string;
|
|
52
102
|
launcherIdentity?: DeskLauncherIdentity;
|
|
53
103
|
/**
|
|
@@ -67,17 +117,9 @@ export interface DeskLaunchPlanOptions {
|
|
|
67
117
|
*/
|
|
68
118
|
restore?: boolean;
|
|
69
119
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
* has been spawned: tests, audit logs, and readiness screens can assert on the
|
|
73
|
-
* exact argv a desk would boot with.
|
|
74
|
-
*/
|
|
75
|
-
export interface DeskLaunchPlan {
|
|
120
|
+
/** What every plan carries, whichever hypervisor runs it. */
|
|
121
|
+
export interface DeskLaunchPlanBase {
|
|
76
122
|
deskId: string;
|
|
77
|
-
vmm: {
|
|
78
|
-
command: string;
|
|
79
|
-
args: string[];
|
|
80
|
-
};
|
|
81
123
|
overlay: {
|
|
82
124
|
path: string;
|
|
83
125
|
basePath: string;
|
|
@@ -87,17 +129,15 @@ export interface DeskLaunchPlan {
|
|
|
87
129
|
args: string[];
|
|
88
130
|
} | null;
|
|
89
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* The host Unix socket the guest's vsock is reached through, with Cloud
|
|
134
|
+
* Hypervisor's hybrid-vsock preamble: write `CONNECT <port>\n`, read
|
|
135
|
+
* `OK <n>\n`, then the stream is the guest connection.
|
|
136
|
+
*/
|
|
90
137
|
vsock: {
|
|
91
138
|
cid: number;
|
|
92
139
|
socketPath: string;
|
|
93
140
|
};
|
|
94
|
-
api: {
|
|
95
|
-
socketPath: string;
|
|
96
|
-
};
|
|
97
|
-
tap: {
|
|
98
|
-
device: string;
|
|
99
|
-
mac: string;
|
|
100
|
-
};
|
|
101
141
|
/** Where a memory snapshot of this desk is written, and read back from. */
|
|
102
142
|
snapshot: {
|
|
103
143
|
dir: string;
|
|
@@ -112,27 +152,85 @@ export interface DeskLaunchPlan {
|
|
|
112
152
|
launcherIdentity?: DeskLauncherIdentity;
|
|
113
153
|
}
|
|
114
154
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
|
|
155
|
+
* The complete Cloud Hypervisor invocation as inspectable data. Nothing here
|
|
156
|
+
* has been spawned: tests, audit logs, and readiness screens can assert on the
|
|
157
|
+
* exact argv a desk would boot with.
|
|
158
|
+
*/
|
|
159
|
+
export interface CloudHypervisorLaunchPlan extends DeskLaunchPlanBase {
|
|
160
|
+
hypervisor: 'cloud-hypervisor';
|
|
161
|
+
vmm: {
|
|
162
|
+
command: string;
|
|
163
|
+
args: string[];
|
|
164
|
+
};
|
|
165
|
+
api: {
|
|
166
|
+
socketPath: string;
|
|
167
|
+
};
|
|
168
|
+
tap: {
|
|
169
|
+
device: string;
|
|
170
|
+
mac: string;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A Virtualization.framework desk as inspectable data: what the helper is
|
|
175
|
+
* asked to boot. The helper path belongs to the backend, not the plan.
|
|
176
|
+
*/
|
|
177
|
+
export interface VirtualizationLaunchPlan extends DeskLaunchPlanBase {
|
|
178
|
+
hypervisor: 'virtualization';
|
|
179
|
+
/**
|
|
180
|
+
* The host Unix socket guest TCP ports are reached through, with the same
|
|
181
|
+
* preamble as `vsock`: `CONNECT <tcp port>\n` answered by `OK <n>\n` once
|
|
182
|
+
* the guest accepts, or a close with nothing written when it refuses.
|
|
183
|
+
*/
|
|
184
|
+
ports: {
|
|
185
|
+
socketPath: string;
|
|
186
|
+
};
|
|
187
|
+
network: {
|
|
188
|
+
mac: string;
|
|
189
|
+
allowLan: boolean;
|
|
190
|
+
guestAddress: string;
|
|
191
|
+
gatewayAddress: string;
|
|
192
|
+
prefixLength: number;
|
|
193
|
+
dns: string[];
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export type DeskLaunchPlan = CloudHypervisorLaunchPlan | VirtualizationLaunchPlan;
|
|
197
|
+
/**
|
|
198
|
+
* Whether this host can run desks at all. On Linux: a usable `/dev/kvm` and
|
|
199
|
+
* the Cloud Hypervisor binary. On macOS: Apple silicon, macOS 14 or later (the
|
|
200
|
+
* first with Virtualization.framework save and restore), and an existing
|
|
201
|
+
* `appkit-desk-vz` helper at `helperPath`. Signature and entitlement are checked
|
|
202
|
+
* by macOS on execution, not by this probe. Every check is injectable and the
|
|
203
|
+
* answer is fail-closed — a host missing any of them gets no desk capability
|
|
204
|
+
* rather than a degraded one.
|
|
119
205
|
*/
|
|
120
206
|
export declare function isDeskSupported(options?: {
|
|
121
207
|
platform?: NodeJS.Platform;
|
|
122
208
|
kvmPath?: string;
|
|
123
209
|
vmmPath?: string;
|
|
210
|
+
/** macOS: the signed `appkit-desk-vz` helper. Required there. */
|
|
211
|
+
helperPath?: string;
|
|
212
|
+
arch?: string;
|
|
213
|
+
/** The kernel release, as `os.release()` reports it. */
|
|
214
|
+
osRelease?: string;
|
|
124
215
|
pathExists?: (path: string) => boolean;
|
|
125
216
|
}): boolean;
|
|
126
217
|
/** Validate a desk identifier, which is used in socket paths and device names. */
|
|
127
218
|
export declare function cleanDeskId(value: string): string;
|
|
219
|
+
export interface DeskLaunchPlanDependencies {
|
|
220
|
+
pathExists?: (path: string) => boolean;
|
|
221
|
+
deviceExists?: (path: string) => boolean;
|
|
222
|
+
}
|
|
128
223
|
/**
|
|
129
|
-
* Build the deterministic
|
|
130
|
-
*
|
|
224
|
+
* Build the deterministic launch plan without spawning anything. Fail closed:
|
|
225
|
+
* a missing `/dev/kvm` or VMM binary (Cloud Hypervisor), kernel, base image,
|
|
131
226
|
* or overlay directory throws instead of producing a plan that cannot boot.
|
|
132
227
|
*/
|
|
133
|
-
export declare function buildDeskLaunchPlan(options: DeskLaunchPlanOptions
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
228
|
+
export declare function buildDeskLaunchPlan(options: DeskLaunchPlanOptions & {
|
|
229
|
+
hypervisor: 'virtualization';
|
|
230
|
+
}, dependencies?: DeskLaunchPlanDependencies): VirtualizationLaunchPlan;
|
|
231
|
+
export declare function buildDeskLaunchPlan(options: DeskLaunchPlanOptions & {
|
|
232
|
+
hypervisor?: 'cloud-hypervisor';
|
|
233
|
+
}, dependencies?: DeskLaunchPlanDependencies): CloudHypervisorLaunchPlan;
|
|
234
|
+
export declare function buildDeskLaunchPlan(options: DeskLaunchPlanOptions, dependencies?: DeskLaunchPlanDependencies): DeskLaunchPlan;
|
|
137
235
|
export declare function cleanLauncherIdentity(identity: DeskLauncherIdentity | undefined): DeskLauncherIdentity | undefined;
|
|
138
236
|
//# sourceMappingURL=plan.d.ts.map
|
package/plan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAOA,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;AAC9D;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,cAAc,CAAA;AACnD,eAAO,MAAM,0BAA0B,aAAa,CAAA;AACpD,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA2B,CAAA;AACvE,0EAA0E;AAC1E,eAAO,MAAM,YAAY,IAAI,CAAA;AAa7B;;;;;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;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG,gBAAgB,CAAA;AAElE;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qFAAqF;IACrF,GAAG,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,sCAAsC;IACtC,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,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;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,YAAY,CAAC,EAAE,uBAAuB,CAAA;IACtC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,oBAAoB,CAAA;IACvC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,EAAE,MAAM,CAAA;QAChB,4EAA4E;QAC5E,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,GAAG,IAAI,CAAA;KACnD,CAAA;IACD;;;;OAIG;IACH,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,2EAA2E;IAC3E,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAA;IAC7C,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,qEAAqE;IACrE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,oBAAoB,CAAA;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,kBAAkB;IACnE,UAAU,EAAE,kBAAkB,CAAA;IAC9B,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IACxC,GAAG,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3B,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CACrC;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAyB,SAAQ,kBAAkB;IAClE,UAAU,EAAE,gBAAgB,CAAA;IAC5B;;;;OAIG;IACH,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7B,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAA;QACX,QAAQ,EAAE,OAAO,CAAA;QACjB,YAAY,EAAE,MAAM,CAAA;QACpB,cAAc,EAAE,MAAM,CAAA;QACtB,YAAY,EAAE,MAAM,CAAA;QACpB,GAAG,EAAE,MAAM,EAAE,CAAA;KACd,CAAA;CACF;AAED,MAAM,MAAM,cAAc,GAAG,yBAAyB,GAAG,wBAAwB,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAClC,GAAG,OAAO,CAmBf;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IACtC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CACzC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,qBAAqB,GAAG;IAAE,UAAU,EAAE,gBAAgB,CAAA;CAAE,EACjE,YAAY,CAAC,EAAE,0BAA0B,GACxC,wBAAwB,CAAA;AAC3B,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,qBAAqB,GAAG;IAAE,UAAU,CAAC,EAAE,kBAAkB,CAAA;CAAE,EACpE,YAAY,CAAC,EAAE,0BAA0B,GACxC,yBAAyB,CAAA;AAC5B,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,qBAAqB,EAC9B,YAAY,CAAC,EAAE,0BAA0B,GACxC,cAAc,CAAA;AAqUjB,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,oBAAoB,GAAG,SAAS,GACzC,oBAAoB,GAAG,SAAS,CAUlC"}
|
package/plan.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
1
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { isIP } from 'node:net';
|
|
5
|
+
import { arch as hostArch, release as hostRelease, tmpdir } from 'node:os';
|
|
2
6
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
7
|
export const DEFAULT_VMM_PATH = '/usr/bin/cloud-hypervisor';
|
|
4
8
|
export const DEFAULT_KVM_PATH = '/dev/kvm';
|
|
@@ -7,6 +11,21 @@ export const DEFAULT_RUNTIME_DIR = '/run/appkit-desk';
|
|
|
7
11
|
export const DEFAULT_MEMORY_MB = 384;
|
|
8
12
|
export const DEFAULT_VCPUS = 2;
|
|
9
13
|
export const DEFAULT_KERNEL_CMDLINE = 'root=/dev/vda rw quiet';
|
|
14
|
+
/**
|
|
15
|
+
* The guest network Virtualization.framework desks get from the helper's NAT:
|
|
16
|
+
* the same addresses QEMU's user networking uses, so they are unsurprising in
|
|
17
|
+
* a guest. Nothing outside the helper ever sees them.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_VZ_GUEST_ADDRESS = '10.0.2.15';
|
|
20
|
+
export const DEFAULT_VZ_GATEWAY_ADDRESS = '10.0.2.2';
|
|
21
|
+
export const DEFAULT_VZ_PREFIX_LENGTH = 24;
|
|
22
|
+
export const DEFAULT_VZ_DNS = ['1.1.1.1', '9.9.9.9'];
|
|
23
|
+
/** Virtualization.framework always gives the guest vsock context ID 3. */
|
|
24
|
+
export const VZ_GUEST_CID = 3;
|
|
25
|
+
/** The first Darwin release with Virtualization.framework save and restore (macOS 14). */
|
|
26
|
+
const MIN_DARWIN_MAJOR = 23;
|
|
27
|
+
/** `sun_path` holds 104 bytes on Darwin, including the terminator. */
|
|
28
|
+
const DARWIN_SOCKET_PATH_LIMIT = 103;
|
|
10
29
|
const DESK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
11
30
|
const TAP_DEVICE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,14}$/;
|
|
12
31
|
const MAC_ADDRESS_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i;
|
|
@@ -23,14 +42,26 @@ export class DeskError extends Error {
|
|
|
23
42
|
name = 'DeskError';
|
|
24
43
|
}
|
|
25
44
|
/**
|
|
26
|
-
* Whether this host can run desks at all
|
|
27
|
-
* Cloud Hypervisor binary.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
45
|
+
* Whether this host can run desks at all. On Linux: a usable `/dev/kvm` and
|
|
46
|
+
* the Cloud Hypervisor binary. On macOS: Apple silicon, macOS 14 or later (the
|
|
47
|
+
* first with Virtualization.framework save and restore), and an existing
|
|
48
|
+
* `appkit-desk-vz` helper at `helperPath`. Signature and entitlement are checked
|
|
49
|
+
* by macOS on execution, not by this probe. Every check is injectable and the
|
|
50
|
+
* answer is fail-closed — a host missing any of them gets no desk capability
|
|
51
|
+
* rather than a degraded one.
|
|
30
52
|
*/
|
|
31
53
|
export function isDeskSupported(options = {}) {
|
|
32
54
|
const platform = options.platform ?? process.platform;
|
|
33
55
|
const pathExists = options.pathExists ?? existsSync;
|
|
56
|
+
if (platform === 'darwin') {
|
|
57
|
+
const major = Number.parseInt((options.osRelease ?? hostRelease()).split('.')[0] ?? '', 10);
|
|
58
|
+
return ((options.arch ?? hostArch()) === 'arm64'
|
|
59
|
+
&& Number.isInteger(major)
|
|
60
|
+
&& major >= MIN_DARWIN_MAJOR
|
|
61
|
+
&& options.helperPath !== undefined
|
|
62
|
+
&& isAbsolute(options.helperPath)
|
|
63
|
+
&& pathExists(options.helperPath));
|
|
64
|
+
}
|
|
34
65
|
return (platform === 'linux'
|
|
35
66
|
&& pathExists(options.kvmPath ?? DEFAULT_KVM_PATH)
|
|
36
67
|
&& pathExists(options.vmmPath ?? DEFAULT_VMM_PATH));
|
|
@@ -42,12 +73,19 @@ export function cleanDeskId(value) {
|
|
|
42
73
|
}
|
|
43
74
|
return value;
|
|
44
75
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Build the deterministic Cloud Hypervisor launch plan without spawning
|
|
47
|
-
* anything. Fail closed: a missing `/dev/kvm`, VMM binary, kernel, base image,
|
|
48
|
-
* or overlay directory throws instead of producing a plan that cannot boot.
|
|
49
|
-
*/
|
|
50
76
|
export function buildDeskLaunchPlan(options, dependencies = {}) {
|
|
77
|
+
const hypervisor = options.hypervisor ?? 'cloud-hypervisor';
|
|
78
|
+
if (hypervisor === 'virtualization')
|
|
79
|
+
return buildVirtualizationPlan(options, dependencies);
|
|
80
|
+
if (hypervisor !== 'cloud-hypervisor') {
|
|
81
|
+
throw new DeskError(`Unknown hypervisor: ${String(hypervisor)}`);
|
|
82
|
+
}
|
|
83
|
+
if (options.guestNetwork !== undefined) {
|
|
84
|
+
throw new DeskError('guestNetwork applies to Virtualization.framework desks; a Cloud Hypervisor desk is networked with createDeskNetwork.');
|
|
85
|
+
}
|
|
86
|
+
return buildCloudHypervisorPlan(options, dependencies);
|
|
87
|
+
}
|
|
88
|
+
function buildCloudHypervisorPlan(options, dependencies) {
|
|
51
89
|
const pathExists = dependencies.pathExists ?? existsSync;
|
|
52
90
|
const deviceExists = dependencies.deviceExists ?? existsSync;
|
|
53
91
|
const deskId = cleanDeskId(options.deskId);
|
|
@@ -159,6 +197,7 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
|
|
|
159
197
|
'--console', 'off',
|
|
160
198
|
];
|
|
161
199
|
return {
|
|
200
|
+
hypervisor: 'cloud-hypervisor',
|
|
162
201
|
deskId,
|
|
163
202
|
vmm: { command: vmmPath, args },
|
|
164
203
|
overlay: { path: overlayPath, basePath: baseImagePath, create },
|
|
@@ -174,6 +213,123 @@ export function buildDeskLaunchPlan(options, dependencies = {}) {
|
|
|
174
213
|
launcherIdentity,
|
|
175
214
|
};
|
|
176
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* The Virtualization.framework plan. The same fail-closed checks on the guest
|
|
218
|
+
* assets; no KVM or VMM binary, because the backend owns the helper; and the
|
|
219
|
+
* overlay is an APFS clone (`cp -c`), which is to APFS what `--reflink=auto`
|
|
220
|
+
* is to XFS: instant and block-sharing. A volume that cannot clone must be
|
|
221
|
+
* replaced with one that supports it.
|
|
222
|
+
*/
|
|
223
|
+
function buildVirtualizationPlan(options, dependencies) {
|
|
224
|
+
const pathExists = dependencies.pathExists ?? existsSync;
|
|
225
|
+
const deskId = cleanDeskId(options.deskId);
|
|
226
|
+
if (options.tapDevice !== undefined) {
|
|
227
|
+
throw new DeskError('tapDevice applies to Cloud Hypervisor desks; a Virtualization.framework desk has no TAP.');
|
|
228
|
+
}
|
|
229
|
+
const kernelPath = absolutePath(options.kernelPath, 'kernelPath');
|
|
230
|
+
if (!pathExists(kernelPath)) {
|
|
231
|
+
throw new DeskError(`Guest kernel does not exist: ${kernelPath}`);
|
|
232
|
+
}
|
|
233
|
+
const initramfsPath = options.initramfsPath === undefined ? null : absolutePath(options.initramfsPath, 'initramfsPath');
|
|
234
|
+
if (initramfsPath !== null && !pathExists(initramfsPath)) {
|
|
235
|
+
throw new DeskError(`Initramfs does not exist: ${initramfsPath}`);
|
|
236
|
+
}
|
|
237
|
+
const baseImagePath = absolutePath(options.baseImagePath, 'baseImagePath');
|
|
238
|
+
if (!pathExists(baseImagePath)) {
|
|
239
|
+
throw new DeskError(`Base image does not exist: ${baseImagePath}`);
|
|
240
|
+
}
|
|
241
|
+
const overlayPath = absolutePath(options.overlayPath, 'overlayPath');
|
|
242
|
+
const overlayDirectory = dirname(overlayPath);
|
|
243
|
+
if (!pathExists(overlayDirectory)) {
|
|
244
|
+
throw new DeskError(`Overlay directory does not exist: ${overlayDirectory}`);
|
|
245
|
+
}
|
|
246
|
+
const memoryMb = positiveInteger(options.memoryMb ?? DEFAULT_MEMORY_MB, 'memoryMb');
|
|
247
|
+
const vcpus = positiveInteger(options.vcpus ?? DEFAULT_VCPUS, 'vcpus');
|
|
248
|
+
const runtimeDir = absolutePath(options.runtimeDir ?? join(tmpdir(), 'appkit-desk'), 'runtimeDir');
|
|
249
|
+
// Keep Darwin's small sockaddr_un limit even for a maximum-length desk ID.
|
|
250
|
+
const socketId = createHash('sha256').update(deskId).digest('hex').slice(0, 20);
|
|
251
|
+
const vsockSocketPath = socketPath(join(runtimeDir, `d-${socketId}.vsock`));
|
|
252
|
+
const portsSocketPath = socketPath(join(runtimeDir, `d-${socketId}.ports`));
|
|
253
|
+
const snapshotDir = options.snapshotDir
|
|
254
|
+
? absolutePath(options.snapshotDir, 'snapshotDir')
|
|
255
|
+
: join(overlayDirectory, `${deskId}.snapshot`);
|
|
256
|
+
const restoring = options.restore === true;
|
|
257
|
+
if (restoring && !pathExists(snapshotDir)) {
|
|
258
|
+
throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`);
|
|
259
|
+
}
|
|
260
|
+
const network = cleanGuestNetwork(options.guestNetwork ?? {});
|
|
261
|
+
return {
|
|
262
|
+
hypervisor: 'virtualization',
|
|
263
|
+
deskId,
|
|
264
|
+
overlay: {
|
|
265
|
+
path: overlayPath,
|
|
266
|
+
basePath: baseImagePath,
|
|
267
|
+
create: pathExists(overlayPath) ? null : { command: 'cp', args: ['-c', baseImagePath, overlayPath] },
|
|
268
|
+
},
|
|
269
|
+
vsock: { cid: VZ_GUEST_CID, socketPath: vsockSocketPath },
|
|
270
|
+
ports: { socketPath: portsSocketPath },
|
|
271
|
+
network: {
|
|
272
|
+
mac: cleanMacAddress(options.macAddress ?? deskMacAddress(deskId)),
|
|
273
|
+
...network,
|
|
274
|
+
},
|
|
275
|
+
snapshot: { dir: snapshotDir, restoring },
|
|
276
|
+
memoryMb,
|
|
277
|
+
vcpus,
|
|
278
|
+
kernelPath,
|
|
279
|
+
initramfsPath,
|
|
280
|
+
kernelCmdline: options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE,
|
|
281
|
+
launcherIdentity: cleanLauncherIdentity(options.launcherIdentity),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function cleanGuestNetwork(options) {
|
|
285
|
+
if (options.allowLan !== undefined && typeof options.allowLan !== 'boolean') {
|
|
286
|
+
throw new DeskError('guestNetwork.allowLan must be a boolean.');
|
|
287
|
+
}
|
|
288
|
+
const guestAddress = ipv4(options.guestAddress ?? DEFAULT_VZ_GUEST_ADDRESS, 'guestNetwork.guestAddress');
|
|
289
|
+
const gatewayAddress = ipv4(options.gatewayAddress ?? DEFAULT_VZ_GATEWAY_ADDRESS, 'guestNetwork.gatewayAddress');
|
|
290
|
+
const prefixLength = options.prefixLength ?? DEFAULT_VZ_PREFIX_LENGTH;
|
|
291
|
+
if (!Number.isInteger(prefixLength) || prefixLength < 8 || prefixLength > 30) {
|
|
292
|
+
throw new DeskError('guestNetwork.prefixLength must be an integer between 8 and 30.');
|
|
293
|
+
}
|
|
294
|
+
const mask = (0xffff_ffff << (32 - prefixLength)) >>> 0;
|
|
295
|
+
if (guestAddress === gatewayAddress
|
|
296
|
+
|| ((ipv4Number(guestAddress) & mask) >>> 0) !== ((ipv4Number(gatewayAddress) & mask) >>> 0)) {
|
|
297
|
+
throw new DeskError("guestNetwork.guestAddress must be a different address in the gateway's subnet.");
|
|
298
|
+
}
|
|
299
|
+
const dns = [...(options.dns ?? DEFAULT_VZ_DNS)];
|
|
300
|
+
if (dns.length < 1 || dns.length > 3) {
|
|
301
|
+
throw new DeskError('guestNetwork.dns must list one to three IPv4 resolvers.');
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
allowLan: options.allowLan ?? false,
|
|
305
|
+
guestAddress,
|
|
306
|
+
gatewayAddress,
|
|
307
|
+
prefixLength,
|
|
308
|
+
dns: dns.map((entry) => ipv4(entry, 'guestNetwork.dns')),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Stable per desk, so a restored guest finds the NIC it was saved with. */
|
|
312
|
+
function deskMacAddress(deskId) {
|
|
313
|
+
const digest = createHash('sha256').update(`appkit-desk:${deskId}`).digest();
|
|
314
|
+
const octets = [0x06, ...digest.subarray(0, 5)];
|
|
315
|
+
return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':');
|
|
316
|
+
}
|
|
317
|
+
function socketPath(path) {
|
|
318
|
+
if (Buffer.byteLength(path) > DARWIN_SOCKET_PATH_LIMIT) {
|
|
319
|
+
throw new DeskError(`Socket path ${path} is longer than the ${DARWIN_SOCKET_PATH_LIMIT} bytes macOS allows; `
|
|
320
|
+
+ 'use a shorter runtimeDir or deskId.');
|
|
321
|
+
}
|
|
322
|
+
return path;
|
|
323
|
+
}
|
|
324
|
+
function ipv4(value, field) {
|
|
325
|
+
if (typeof value !== 'string' || isIP(value) !== 4) {
|
|
326
|
+
throw new DeskError(`${field} must be an IPv4 address.`);
|
|
327
|
+
}
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
function ipv4Number(address) {
|
|
331
|
+
return address.split('.').reduce((value, octet) => ((value << 8) | Number(octet)) >>> 0, 0);
|
|
332
|
+
}
|
|
177
333
|
function defaultTapDevice(cid) {
|
|
178
334
|
return `dsk${cid}`;
|
|
179
335
|
}
|
package/plan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer'\nimport { createHash } from 'node:crypto'\nimport { existsSync } from 'node:fs'\nimport { isIP } from 'node:net'\nimport { arch as hostArch, release as hostRelease, tmpdir } from 'node:os'\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/**\n * The guest network Virtualization.framework desks get from the helper's NAT:\n * the same addresses QEMU's user networking uses, so they are unsurprising in\n * a guest. Nothing outside the helper ever sees them.\n */\nexport const DEFAULT_VZ_GUEST_ADDRESS = '10.0.2.15'\nexport const DEFAULT_VZ_GATEWAY_ADDRESS = '10.0.2.2'\nexport const DEFAULT_VZ_PREFIX_LENGTH = 24\nexport const DEFAULT_VZ_DNS: readonly string[] = ['1.1.1.1', '9.9.9.9']\n/** Virtualization.framework always gives the guest vsock context ID 3. */\nexport const VZ_GUEST_CID = 3\n/** The first Darwin release with Virtualization.framework save and restore (macOS 14). */\nconst MIN_DARWIN_MAJOR = 23\n/** `sun_path` holds 104 bytes on Darwin, including the terminator. */\nconst DARWIN_SOCKET_PATH_LIMIT = 103\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\n/**\n * Which hypervisor a plan is for: Cloud Hypervisor over KVM on Linux, or\n * Apple's Virtualization.framework through the `appkit-desk-vz` helper on an\n * Apple silicon Mac.\n */\nexport type DeskHypervisor = 'cloud-hypervisor' | 'virtualization'\n\n/**\n * The guest network of a Virtualization.framework desk. The helper runs a\n * userspace NAT: the guest reaches the public internet and nothing else unless\n * `allowLan` is set — no RFC 1918 space, link-local, CGNAT, or the Mac itself.\n */\nexport interface DeskGuestNetworkOptions {\n /** Let the guest reach the Mac's LAN, link-local and CGNAT space, and the Mac. */\n allowLan?: boolean\n /** The address the helper's DHCP server leases the guest. */\n guestAddress?: string\n /** The helper's own address on the link: the guest's router. */\n gatewayAddress?: string\n prefixLength?: number\n /** Resolvers the guest is handed; the first also answers DNS sent to the gateway. */\n dns?: readonly string[]\n}\n\nexport interface DeskLaunchPlanOptions {\n deskId: string\n /** Defaults to `cloud-hypervisor`. */\n hypervisor?: DeskHypervisor\n /** Cloud Hypervisor only: the VMM binary. */\n vmmPath?: string\n /** Cloud Hypervisor only: the KVM device. */\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 /**\n * Guest vsock context ID; must be unique among resident desks. Cloud\n * Hypervisor only — a Virtualization.framework guest is always CID 3, and\n * its vsock is private to its helper.\n */\n vsockCid: number\n /**\n * Directory holding the desk's control sockets. Defaults to\n * `/run/appkit-desk` for Cloud Hypervisor and `<os tmpdir>/appkit-desk` for\n * Virtualization.framework.\n */\n runtimeDir?: string\n /** Cloud Hypervisor only: host TAP device name; derived from the CID when omitted. */\n tapDevice?: string\n /**\n * Guest MAC address. Derived from the CID for Cloud Hypervisor, and from the\n * desk ID for Virtualization.framework, whose restore needs the same NIC.\n */\n macAddress?: string\n /** Virtualization.framework only: the helper's NAT network and fence. */\n guestNetwork?: DeskGuestNetworkOptions\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/** What every plan carries, whichever hypervisor runs it. */\nexport interface DeskLaunchPlanBase {\n deskId: 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 /**\n * The host Unix socket the guest's vsock is reached through, with Cloud\n * Hypervisor's hybrid-vsock preamble: write `CONNECT <port>\\n`, read\n * `OK <n>\\n`, then the stream is the guest connection.\n */\n vsock: { cid: number; socketPath: 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 * 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 CloudHypervisorLaunchPlan extends DeskLaunchPlanBase {\n hypervisor: 'cloud-hypervisor'\n vmm: { command: string; args: string[] }\n api: { socketPath: string }\n tap: { device: string; mac: string }\n}\n\n/**\n * A Virtualization.framework desk as inspectable data: what the helper is\n * asked to boot. The helper path belongs to the backend, not the plan.\n */\nexport interface VirtualizationLaunchPlan extends DeskLaunchPlanBase {\n hypervisor: 'virtualization'\n /**\n * The host Unix socket guest TCP ports are reached through, with the same\n * preamble as `vsock`: `CONNECT <tcp port>\\n` answered by `OK <n>\\n` once\n * the guest accepts, or a close with nothing written when it refuses.\n */\n ports: { socketPath: string }\n network: {\n mac: string\n allowLan: boolean\n guestAddress: string\n gatewayAddress: string\n prefixLength: number\n dns: string[]\n }\n}\n\nexport type DeskLaunchPlan = CloudHypervisorLaunchPlan | VirtualizationLaunchPlan\n\n/**\n * Whether this host can run desks at all. On Linux: a usable `/dev/kvm` and\n * the Cloud Hypervisor binary. On macOS: Apple silicon, macOS 14 or later (the\n * first with Virtualization.framework save and restore), and an existing\n * `appkit-desk-vz` helper at `helperPath`. Signature and entitlement are checked\n * by macOS on execution, not by this probe. Every check is injectable and the\n * answer is fail-closed — a host missing any of them gets no desk capability\n * rather than a degraded one.\n */\nexport function isDeskSupported(options: {\n platform?: NodeJS.Platform\n kvmPath?: string\n vmmPath?: string\n /** macOS: the signed `appkit-desk-vz` helper. Required there. */\n helperPath?: string\n arch?: string\n /** The kernel release, as `os.release()` reports it. */\n osRelease?: string\n pathExists?: (path: string) => boolean\n} = {}): boolean {\n const platform = options.platform ?? process.platform\n const pathExists = options.pathExists ?? existsSync\n if (platform === 'darwin') {\n const major = Number.parseInt((options.osRelease ?? hostRelease()).split('.')[0] ?? '', 10)\n return (\n (options.arch ?? hostArch()) === 'arm64'\n && Number.isInteger(major)\n && major >= MIN_DARWIN_MAJOR\n && options.helperPath !== undefined\n && isAbsolute(options.helperPath)\n && pathExists(options.helperPath)\n )\n }\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\nexport interface DeskLaunchPlanDependencies {\n pathExists?: (path: string) => boolean\n deviceExists?: (path: string) => boolean\n}\n\n/**\n * Build the deterministic launch plan without spawning anything. Fail closed:\n * a missing `/dev/kvm` or VMM binary (Cloud Hypervisor), kernel, base image,\n * or overlay directory throws instead of producing a plan that cannot boot.\n */\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions & { hypervisor: 'virtualization' },\n dependencies?: DeskLaunchPlanDependencies,\n): VirtualizationLaunchPlan\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions & { hypervisor?: 'cloud-hypervisor' },\n dependencies?: DeskLaunchPlanDependencies,\n): CloudHypervisorLaunchPlan\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions,\n dependencies?: DeskLaunchPlanDependencies,\n): DeskLaunchPlan\nexport function buildDeskLaunchPlan(\n options: DeskLaunchPlanOptions,\n dependencies: DeskLaunchPlanDependencies = {},\n): DeskLaunchPlan {\n const hypervisor = options.hypervisor ?? 'cloud-hypervisor'\n if (hypervisor === 'virtualization') return buildVirtualizationPlan(options, dependencies)\n if (hypervisor !== 'cloud-hypervisor') {\n throw new DeskError(`Unknown hypervisor: ${String(hypervisor)}`)\n }\n if (options.guestNetwork !== undefined) {\n throw new DeskError(\n 'guestNetwork applies to Virtualization.framework desks; a Cloud Hypervisor desk is networked with createDeskNetwork.',\n )\n }\n return buildCloudHypervisorPlan(options, dependencies)\n}\n\nfunction buildCloudHypervisorPlan(\n options: DeskLaunchPlanOptions,\n dependencies: DeskLaunchPlanDependencies,\n): CloudHypervisorLaunchPlan {\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 hypervisor: 'cloud-hypervisor',\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\n/**\n * The Virtualization.framework plan. The same fail-closed checks on the guest\n * assets; no KVM or VMM binary, because the backend owns the helper; and the\n * overlay is an APFS clone (`cp -c`), which is to APFS what `--reflink=auto`\n * is to XFS: instant and block-sharing. A volume that cannot clone must be\n * replaced with one that supports it.\n */\nfunction buildVirtualizationPlan(\n options: DeskLaunchPlanOptions,\n dependencies: DeskLaunchPlanDependencies,\n): VirtualizationLaunchPlan {\n const pathExists = dependencies.pathExists ?? existsSync\n const deskId = cleanDeskId(options.deskId)\n if (options.tapDevice !== undefined) {\n throw new DeskError('tapDevice applies to Cloud Hypervisor desks; a Virtualization.framework desk has no TAP.')\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 ? null : 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 runtimeDir = absolutePath(options.runtimeDir ?? join(tmpdir(), 'appkit-desk'), 'runtimeDir')\n // Keep Darwin's small sockaddr_un limit even for a maximum-length desk ID.\n const socketId = createHash('sha256').update(deskId).digest('hex').slice(0, 20)\n const vsockSocketPath = socketPath(join(runtimeDir, `d-${socketId}.vsock`))\n const portsSocketPath = socketPath(join(runtimeDir, `d-${socketId}.ports`))\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 throw new DeskError(`Desk ${deskId} has no snapshot to restore at ${snapshotDir}`)\n }\n const network = cleanGuestNetwork(options.guestNetwork ?? {})\n return {\n hypervisor: 'virtualization',\n deskId,\n overlay: {\n path: overlayPath,\n basePath: baseImagePath,\n create: pathExists(overlayPath) ? null : { command: 'cp', args: ['-c', baseImagePath, overlayPath] },\n },\n vsock: { cid: VZ_GUEST_CID, socketPath: vsockSocketPath },\n ports: { socketPath: portsSocketPath },\n network: {\n mac: cleanMacAddress(options.macAddress ?? deskMacAddress(deskId)),\n ...network,\n },\n snapshot: { dir: snapshotDir, restoring },\n memoryMb,\n vcpus,\n kernelPath,\n initramfsPath,\n kernelCmdline: options.kernelCmdline ?? DEFAULT_KERNEL_CMDLINE,\n launcherIdentity: cleanLauncherIdentity(options.launcherIdentity),\n }\n}\n\nfunction cleanGuestNetwork(\n options: DeskGuestNetworkOptions,\n): Omit<VirtualizationLaunchPlan['network'], 'mac'> {\n if (options.allowLan !== undefined && typeof options.allowLan !== 'boolean') {\n throw new DeskError('guestNetwork.allowLan must be a boolean.')\n }\n const guestAddress = ipv4(options.guestAddress ?? DEFAULT_VZ_GUEST_ADDRESS, 'guestNetwork.guestAddress')\n const gatewayAddress = ipv4(options.gatewayAddress ?? DEFAULT_VZ_GATEWAY_ADDRESS, 'guestNetwork.gatewayAddress')\n const prefixLength = options.prefixLength ?? DEFAULT_VZ_PREFIX_LENGTH\n if (!Number.isInteger(prefixLength) || prefixLength < 8 || prefixLength > 30) {\n throw new DeskError('guestNetwork.prefixLength must be an integer between 8 and 30.')\n }\n const mask = (0xffff_ffff << (32 - prefixLength)) >>> 0\n if (\n guestAddress === gatewayAddress\n || ((ipv4Number(guestAddress) & mask) >>> 0) !== ((ipv4Number(gatewayAddress) & mask) >>> 0)\n ) {\n throw new DeskError(\"guestNetwork.guestAddress must be a different address in the gateway's subnet.\")\n }\n const dns = [...(options.dns ?? DEFAULT_VZ_DNS)]\n if (dns.length < 1 || dns.length > 3) {\n throw new DeskError('guestNetwork.dns must list one to three IPv4 resolvers.')\n }\n return {\n allowLan: options.allowLan ?? false,\n guestAddress,\n gatewayAddress,\n prefixLength,\n dns: dns.map((entry) => ipv4(entry, 'guestNetwork.dns')),\n }\n}\n\n/** Stable per desk, so a restored guest finds the NIC it was saved with. */\nfunction deskMacAddress(deskId: string): string {\n const digest = createHash('sha256').update(`appkit-desk:${deskId}`).digest()\n const octets = [0x06, ...digest.subarray(0, 5)]\n return octets.map((octet) => octet.toString(16).padStart(2, '0')).join(':')\n}\n\nfunction socketPath(path: string): string {\n if (Buffer.byteLength(path) > DARWIN_SOCKET_PATH_LIMIT) {\n throw new DeskError(\n `Socket path ${path} is longer than the ${DARWIN_SOCKET_PATH_LIMIT} bytes macOS allows; `\n + 'use a shorter runtimeDir or deskId.',\n )\n }\n return path\n}\n\nfunction ipv4(value: string, field: string): string {\n if (typeof value !== 'string' || isIP(value) !== 4) {\n throw new DeskError(`${field} must be an IPv4 address.`)\n }\n return value\n}\n\nfunction ipv4Number(address: string): number {\n return address.split('.').reduce((value, octet) => ((value << 8) | Number(octet)) >>> 0, 0)\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"]}
|
package/port-relay.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Duplex } from 'node:stream';
|
|
2
|
+
/** A local endpoint backed by a running desk's TCP connection. */
|
|
3
|
+
export interface DeskPortListener {
|
|
4
|
+
host: '127.0.0.1';
|
|
5
|
+
port: number;
|
|
6
|
+
/** Close the listener and every active connection. Safe to call repeatedly. */
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** Expose one guest TCP port to local HTTP, WebSocket or other TCP clients. */
|
|
10
|
+
export declare function listenDeskPort(desk: {
|
|
11
|
+
connect(port: number): Promise<Duplex>;
|
|
12
|
+
}, port: number): Promise<DeskPortListener>;
|
|
13
|
+
//# sourceMappingURL=port-relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-relay.d.ts","sourceRoot":"","sources":["../src/port-relay.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,+EAA+E;AAC/E,wBAAsB,cAAc,CAClC,IAAI,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,EAChD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CAsC3B"}
|