vmsan 0.1.0-alpha.2 → 0.1.0-alpha.21
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/LICENSE +190 -21
- package/README.md +91 -47
- package/dist/_chunks/agent.mjs +231 -4
- package/dist/_chunks/connect.mjs +53 -11
- package/dist/_chunks/context.mjs +2380 -0
- package/dist/_chunks/create.mjs +48 -180
- package/dist/_chunks/download.mjs +14 -22
- package/dist/_chunks/errors.mjs +11 -5
- package/dist/_chunks/exec.mjs +190 -0
- package/dist/_chunks/list.mjs +60 -54
- package/dist/_chunks/network.mjs +6 -5
- package/dist/_chunks/remove.mjs +9 -8
- package/dist/_chunks/shell.mjs +2 -0
- package/dist/_chunks/start.mjs +16 -165
- package/dist/_chunks/stop.mjs +8 -7
- package/dist/_chunks/summary.mjs +69 -0
- package/dist/_chunks/timeout-extender.mjs +66 -0
- package/dist/_chunks/timeout-killer.mjs +33 -0
- package/dist/_chunks/upload.mjs +5 -20
- package/dist/_chunks/validation.mjs +1 -1
- package/dist/_chunks/vm-context.mjs +34 -0
- package/dist/_chunks/vm-state.mjs +56 -24
- package/dist/bin/cli.mjs +16 -2
- package/dist/index.d.mts +660 -366
- package/dist/index.mjs +35 -8
- package/package.json +7 -6
- package/dist/_chunks/cleanup.mjs +0 -328
- package/dist/_chunks/connect2.mjs +0 -72
- package/dist/_chunks/environment.mjs +0 -1064
- package/dist/_chunks/image-rootfs.mjs +0 -329
- package/dist/_chunks/vm.mjs +0 -208
|
@@ -0,0 +1,2380 @@
|
|
|
1
|
+
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
|
+
import { B as mutuallyExclusiveFlagsError, C as vmNotStoppedError, S as vmNotRunningError, U as VmsanError, _ as chrootNotFoundError, a as noKernelDirError, d as socketTimeoutError, i as noExt4RootfsError, o as noKernelError, p as defaultInterfaceNotFoundError, r as missingBinaryError, s as noRootfsDirError, u as lockTimeoutError, x as vmNotFoundError, y as snapshotNotFoundError } from "./errors.mjs";
|
|
3
|
+
import { c as safeKill, f as toError, i as generateVmId, t as FileVmStateStore } from "./vm-state.mjs";
|
|
4
|
+
import { t as FirecrackerClient } from "./firecracker.mjs";
|
|
5
|
+
import { t as spawnTimeoutKiller } from "./timeout-killer.mjs";
|
|
6
|
+
import { createHooks } from "hookable";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { consola } from "consola";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { lock, lockSync } from "proper-lockfile";
|
|
13
|
+
import { connect } from "node:net";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
function createDefaultLogger() {
|
|
16
|
+
return wrapConsola(consola);
|
|
17
|
+
}
|
|
18
|
+
function wrapConsola(instance) {
|
|
19
|
+
return {
|
|
20
|
+
debug: instance.debug.bind(instance),
|
|
21
|
+
info: instance.info.bind(instance),
|
|
22
|
+
success: instance.success.bind(instance),
|
|
23
|
+
warn: instance.warn.bind(instance),
|
|
24
|
+
error: instance.error.bind(instance),
|
|
25
|
+
start: instance.start.bind(instance),
|
|
26
|
+
box: (message) => instance.box(message),
|
|
27
|
+
withTag: (tag) => wrapConsola(instance.withTag(tag))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const noop = () => {};
|
|
31
|
+
function createSilentLogger() {
|
|
32
|
+
const silent = {
|
|
33
|
+
debug: noop,
|
|
34
|
+
info: noop,
|
|
35
|
+
success: noop,
|
|
36
|
+
warn: noop,
|
|
37
|
+
error: noop,
|
|
38
|
+
start: noop,
|
|
39
|
+
box: noop,
|
|
40
|
+
withTag: () => silent
|
|
41
|
+
};
|
|
42
|
+
return silent;
|
|
43
|
+
}
|
|
44
|
+
const DNS_RESOLVERS = ["8.8.8.8", "8.8.4.4"];
|
|
45
|
+
const DOH_RESOLVER_IPS = [
|
|
46
|
+
"8.8.8.8",
|
|
47
|
+
"8.8.4.4",
|
|
48
|
+
"1.1.1.1",
|
|
49
|
+
"1.0.0.1",
|
|
50
|
+
"9.9.9.9",
|
|
51
|
+
"149.112.112.112",
|
|
52
|
+
"208.67.222.222",
|
|
53
|
+
"208.67.220.220",
|
|
54
|
+
"185.228.168.168",
|
|
55
|
+
"185.228.169.168"
|
|
56
|
+
];
|
|
57
|
+
function runArgs(bin, args) {
|
|
58
|
+
execFileSync(bin, args, { stdio: "pipe" });
|
|
59
|
+
}
|
|
60
|
+
function sudo(args) {
|
|
61
|
+
runArgs("sudo", args);
|
|
62
|
+
}
|
|
63
|
+
function sudoNetns(nsName, args) {
|
|
64
|
+
sudo([
|
|
65
|
+
"ip",
|
|
66
|
+
"netns",
|
|
67
|
+
"exec",
|
|
68
|
+
nsName,
|
|
69
|
+
...args
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
function getDefaultInterface() {
|
|
73
|
+
const match = execFileSync("ip", [
|
|
74
|
+
"route",
|
|
75
|
+
"show",
|
|
76
|
+
"default"
|
|
77
|
+
], {
|
|
78
|
+
encoding: "utf-8",
|
|
79
|
+
stdio: "pipe"
|
|
80
|
+
}).trim().match(/dev\s+(\S+)/);
|
|
81
|
+
if (!match) throw defaultInterfaceNotFoundError();
|
|
82
|
+
return match[1];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Detect effective policy mode:
|
|
86
|
+
* - "deny-all": explicit deny-all, no outbound
|
|
87
|
+
* - "custom": any of allowedDomains/allowedCidrs/deniedCidrs present
|
|
88
|
+
* - "allow-all": default, unrestricted
|
|
89
|
+
*/
|
|
90
|
+
function effectivePolicy(config) {
|
|
91
|
+
if (config.networkPolicy === "deny-all") return "deny-all";
|
|
92
|
+
if (config.allowedDomains.length > 0 || config.allowedCidrs.length > 0 || config.deniedCidrs.length > 0) return "custom";
|
|
93
|
+
return "allow-all";
|
|
94
|
+
}
|
|
95
|
+
var NetworkManager = class NetworkManager {
|
|
96
|
+
config;
|
|
97
|
+
constructor(slot, networkPolicy, allowedDomains, allowedCidrs, deniedCidrs, publishedPorts, bandwidthMbit, netnsName) {
|
|
98
|
+
this.config = {
|
|
99
|
+
slot,
|
|
100
|
+
tapDevice: `fhvm${slot}`,
|
|
101
|
+
hostIp: `172.16.${slot}.1`,
|
|
102
|
+
guestIp: `172.16.${slot}.2`,
|
|
103
|
+
subnetMask: "255.255.255.252",
|
|
104
|
+
macAddress: `AA:FC:00:00:00:${(slot + 1).toString(16).padStart(2, "0").toUpperCase()}`,
|
|
105
|
+
networkPolicy,
|
|
106
|
+
allowedDomains,
|
|
107
|
+
allowedCidrs,
|
|
108
|
+
deniedCidrs,
|
|
109
|
+
publishedPorts,
|
|
110
|
+
bandwidthMbit,
|
|
111
|
+
netnsName
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
static bootArgs(slot) {
|
|
115
|
+
return `console=ttyS0 reboot=k panic=1 pci=off ip=172.16.${slot}.2::${`172.16.${slot}.1`}:255.255.255.252::eth0:off:${DNS_RESOLVERS[0]}`;
|
|
116
|
+
}
|
|
117
|
+
static fromConfig(config) {
|
|
118
|
+
const mgr = Object.create(NetworkManager.prototype);
|
|
119
|
+
mgr.config = config;
|
|
120
|
+
return mgr;
|
|
121
|
+
}
|
|
122
|
+
static fromVmNetwork(network) {
|
|
123
|
+
const slot = Number(network.hostIp.split(".")[2]);
|
|
124
|
+
if (!Number.isInteger(slot)) throw new Error(`invalid network slot derived from hostIp: ${network.hostIp}`);
|
|
125
|
+
return NetworkManager.fromConfig({
|
|
126
|
+
slot,
|
|
127
|
+
tapDevice: network.tapDevice,
|
|
128
|
+
hostIp: network.hostIp,
|
|
129
|
+
guestIp: network.guestIp,
|
|
130
|
+
subnetMask: network.subnetMask,
|
|
131
|
+
macAddress: network.macAddress,
|
|
132
|
+
networkPolicy: network.networkPolicy,
|
|
133
|
+
allowedDomains: network.allowedDomains,
|
|
134
|
+
allowedCidrs: network.allowedCidrs || [],
|
|
135
|
+
deniedCidrs: network.deniedCidrs || [],
|
|
136
|
+
publishedPorts: network.publishedPorts,
|
|
137
|
+
bandwidthMbit: network.bandwidthMbit,
|
|
138
|
+
netnsName: network.netnsName
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
nsRun(args) {
|
|
142
|
+
const { netnsName } = this.config;
|
|
143
|
+
if (netnsName) sudoNetns(netnsName, args);
|
|
144
|
+
else sudo(args);
|
|
145
|
+
}
|
|
146
|
+
setupNamespace() {
|
|
147
|
+
const { slot, netnsName } = this.config;
|
|
148
|
+
if (!netnsName) return;
|
|
149
|
+
const vethHost = `veth-h-${slot}`;
|
|
150
|
+
const vethGuest = `veth-g-${slot}`;
|
|
151
|
+
const transitHostIp = `10.200.${slot}.1`;
|
|
152
|
+
const transitGuestIp = `10.200.${slot}.2`;
|
|
153
|
+
if (existsSync(`/sys/class/net/${vethHost}`)) try {
|
|
154
|
+
sudo([
|
|
155
|
+
"ip",
|
|
156
|
+
"link",
|
|
157
|
+
"delete",
|
|
158
|
+
vethHost
|
|
159
|
+
]);
|
|
160
|
+
} catch {}
|
|
161
|
+
sudo([
|
|
162
|
+
"ip",
|
|
163
|
+
"netns",
|
|
164
|
+
"add",
|
|
165
|
+
netnsName
|
|
166
|
+
]);
|
|
167
|
+
sudo([
|
|
168
|
+
"ip",
|
|
169
|
+
"link",
|
|
170
|
+
"add",
|
|
171
|
+
vethHost,
|
|
172
|
+
"type",
|
|
173
|
+
"veth",
|
|
174
|
+
"peer",
|
|
175
|
+
"name",
|
|
176
|
+
vethGuest
|
|
177
|
+
]);
|
|
178
|
+
sudo([
|
|
179
|
+
"ip",
|
|
180
|
+
"link",
|
|
181
|
+
"set",
|
|
182
|
+
vethGuest,
|
|
183
|
+
"netns",
|
|
184
|
+
netnsName
|
|
185
|
+
]);
|
|
186
|
+
sudo([
|
|
187
|
+
"ip",
|
|
188
|
+
"addr",
|
|
189
|
+
"add",
|
|
190
|
+
`${transitHostIp}/30`,
|
|
191
|
+
"dev",
|
|
192
|
+
vethHost
|
|
193
|
+
]);
|
|
194
|
+
sudo([
|
|
195
|
+
"ip",
|
|
196
|
+
"link",
|
|
197
|
+
"set",
|
|
198
|
+
vethHost,
|
|
199
|
+
"up"
|
|
200
|
+
]);
|
|
201
|
+
sudoNetns(netnsName, [
|
|
202
|
+
"ip",
|
|
203
|
+
"addr",
|
|
204
|
+
"add",
|
|
205
|
+
`${transitGuestIp}/30`,
|
|
206
|
+
"dev",
|
|
207
|
+
vethGuest
|
|
208
|
+
]);
|
|
209
|
+
sudoNetns(netnsName, [
|
|
210
|
+
"ip",
|
|
211
|
+
"link",
|
|
212
|
+
"set",
|
|
213
|
+
vethGuest,
|
|
214
|
+
"up"
|
|
215
|
+
]);
|
|
216
|
+
sudoNetns(netnsName, [
|
|
217
|
+
"ip",
|
|
218
|
+
"link",
|
|
219
|
+
"set",
|
|
220
|
+
"lo",
|
|
221
|
+
"up"
|
|
222
|
+
]);
|
|
223
|
+
sudoNetns(netnsName, [
|
|
224
|
+
"ip",
|
|
225
|
+
"route",
|
|
226
|
+
"add",
|
|
227
|
+
"default",
|
|
228
|
+
"via",
|
|
229
|
+
transitHostIp
|
|
230
|
+
]);
|
|
231
|
+
sudoNetns(netnsName, [
|
|
232
|
+
"sysctl",
|
|
233
|
+
"-w",
|
|
234
|
+
"net.ipv4.ip_forward=1"
|
|
235
|
+
]);
|
|
236
|
+
sudo([
|
|
237
|
+
"ip",
|
|
238
|
+
"route",
|
|
239
|
+
"add",
|
|
240
|
+
`172.16.${slot}.0/30`,
|
|
241
|
+
"via",
|
|
242
|
+
transitGuestIp
|
|
243
|
+
]);
|
|
244
|
+
sudo([
|
|
245
|
+
"sysctl",
|
|
246
|
+
"-w",
|
|
247
|
+
"net.ipv4.ip_forward=1"
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
teardownNamespace() {
|
|
251
|
+
const { slot, netnsName } = this.config;
|
|
252
|
+
if (!netnsName) return;
|
|
253
|
+
const tryRun = (args) => {
|
|
254
|
+
try {
|
|
255
|
+
sudo(args);
|
|
256
|
+
} catch {}
|
|
257
|
+
};
|
|
258
|
+
tryRun([
|
|
259
|
+
"ip",
|
|
260
|
+
"route",
|
|
261
|
+
"del",
|
|
262
|
+
`172.16.${slot}.0/30`
|
|
263
|
+
]);
|
|
264
|
+
tryRun([
|
|
265
|
+
"ip",
|
|
266
|
+
"netns",
|
|
267
|
+
"delete",
|
|
268
|
+
netnsName
|
|
269
|
+
]);
|
|
270
|
+
}
|
|
271
|
+
setupDevice() {
|
|
272
|
+
const { tapDevice, hostIp, netnsName } = this.config;
|
|
273
|
+
if (!netnsName) {
|
|
274
|
+
if (existsSync(`/sys/class/net/${tapDevice}`)) try {
|
|
275
|
+
sudo([
|
|
276
|
+
"ip",
|
|
277
|
+
"link",
|
|
278
|
+
"delete",
|
|
279
|
+
tapDevice
|
|
280
|
+
]);
|
|
281
|
+
} catch {}
|
|
282
|
+
sudo([
|
|
283
|
+
"ip",
|
|
284
|
+
"tuntap",
|
|
285
|
+
"add",
|
|
286
|
+
"dev",
|
|
287
|
+
tapDevice,
|
|
288
|
+
"mode",
|
|
289
|
+
"tap"
|
|
290
|
+
]);
|
|
291
|
+
sudo([
|
|
292
|
+
"ip",
|
|
293
|
+
"addr",
|
|
294
|
+
"add",
|
|
295
|
+
`${hostIp}/30`,
|
|
296
|
+
"dev",
|
|
297
|
+
tapDevice
|
|
298
|
+
]);
|
|
299
|
+
sudo([
|
|
300
|
+
"ip",
|
|
301
|
+
"link",
|
|
302
|
+
"set",
|
|
303
|
+
tapDevice,
|
|
304
|
+
"up"
|
|
305
|
+
]);
|
|
306
|
+
sudo([
|
|
307
|
+
"sysctl",
|
|
308
|
+
"-w",
|
|
309
|
+
"net.ipv4.ip_forward=1"
|
|
310
|
+
]);
|
|
311
|
+
} else {
|
|
312
|
+
sudoNetns(netnsName, [
|
|
313
|
+
"ip",
|
|
314
|
+
"tuntap",
|
|
315
|
+
"add",
|
|
316
|
+
"dev",
|
|
317
|
+
tapDevice,
|
|
318
|
+
"mode",
|
|
319
|
+
"tap"
|
|
320
|
+
]);
|
|
321
|
+
sudoNetns(netnsName, [
|
|
322
|
+
"ip",
|
|
323
|
+
"addr",
|
|
324
|
+
"add",
|
|
325
|
+
`${hostIp}/30`,
|
|
326
|
+
"dev",
|
|
327
|
+
tapDevice
|
|
328
|
+
]);
|
|
329
|
+
sudoNetns(netnsName, [
|
|
330
|
+
"ip",
|
|
331
|
+
"link",
|
|
332
|
+
"set",
|
|
333
|
+
tapDevice,
|
|
334
|
+
"up"
|
|
335
|
+
]);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
setupRules() {
|
|
339
|
+
const { tapDevice, hostIp, guestIp, publishedPorts } = this.config;
|
|
340
|
+
const policy = effectivePolicy(this.config);
|
|
341
|
+
const fwd = this.nsRun.bind(this);
|
|
342
|
+
if (policy === "deny-all") {
|
|
343
|
+
fwd([
|
|
344
|
+
"iptables",
|
|
345
|
+
"-I",
|
|
346
|
+
"FORWARD",
|
|
347
|
+
"-i",
|
|
348
|
+
tapDevice,
|
|
349
|
+
"-j",
|
|
350
|
+
"DROP"
|
|
351
|
+
]);
|
|
352
|
+
fwd([
|
|
353
|
+
"iptables",
|
|
354
|
+
"-I",
|
|
355
|
+
"FORWARD",
|
|
356
|
+
"-o",
|
|
357
|
+
tapDevice,
|
|
358
|
+
"-j",
|
|
359
|
+
"DROP"
|
|
360
|
+
]);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const defaultIface = getDefaultInterface();
|
|
364
|
+
sudo([
|
|
365
|
+
"iptables",
|
|
366
|
+
"-t",
|
|
367
|
+
"nat",
|
|
368
|
+
"-A",
|
|
369
|
+
"POSTROUTING",
|
|
370
|
+
"-s",
|
|
371
|
+
`${guestIp}/30`,
|
|
372
|
+
"-o",
|
|
373
|
+
defaultIface,
|
|
374
|
+
"-j",
|
|
375
|
+
"MASQUERADE"
|
|
376
|
+
]);
|
|
377
|
+
if (this.config.netnsName) {
|
|
378
|
+
const vethHost = `veth-h-${this.config.slot}`;
|
|
379
|
+
sudo([
|
|
380
|
+
"iptables",
|
|
381
|
+
"-A",
|
|
382
|
+
"FORWARD",
|
|
383
|
+
"-i",
|
|
384
|
+
vethHost,
|
|
385
|
+
"-o",
|
|
386
|
+
defaultIface,
|
|
387
|
+
"-s",
|
|
388
|
+
`${guestIp}/30`,
|
|
389
|
+
"-j",
|
|
390
|
+
"ACCEPT"
|
|
391
|
+
]);
|
|
392
|
+
sudo([
|
|
393
|
+
"iptables",
|
|
394
|
+
"-A",
|
|
395
|
+
"FORWARD",
|
|
396
|
+
"-i",
|
|
397
|
+
defaultIface,
|
|
398
|
+
"-o",
|
|
399
|
+
vethHost,
|
|
400
|
+
"-d",
|
|
401
|
+
`${guestIp}/30`,
|
|
402
|
+
"-m",
|
|
403
|
+
"state",
|
|
404
|
+
"--state",
|
|
405
|
+
"RELATED,ESTABLISHED",
|
|
406
|
+
"-j",
|
|
407
|
+
"ACCEPT"
|
|
408
|
+
]);
|
|
409
|
+
}
|
|
410
|
+
if (policy === "custom") {
|
|
411
|
+
for (const cidr of this.config.deniedCidrs) fwd([
|
|
412
|
+
"iptables",
|
|
413
|
+
"-A",
|
|
414
|
+
"FORWARD",
|
|
415
|
+
"-i",
|
|
416
|
+
tapDevice,
|
|
417
|
+
"-d",
|
|
418
|
+
cidr,
|
|
419
|
+
"-j",
|
|
420
|
+
"DROP"
|
|
421
|
+
]);
|
|
422
|
+
for (const dnsIp of DNS_RESOLVERS) {
|
|
423
|
+
fwd([
|
|
424
|
+
"iptables",
|
|
425
|
+
"-A",
|
|
426
|
+
"FORWARD",
|
|
427
|
+
"-i",
|
|
428
|
+
tapDevice,
|
|
429
|
+
"-d",
|
|
430
|
+
dnsIp,
|
|
431
|
+
"-p",
|
|
432
|
+
"udp",
|
|
433
|
+
"--dport",
|
|
434
|
+
"53",
|
|
435
|
+
"-j",
|
|
436
|
+
"ACCEPT"
|
|
437
|
+
]);
|
|
438
|
+
fwd([
|
|
439
|
+
"iptables",
|
|
440
|
+
"-A",
|
|
441
|
+
"FORWARD",
|
|
442
|
+
"-i",
|
|
443
|
+
tapDevice,
|
|
444
|
+
"-d",
|
|
445
|
+
dnsIp,
|
|
446
|
+
"-p",
|
|
447
|
+
"tcp",
|
|
448
|
+
"--dport",
|
|
449
|
+
"53",
|
|
450
|
+
"-j",
|
|
451
|
+
"ACCEPT"
|
|
452
|
+
]);
|
|
453
|
+
}
|
|
454
|
+
fwd([
|
|
455
|
+
"iptables",
|
|
456
|
+
"-A",
|
|
457
|
+
"FORWARD",
|
|
458
|
+
"-i",
|
|
459
|
+
tapDevice,
|
|
460
|
+
"-p",
|
|
461
|
+
"udp",
|
|
462
|
+
"--dport",
|
|
463
|
+
"53",
|
|
464
|
+
"-j",
|
|
465
|
+
"DROP"
|
|
466
|
+
]);
|
|
467
|
+
fwd([
|
|
468
|
+
"iptables",
|
|
469
|
+
"-A",
|
|
470
|
+
"FORWARD",
|
|
471
|
+
"-i",
|
|
472
|
+
tapDevice,
|
|
473
|
+
"-p",
|
|
474
|
+
"tcp",
|
|
475
|
+
"--dport",
|
|
476
|
+
"53",
|
|
477
|
+
"-j",
|
|
478
|
+
"DROP"
|
|
479
|
+
]);
|
|
480
|
+
fwd([
|
|
481
|
+
"iptables",
|
|
482
|
+
"-A",
|
|
483
|
+
"FORWARD",
|
|
484
|
+
"-i",
|
|
485
|
+
tapDevice,
|
|
486
|
+
"-p",
|
|
487
|
+
"udp",
|
|
488
|
+
"--dport",
|
|
489
|
+
"853",
|
|
490
|
+
"-j",
|
|
491
|
+
"DROP"
|
|
492
|
+
]);
|
|
493
|
+
fwd([
|
|
494
|
+
"iptables",
|
|
495
|
+
"-A",
|
|
496
|
+
"FORWARD",
|
|
497
|
+
"-i",
|
|
498
|
+
tapDevice,
|
|
499
|
+
"-p",
|
|
500
|
+
"tcp",
|
|
501
|
+
"--dport",
|
|
502
|
+
"853",
|
|
503
|
+
"-j",
|
|
504
|
+
"DROP"
|
|
505
|
+
]);
|
|
506
|
+
for (const ip of DOH_RESOLVER_IPS) {
|
|
507
|
+
fwd([
|
|
508
|
+
"iptables",
|
|
509
|
+
"-A",
|
|
510
|
+
"FORWARD",
|
|
511
|
+
"-i",
|
|
512
|
+
tapDevice,
|
|
513
|
+
"-d",
|
|
514
|
+
ip,
|
|
515
|
+
"-p",
|
|
516
|
+
"tcp",
|
|
517
|
+
"--dport",
|
|
518
|
+
"443",
|
|
519
|
+
"-j",
|
|
520
|
+
"DROP"
|
|
521
|
+
]);
|
|
522
|
+
fwd([
|
|
523
|
+
"iptables",
|
|
524
|
+
"-A",
|
|
525
|
+
"FORWARD",
|
|
526
|
+
"-i",
|
|
527
|
+
tapDevice,
|
|
528
|
+
"-d",
|
|
529
|
+
ip,
|
|
530
|
+
"-p",
|
|
531
|
+
"udp",
|
|
532
|
+
"--dport",
|
|
533
|
+
"443",
|
|
534
|
+
"-j",
|
|
535
|
+
"DROP"
|
|
536
|
+
]);
|
|
537
|
+
}
|
|
538
|
+
fwd([
|
|
539
|
+
"iptables",
|
|
540
|
+
"-A",
|
|
541
|
+
"FORWARD",
|
|
542
|
+
"-i",
|
|
543
|
+
tapDevice,
|
|
544
|
+
"-d",
|
|
545
|
+
"172.16.0.0/16",
|
|
546
|
+
"-j",
|
|
547
|
+
"DROP"
|
|
548
|
+
]);
|
|
549
|
+
for (const cidr of this.config.allowedCidrs) fwd([
|
|
550
|
+
"iptables",
|
|
551
|
+
"-A",
|
|
552
|
+
"FORWARD",
|
|
553
|
+
"-i",
|
|
554
|
+
tapDevice,
|
|
555
|
+
"-d",
|
|
556
|
+
cidr,
|
|
557
|
+
"-j",
|
|
558
|
+
"ACCEPT"
|
|
559
|
+
]);
|
|
560
|
+
fwd([
|
|
561
|
+
"iptables",
|
|
562
|
+
"-A",
|
|
563
|
+
"FORWARD",
|
|
564
|
+
"-i",
|
|
565
|
+
tapDevice,
|
|
566
|
+
"-j",
|
|
567
|
+
"ACCEPT"
|
|
568
|
+
]);
|
|
569
|
+
} else {
|
|
570
|
+
for (const dnsIp of DNS_RESOLVERS) {
|
|
571
|
+
fwd([
|
|
572
|
+
"iptables",
|
|
573
|
+
"-A",
|
|
574
|
+
"FORWARD",
|
|
575
|
+
"-i",
|
|
576
|
+
tapDevice,
|
|
577
|
+
"-d",
|
|
578
|
+
dnsIp,
|
|
579
|
+
"-p",
|
|
580
|
+
"udp",
|
|
581
|
+
"--dport",
|
|
582
|
+
"53",
|
|
583
|
+
"-j",
|
|
584
|
+
"ACCEPT"
|
|
585
|
+
]);
|
|
586
|
+
fwd([
|
|
587
|
+
"iptables",
|
|
588
|
+
"-A",
|
|
589
|
+
"FORWARD",
|
|
590
|
+
"-i",
|
|
591
|
+
tapDevice,
|
|
592
|
+
"-d",
|
|
593
|
+
dnsIp,
|
|
594
|
+
"-p",
|
|
595
|
+
"tcp",
|
|
596
|
+
"--dport",
|
|
597
|
+
"53",
|
|
598
|
+
"-j",
|
|
599
|
+
"ACCEPT"
|
|
600
|
+
]);
|
|
601
|
+
}
|
|
602
|
+
fwd([
|
|
603
|
+
"iptables",
|
|
604
|
+
"-A",
|
|
605
|
+
"FORWARD",
|
|
606
|
+
"-i",
|
|
607
|
+
tapDevice,
|
|
608
|
+
"-p",
|
|
609
|
+
"udp",
|
|
610
|
+
"--dport",
|
|
611
|
+
"53",
|
|
612
|
+
"-j",
|
|
613
|
+
"DROP"
|
|
614
|
+
]);
|
|
615
|
+
fwd([
|
|
616
|
+
"iptables",
|
|
617
|
+
"-A",
|
|
618
|
+
"FORWARD",
|
|
619
|
+
"-i",
|
|
620
|
+
tapDevice,
|
|
621
|
+
"-p",
|
|
622
|
+
"tcp",
|
|
623
|
+
"--dport",
|
|
624
|
+
"53",
|
|
625
|
+
"-j",
|
|
626
|
+
"DROP"
|
|
627
|
+
]);
|
|
628
|
+
fwd([
|
|
629
|
+
"iptables",
|
|
630
|
+
"-A",
|
|
631
|
+
"FORWARD",
|
|
632
|
+
"-i",
|
|
633
|
+
tapDevice,
|
|
634
|
+
"-p",
|
|
635
|
+
"udp",
|
|
636
|
+
"--dport",
|
|
637
|
+
"853",
|
|
638
|
+
"-j",
|
|
639
|
+
"DROP"
|
|
640
|
+
]);
|
|
641
|
+
fwd([
|
|
642
|
+
"iptables",
|
|
643
|
+
"-A",
|
|
644
|
+
"FORWARD",
|
|
645
|
+
"-i",
|
|
646
|
+
tapDevice,
|
|
647
|
+
"-p",
|
|
648
|
+
"tcp",
|
|
649
|
+
"--dport",
|
|
650
|
+
"853",
|
|
651
|
+
"-j",
|
|
652
|
+
"DROP"
|
|
653
|
+
]);
|
|
654
|
+
for (const ip of DOH_RESOLVER_IPS) {
|
|
655
|
+
fwd([
|
|
656
|
+
"iptables",
|
|
657
|
+
"-A",
|
|
658
|
+
"FORWARD",
|
|
659
|
+
"-i",
|
|
660
|
+
tapDevice,
|
|
661
|
+
"-d",
|
|
662
|
+
ip,
|
|
663
|
+
"-p",
|
|
664
|
+
"tcp",
|
|
665
|
+
"--dport",
|
|
666
|
+
"443",
|
|
667
|
+
"-j",
|
|
668
|
+
"DROP"
|
|
669
|
+
]);
|
|
670
|
+
fwd([
|
|
671
|
+
"iptables",
|
|
672
|
+
"-A",
|
|
673
|
+
"FORWARD",
|
|
674
|
+
"-i",
|
|
675
|
+
tapDevice,
|
|
676
|
+
"-d",
|
|
677
|
+
ip,
|
|
678
|
+
"-p",
|
|
679
|
+
"udp",
|
|
680
|
+
"--dport",
|
|
681
|
+
"443",
|
|
682
|
+
"-j",
|
|
683
|
+
"DROP"
|
|
684
|
+
]);
|
|
685
|
+
}
|
|
686
|
+
fwd([
|
|
687
|
+
"iptables",
|
|
688
|
+
"-A",
|
|
689
|
+
"FORWARD",
|
|
690
|
+
"-i",
|
|
691
|
+
tapDevice,
|
|
692
|
+
"-d",
|
|
693
|
+
"172.16.0.0/16",
|
|
694
|
+
"-j",
|
|
695
|
+
"DROP"
|
|
696
|
+
]);
|
|
697
|
+
fwd([
|
|
698
|
+
"iptables",
|
|
699
|
+
"-A",
|
|
700
|
+
"FORWARD",
|
|
701
|
+
"-i",
|
|
702
|
+
tapDevice,
|
|
703
|
+
"-j",
|
|
704
|
+
"ACCEPT"
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
fwd([
|
|
708
|
+
"iptables",
|
|
709
|
+
"-A",
|
|
710
|
+
"FORWARD",
|
|
711
|
+
"-o",
|
|
712
|
+
tapDevice,
|
|
713
|
+
"-m",
|
|
714
|
+
"state",
|
|
715
|
+
"--state",
|
|
716
|
+
"RELATED,ESTABLISHED",
|
|
717
|
+
"-j",
|
|
718
|
+
"ACCEPT"
|
|
719
|
+
]);
|
|
720
|
+
for (const port of publishedPorts) {
|
|
721
|
+
const portStr = String(port);
|
|
722
|
+
sudo([
|
|
723
|
+
"iptables",
|
|
724
|
+
"-t",
|
|
725
|
+
"nat",
|
|
726
|
+
"-A",
|
|
727
|
+
"PREROUTING",
|
|
728
|
+
"-i",
|
|
729
|
+
defaultIface,
|
|
730
|
+
"-p",
|
|
731
|
+
"tcp",
|
|
732
|
+
"--dport",
|
|
733
|
+
portStr,
|
|
734
|
+
"-j",
|
|
735
|
+
"DNAT",
|
|
736
|
+
"--to-destination",
|
|
737
|
+
`${guestIp}:${portStr}`
|
|
738
|
+
]);
|
|
739
|
+
sudo([
|
|
740
|
+
"iptables",
|
|
741
|
+
"-A",
|
|
742
|
+
"FORWARD",
|
|
743
|
+
"-p",
|
|
744
|
+
"tcp",
|
|
745
|
+
"-d",
|
|
746
|
+
guestIp,
|
|
747
|
+
"--dport",
|
|
748
|
+
portStr,
|
|
749
|
+
"-j",
|
|
750
|
+
"ACCEPT"
|
|
751
|
+
]);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
setupThrottle() {
|
|
755
|
+
const { tapDevice, bandwidthMbit } = this.config;
|
|
756
|
+
if (bandwidthMbit === void 0) return;
|
|
757
|
+
const rateKbit = bandwidthMbit * 1e3;
|
|
758
|
+
const burstKb = Math.max(32, Math.floor(rateKbit / 8));
|
|
759
|
+
this.nsRun([
|
|
760
|
+
"tc",
|
|
761
|
+
"qdisc",
|
|
762
|
+
"add",
|
|
763
|
+
"dev",
|
|
764
|
+
tapDevice,
|
|
765
|
+
"root",
|
|
766
|
+
"tbf",
|
|
767
|
+
"rate",
|
|
768
|
+
`${bandwidthMbit}mbit`,
|
|
769
|
+
"burst",
|
|
770
|
+
`${burstKb}kb`,
|
|
771
|
+
"latency",
|
|
772
|
+
"400ms"
|
|
773
|
+
]);
|
|
774
|
+
}
|
|
775
|
+
teardownThrottle() {
|
|
776
|
+
const { tapDevice } = this.config;
|
|
777
|
+
try {
|
|
778
|
+
this.nsRun([
|
|
779
|
+
"tc",
|
|
780
|
+
"qdisc",
|
|
781
|
+
"del",
|
|
782
|
+
"dev",
|
|
783
|
+
tapDevice,
|
|
784
|
+
"root"
|
|
785
|
+
]);
|
|
786
|
+
} catch {}
|
|
787
|
+
}
|
|
788
|
+
teardownRules() {
|
|
789
|
+
const { tapDevice, hostIp, guestIp, publishedPorts, netnsName } = this.config;
|
|
790
|
+
let defaultIface;
|
|
791
|
+
try {
|
|
792
|
+
defaultIface = getDefaultInterface();
|
|
793
|
+
} catch {}
|
|
794
|
+
const tryRun = (args) => {
|
|
795
|
+
try {
|
|
796
|
+
sudo(args);
|
|
797
|
+
} catch {}
|
|
798
|
+
};
|
|
799
|
+
const tryFwd = (args) => {
|
|
800
|
+
try {
|
|
801
|
+
this.nsRun(args);
|
|
802
|
+
} catch {}
|
|
803
|
+
};
|
|
804
|
+
for (const port of publishedPorts) {
|
|
805
|
+
const portStr = String(port);
|
|
806
|
+
if (defaultIface) tryRun([
|
|
807
|
+
"iptables",
|
|
808
|
+
"-t",
|
|
809
|
+
"nat",
|
|
810
|
+
"-D",
|
|
811
|
+
"PREROUTING",
|
|
812
|
+
"-i",
|
|
813
|
+
defaultIface,
|
|
814
|
+
"-p",
|
|
815
|
+
"tcp",
|
|
816
|
+
"--dport",
|
|
817
|
+
portStr,
|
|
818
|
+
"-j",
|
|
819
|
+
"DNAT",
|
|
820
|
+
"--to-destination",
|
|
821
|
+
`${guestIp}:${portStr}`
|
|
822
|
+
]);
|
|
823
|
+
tryRun([
|
|
824
|
+
"iptables",
|
|
825
|
+
"-D",
|
|
826
|
+
"FORWARD",
|
|
827
|
+
"-p",
|
|
828
|
+
"tcp",
|
|
829
|
+
"-d",
|
|
830
|
+
guestIp,
|
|
831
|
+
"--dport",
|
|
832
|
+
portStr,
|
|
833
|
+
"-j",
|
|
834
|
+
"ACCEPT"
|
|
835
|
+
]);
|
|
836
|
+
}
|
|
837
|
+
if (!netnsName) {
|
|
838
|
+
for (const cidr of this.config.deniedCidrs) tryFwd([
|
|
839
|
+
"iptables",
|
|
840
|
+
"-D",
|
|
841
|
+
"FORWARD",
|
|
842
|
+
"-i",
|
|
843
|
+
tapDevice,
|
|
844
|
+
"-d",
|
|
845
|
+
cidr,
|
|
846
|
+
"-j",
|
|
847
|
+
"DROP"
|
|
848
|
+
]);
|
|
849
|
+
for (const cidr of this.config.allowedCidrs) tryFwd([
|
|
850
|
+
"iptables",
|
|
851
|
+
"-D",
|
|
852
|
+
"FORWARD",
|
|
853
|
+
"-i",
|
|
854
|
+
tapDevice,
|
|
855
|
+
"-d",
|
|
856
|
+
cidr,
|
|
857
|
+
"-j",
|
|
858
|
+
"ACCEPT"
|
|
859
|
+
]);
|
|
860
|
+
for (const dnsIp of DNS_RESOLVERS) {
|
|
861
|
+
tryFwd([
|
|
862
|
+
"iptables",
|
|
863
|
+
"-D",
|
|
864
|
+
"FORWARD",
|
|
865
|
+
"-i",
|
|
866
|
+
tapDevice,
|
|
867
|
+
"-d",
|
|
868
|
+
dnsIp,
|
|
869
|
+
"-p",
|
|
870
|
+
"udp",
|
|
871
|
+
"--dport",
|
|
872
|
+
"53",
|
|
873
|
+
"-j",
|
|
874
|
+
"ACCEPT"
|
|
875
|
+
]);
|
|
876
|
+
tryFwd([
|
|
877
|
+
"iptables",
|
|
878
|
+
"-D",
|
|
879
|
+
"FORWARD",
|
|
880
|
+
"-i",
|
|
881
|
+
tapDevice,
|
|
882
|
+
"-d",
|
|
883
|
+
dnsIp,
|
|
884
|
+
"-p",
|
|
885
|
+
"tcp",
|
|
886
|
+
"--dport",
|
|
887
|
+
"53",
|
|
888
|
+
"-j",
|
|
889
|
+
"ACCEPT"
|
|
890
|
+
]);
|
|
891
|
+
}
|
|
892
|
+
tryFwd([
|
|
893
|
+
"iptables",
|
|
894
|
+
"-D",
|
|
895
|
+
"FORWARD",
|
|
896
|
+
"-i",
|
|
897
|
+
tapDevice,
|
|
898
|
+
"-p",
|
|
899
|
+
"udp",
|
|
900
|
+
"--dport",
|
|
901
|
+
"53",
|
|
902
|
+
"-j",
|
|
903
|
+
"DROP"
|
|
904
|
+
]);
|
|
905
|
+
tryFwd([
|
|
906
|
+
"iptables",
|
|
907
|
+
"-D",
|
|
908
|
+
"FORWARD",
|
|
909
|
+
"-i",
|
|
910
|
+
tapDevice,
|
|
911
|
+
"-p",
|
|
912
|
+
"tcp",
|
|
913
|
+
"--dport",
|
|
914
|
+
"53",
|
|
915
|
+
"-j",
|
|
916
|
+
"DROP"
|
|
917
|
+
]);
|
|
918
|
+
tryFwd([
|
|
919
|
+
"iptables",
|
|
920
|
+
"-D",
|
|
921
|
+
"FORWARD",
|
|
922
|
+
"-i",
|
|
923
|
+
tapDevice,
|
|
924
|
+
"-p",
|
|
925
|
+
"udp",
|
|
926
|
+
"--dport",
|
|
927
|
+
"853",
|
|
928
|
+
"-j",
|
|
929
|
+
"DROP"
|
|
930
|
+
]);
|
|
931
|
+
tryFwd([
|
|
932
|
+
"iptables",
|
|
933
|
+
"-D",
|
|
934
|
+
"FORWARD",
|
|
935
|
+
"-i",
|
|
936
|
+
tapDevice,
|
|
937
|
+
"-p",
|
|
938
|
+
"tcp",
|
|
939
|
+
"--dport",
|
|
940
|
+
"853",
|
|
941
|
+
"-j",
|
|
942
|
+
"DROP"
|
|
943
|
+
]);
|
|
944
|
+
for (const ip of DOH_RESOLVER_IPS) {
|
|
945
|
+
tryFwd([
|
|
946
|
+
"iptables",
|
|
947
|
+
"-D",
|
|
948
|
+
"FORWARD",
|
|
949
|
+
"-i",
|
|
950
|
+
tapDevice,
|
|
951
|
+
"-d",
|
|
952
|
+
ip,
|
|
953
|
+
"-p",
|
|
954
|
+
"tcp",
|
|
955
|
+
"--dport",
|
|
956
|
+
"443",
|
|
957
|
+
"-j",
|
|
958
|
+
"DROP"
|
|
959
|
+
]);
|
|
960
|
+
tryFwd([
|
|
961
|
+
"iptables",
|
|
962
|
+
"-D",
|
|
963
|
+
"FORWARD",
|
|
964
|
+
"-i",
|
|
965
|
+
tapDevice,
|
|
966
|
+
"-d",
|
|
967
|
+
ip,
|
|
968
|
+
"-p",
|
|
969
|
+
"udp",
|
|
970
|
+
"--dport",
|
|
971
|
+
"443",
|
|
972
|
+
"-j",
|
|
973
|
+
"DROP"
|
|
974
|
+
]);
|
|
975
|
+
}
|
|
976
|
+
tryFwd([
|
|
977
|
+
"iptables",
|
|
978
|
+
"-D",
|
|
979
|
+
"FORWARD",
|
|
980
|
+
"-i",
|
|
981
|
+
tapDevice,
|
|
982
|
+
"-d",
|
|
983
|
+
"172.16.0.0/16",
|
|
984
|
+
"-j",
|
|
985
|
+
"DROP"
|
|
986
|
+
]);
|
|
987
|
+
tryFwd([
|
|
988
|
+
"iptables",
|
|
989
|
+
"-D",
|
|
990
|
+
"FORWARD",
|
|
991
|
+
"-i",
|
|
992
|
+
tapDevice,
|
|
993
|
+
"-j",
|
|
994
|
+
"ACCEPT"
|
|
995
|
+
]);
|
|
996
|
+
tryFwd([
|
|
997
|
+
"iptables",
|
|
998
|
+
"-D",
|
|
999
|
+
"FORWARD",
|
|
1000
|
+
"-o",
|
|
1001
|
+
tapDevice,
|
|
1002
|
+
"-m",
|
|
1003
|
+
"state",
|
|
1004
|
+
"--state",
|
|
1005
|
+
"RELATED,ESTABLISHED",
|
|
1006
|
+
"-j",
|
|
1007
|
+
"ACCEPT"
|
|
1008
|
+
]);
|
|
1009
|
+
tryFwd([
|
|
1010
|
+
"iptables",
|
|
1011
|
+
"-D",
|
|
1012
|
+
"FORWARD",
|
|
1013
|
+
"-i",
|
|
1014
|
+
tapDevice,
|
|
1015
|
+
"-j",
|
|
1016
|
+
"DROP"
|
|
1017
|
+
]);
|
|
1018
|
+
tryFwd([
|
|
1019
|
+
"iptables",
|
|
1020
|
+
"-D",
|
|
1021
|
+
"FORWARD",
|
|
1022
|
+
"-o",
|
|
1023
|
+
tapDevice,
|
|
1024
|
+
"-j",
|
|
1025
|
+
"DROP"
|
|
1026
|
+
]);
|
|
1027
|
+
}
|
|
1028
|
+
if (netnsName && defaultIface) {
|
|
1029
|
+
const vethHost = `veth-h-${this.config.slot}`;
|
|
1030
|
+
tryRun([
|
|
1031
|
+
"iptables",
|
|
1032
|
+
"-D",
|
|
1033
|
+
"FORWARD",
|
|
1034
|
+
"-i",
|
|
1035
|
+
vethHost,
|
|
1036
|
+
"-o",
|
|
1037
|
+
defaultIface,
|
|
1038
|
+
"-s",
|
|
1039
|
+
`${guestIp}/30`,
|
|
1040
|
+
"-j",
|
|
1041
|
+
"ACCEPT"
|
|
1042
|
+
]);
|
|
1043
|
+
tryRun([
|
|
1044
|
+
"iptables",
|
|
1045
|
+
"-D",
|
|
1046
|
+
"FORWARD",
|
|
1047
|
+
"-i",
|
|
1048
|
+
defaultIface,
|
|
1049
|
+
"-o",
|
|
1050
|
+
vethHost,
|
|
1051
|
+
"-d",
|
|
1052
|
+
`${guestIp}/30`,
|
|
1053
|
+
"-m",
|
|
1054
|
+
"state",
|
|
1055
|
+
"--state",
|
|
1056
|
+
"RELATED,ESTABLISHED",
|
|
1057
|
+
"-j",
|
|
1058
|
+
"ACCEPT"
|
|
1059
|
+
]);
|
|
1060
|
+
}
|
|
1061
|
+
if (defaultIface) tryRun([
|
|
1062
|
+
"iptables",
|
|
1063
|
+
"-t",
|
|
1064
|
+
"nat",
|
|
1065
|
+
"-D",
|
|
1066
|
+
"POSTROUTING",
|
|
1067
|
+
"-s",
|
|
1068
|
+
`${guestIp}/30`,
|
|
1069
|
+
"-o",
|
|
1070
|
+
defaultIface,
|
|
1071
|
+
"-j",
|
|
1072
|
+
"MASQUERADE"
|
|
1073
|
+
]);
|
|
1074
|
+
}
|
|
1075
|
+
teardownDevice() {
|
|
1076
|
+
const { tapDevice, netnsName } = this.config;
|
|
1077
|
+
if (netnsName) return;
|
|
1078
|
+
try {
|
|
1079
|
+
sudo([
|
|
1080
|
+
"ip",
|
|
1081
|
+
"link",
|
|
1082
|
+
"delete",
|
|
1083
|
+
tapDevice
|
|
1084
|
+
]);
|
|
1085
|
+
} catch {}
|
|
1086
|
+
}
|
|
1087
|
+
async setup() {
|
|
1088
|
+
this.setupNamespace();
|
|
1089
|
+
this.setupDevice();
|
|
1090
|
+
this.setupRules();
|
|
1091
|
+
this.setupThrottle();
|
|
1092
|
+
}
|
|
1093
|
+
teardown() {
|
|
1094
|
+
if (this.config.netnsName) {
|
|
1095
|
+
this.teardownRules();
|
|
1096
|
+
this.teardownNamespace();
|
|
1097
|
+
} else {
|
|
1098
|
+
this.teardownThrottle();
|
|
1099
|
+
this.teardownRules();
|
|
1100
|
+
this.teardownDevice();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
updatePolicy(newPolicy, newDomains, newAllowedCidrs, newDeniedCidrs) {
|
|
1104
|
+
const oldConfig = { ...this.config };
|
|
1105
|
+
this.teardownRules();
|
|
1106
|
+
this.config.networkPolicy = newPolicy;
|
|
1107
|
+
this.config.allowedDomains = newDomains;
|
|
1108
|
+
this.config.allowedCidrs = newAllowedCidrs;
|
|
1109
|
+
this.config.deniedCidrs = newDeniedCidrs;
|
|
1110
|
+
try {
|
|
1111
|
+
this.setupRules();
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
this.config = oldConfig;
|
|
1114
|
+
try {
|
|
1115
|
+
this.setupRules();
|
|
1116
|
+
} catch {}
|
|
1117
|
+
throw err;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const STALE_MS = 3e5;
|
|
1122
|
+
const RETRY_MS = 50;
|
|
1123
|
+
const MAX_RETRIES = 600;
|
|
1124
|
+
const WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
|
|
1125
|
+
var FileLock = class {
|
|
1126
|
+
stale;
|
|
1127
|
+
retries;
|
|
1128
|
+
realpath;
|
|
1129
|
+
constructor(path, name, options) {
|
|
1130
|
+
this.path = path;
|
|
1131
|
+
this.name = name;
|
|
1132
|
+
this.stale = options?.stale ?? STALE_MS;
|
|
1133
|
+
this.retries = options?.retries ?? MAX_RETRIES;
|
|
1134
|
+
this.realpath = options?.realpath ?? false;
|
|
1135
|
+
}
|
|
1136
|
+
run(fn) {
|
|
1137
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
1138
|
+
const syncOpts = {
|
|
1139
|
+
stale: this.stale,
|
|
1140
|
+
realpath: this.realpath
|
|
1141
|
+
};
|
|
1142
|
+
let release;
|
|
1143
|
+
for (let attempt = 0;; attempt++) try {
|
|
1144
|
+
release = lockSync(this.path, syncOpts);
|
|
1145
|
+
break;
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
if (error.code !== "ELOCKED") throw error;
|
|
1148
|
+
if (attempt >= this.retries) throw lockTimeoutError(this.name);
|
|
1149
|
+
Atomics.wait(WAIT_ARRAY, 0, 0, RETRY_MS);
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
return fn();
|
|
1153
|
+
} finally {
|
|
1154
|
+
release();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async runAsync(fn) {
|
|
1158
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
1159
|
+
const asyncOpts = {
|
|
1160
|
+
stale: this.stale,
|
|
1161
|
+
realpath: this.realpath,
|
|
1162
|
+
retries: {
|
|
1163
|
+
retries: this.retries,
|
|
1164
|
+
minTimeout: RETRY_MS,
|
|
1165
|
+
maxTimeout: RETRY_MS,
|
|
1166
|
+
factor: 1
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
let release;
|
|
1170
|
+
try {
|
|
1171
|
+
release = await lock(this.path, asyncOpts);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (error.code === "ELOCKED") throw lockTimeoutError(this.name);
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
return await fn();
|
|
1178
|
+
} finally {
|
|
1179
|
+
await release();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
/**
|
|
1184
|
+
* Template generators for the node22-demo runtime welcome page.
|
|
1185
|
+
* All functions are pure and return string content ready to write to files.
|
|
1186
|
+
*/
|
|
1187
|
+
function generateWelcomeHtml(vmId, ports) {
|
|
1188
|
+
ports.map((p) => `<li>${p}</li>`).join("\n ");
|
|
1189
|
+
return `<!DOCTYPE html>
|
|
1190
|
+
<html lang="en">
|
|
1191
|
+
<head>
|
|
1192
|
+
<meta charset="utf-8">
|
|
1193
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1194
|
+
<title>vmsan VM ${vmId}</title>
|
|
1195
|
+
<style>
|
|
1196
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1197
|
+
body {
|
|
1198
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
1199
|
+
background: #0f172a;
|
|
1200
|
+
color: #e2e8f0;
|
|
1201
|
+
min-height: 100vh;
|
|
1202
|
+
display: flex;
|
|
1203
|
+
align-items: center;
|
|
1204
|
+
justify-content: center;
|
|
1205
|
+
padding: 2rem;
|
|
1206
|
+
}
|
|
1207
|
+
.container { max-width: 640px; width: 100%; }
|
|
1208
|
+
.header { text-align: center; margin-bottom: 2rem; }
|
|
1209
|
+
.logo {
|
|
1210
|
+
font-size: 2.5rem;
|
|
1211
|
+
font-weight: 800;
|
|
1212
|
+
background: linear-gradient(135deg, #f97316, #ef4444);
|
|
1213
|
+
-webkit-background-clip: text;
|
|
1214
|
+
-webkit-text-fill-color: transparent;
|
|
1215
|
+
background-clip: text;
|
|
1216
|
+
}
|
|
1217
|
+
.subtitle { color: #94a3b8; margin-top: 0.5rem; font-size: 1.1rem; }
|
|
1218
|
+
.card {
|
|
1219
|
+
background: #1e293b;
|
|
1220
|
+
border: 1px solid #334155;
|
|
1221
|
+
border-radius: 12px;
|
|
1222
|
+
padding: 1.5rem;
|
|
1223
|
+
margin-bottom: 1.25rem;
|
|
1224
|
+
}
|
|
1225
|
+
.card h2 { font-size: 1rem; color: #f97316; margin-bottom: 0.75rem; }
|
|
1226
|
+
.info-row { display: flex; justify-content: space-between; padding: 0.35rem 0; }
|
|
1227
|
+
.info-label { color: #94a3b8; }
|
|
1228
|
+
.info-value { font-family: monospace; color: #e2e8f0; }
|
|
1229
|
+
ul { list-style: none; }
|
|
1230
|
+
ul li { padding: 0.25rem 0; }
|
|
1231
|
+
code {
|
|
1232
|
+
background: #0f172a;
|
|
1233
|
+
border: 1px solid #334155;
|
|
1234
|
+
border-radius: 6px;
|
|
1235
|
+
padding: 0.2rem 0.5rem;
|
|
1236
|
+
font-size: 0.875rem;
|
|
1237
|
+
color: #f97316;
|
|
1238
|
+
}
|
|
1239
|
+
.steps li { padding: 0.5rem 0; color: #cbd5e1; }
|
|
1240
|
+
.steps li strong { color: #e2e8f0; }
|
|
1241
|
+
.footer { text-align: center; color: #475569; font-size: 0.85rem; margin-top: 1.5rem; }
|
|
1242
|
+
</style>
|
|
1243
|
+
</head>
|
|
1244
|
+
<body>
|
|
1245
|
+
<div class="container">
|
|
1246
|
+
<div class="header">
|
|
1247
|
+
<div class="logo">vmsan</div>
|
|
1248
|
+
<div class="subtitle">Your microVM is running</div>
|
|
1249
|
+
</div>
|
|
1250
|
+
<div class="card">
|
|
1251
|
+
<h2>VM Info</h2>
|
|
1252
|
+
<div class="info-row">
|
|
1253
|
+
<span class="info-label">VM ID</span>
|
|
1254
|
+
<span class="info-value">${vmId}</span>
|
|
1255
|
+
</div>
|
|
1256
|
+
<div class="info-row">
|
|
1257
|
+
<span class="info-label">Runtime</span>
|
|
1258
|
+
<span class="info-value">node22-demo</span>
|
|
1259
|
+
</div>
|
|
1260
|
+
<div class="info-row">
|
|
1261
|
+
<span class="info-label">Published Ports</span>
|
|
1262
|
+
<span class="info-value">${ports.join(", ")}</span>
|
|
1263
|
+
</div>
|
|
1264
|
+
</div>
|
|
1265
|
+
<div class="card">
|
|
1266
|
+
<h2>Next Steps</h2>
|
|
1267
|
+
<ul class="steps">
|
|
1268
|
+
<li><strong>Connect to the VM:</strong> <code>vmsan connect ${vmId}</code></li>
|
|
1269
|
+
<li><strong>Deploy your app:</strong> Replace this page by stopping the welcome service and running your own server on the published port(s).</li>
|
|
1270
|
+
<li><strong>Stop this page:</strong> <code>systemctl stop vmsan-welcome</code></li>
|
|
1271
|
+
</ul>
|
|
1272
|
+
</div>
|
|
1273
|
+
<div class="footer">Powered by vmsan · Firecracker microVMs</div>
|
|
1274
|
+
</div>
|
|
1275
|
+
</body>
|
|
1276
|
+
</html>`;
|
|
1277
|
+
}
|
|
1278
|
+
function generateWelcomeServer(ports) {
|
|
1279
|
+
return `"use strict";
|
|
1280
|
+
const http = require("node:http");
|
|
1281
|
+
const fs = require("node:fs");
|
|
1282
|
+
const path = require("node:path");
|
|
1283
|
+
|
|
1284
|
+
const html = fs.readFileSync(path.join(__dirname, "index.html"), "utf-8");
|
|
1285
|
+
|
|
1286
|
+
const server = http.createServer((req, res) => {
|
|
1287
|
+
res.writeHead(200, {
|
|
1288
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1289
|
+
"Cache-Control": "no-cache",
|
|
1290
|
+
});
|
|
1291
|
+
res.end(html);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
${ports.map((p) => `server.listen(${p}, "0.0.0.0", () => console.log("vmsan-welcome listening on 0.0.0.0:${p}"));`).join("\n")}
|
|
1295
|
+
`;
|
|
1296
|
+
}
|
|
1297
|
+
function generateWelcomeService(ports) {
|
|
1298
|
+
return `[Unit]
|
|
1299
|
+
Description=${`vmsan welcome page on port(s) ${ports.join(", ")}`}
|
|
1300
|
+
After=network.target
|
|
1301
|
+
|
|
1302
|
+
[Service]
|
|
1303
|
+
Type=simple
|
|
1304
|
+
ExecStart=/usr/local/bin/node /opt/vmsan/welcome/server.js
|
|
1305
|
+
Restart=on-failure
|
|
1306
|
+
RestartSec=2
|
|
1307
|
+
|
|
1308
|
+
[Install]
|
|
1309
|
+
WantedBy=multi-user.target
|
|
1310
|
+
`;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Template generators for the vmsan-agent systemd service.
|
|
1314
|
+
* Follows the same pattern as welcome-page.ts.
|
|
1315
|
+
*/
|
|
1316
|
+
function generateAgentService() {
|
|
1317
|
+
return `[Unit]
|
|
1318
|
+
Description=Vmsan VM Agent
|
|
1319
|
+
After=network.target
|
|
1320
|
+
|
|
1321
|
+
[Service]
|
|
1322
|
+
Type=simple
|
|
1323
|
+
ExecStart=/usr/local/bin/vmsan-agent
|
|
1324
|
+
EnvironmentFile=/etc/vmsan/agent.env
|
|
1325
|
+
Restart=always
|
|
1326
|
+
RestartSec=2
|
|
1327
|
+
|
|
1328
|
+
[Install]
|
|
1329
|
+
WantedBy=multi-user.target
|
|
1330
|
+
`;
|
|
1331
|
+
}
|
|
1332
|
+
function generateAgentEnv(token, port, vmId) {
|
|
1333
|
+
return `VMSAN_AGENT_TOKEN=${token}
|
|
1334
|
+
VMSAN_AGENT_PORT=${port}
|
|
1335
|
+
VMSAN_VM_ID=${vmId}
|
|
1336
|
+
VMSAN_DEFAULT_USER=ubuntu
|
|
1337
|
+
`;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Extra memory (in MiB) added to the cgroup limit beyond guest memory.
|
|
1341
|
+
* Covers Firecracker VMM process overhead, page tables, and kernel slab.
|
|
1342
|
+
* Without this, the OOM killer can terminate the VM under memory pressure.
|
|
1343
|
+
*/
|
|
1344
|
+
const CGROUP_VMM_OVERHEAD_MIB = 64;
|
|
1345
|
+
function detectCgroupVersion() {
|
|
1346
|
+
try {
|
|
1347
|
+
readFileSync("/sys/fs/cgroup/cgroup.controllers", "utf-8");
|
|
1348
|
+
return 2;
|
|
1349
|
+
} catch {
|
|
1350
|
+
return 1;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
var Jailer = class {
|
|
1354
|
+
paths;
|
|
1355
|
+
constructor(vmId, jailerBaseDir) {
|
|
1356
|
+
this.vmId = vmId;
|
|
1357
|
+
const chrootBase = jailerBaseDir;
|
|
1358
|
+
const chrootDir = join(chrootBase, "firecracker", vmId);
|
|
1359
|
+
const rootDir = join(chrootDir, "root");
|
|
1360
|
+
const kernelDir = join(rootDir, "kernel");
|
|
1361
|
+
const rootfsDir = join(rootDir, "rootfs");
|
|
1362
|
+
const socketDir = join(rootDir, "run");
|
|
1363
|
+
const snapshotDir = join(rootDir, "snapshot");
|
|
1364
|
+
this.paths = {
|
|
1365
|
+
chrootBase,
|
|
1366
|
+
chrootDir,
|
|
1367
|
+
rootDir,
|
|
1368
|
+
kernelDir,
|
|
1369
|
+
kernelPath: join(kernelDir, "vmlinux"),
|
|
1370
|
+
rootfsDir,
|
|
1371
|
+
rootfsPath: join(rootfsDir, "rootfs.ext4"),
|
|
1372
|
+
socketDir,
|
|
1373
|
+
socketPath: join(socketDir, "firecracker.socket"),
|
|
1374
|
+
snapshotDir
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
prepare(config) {
|
|
1378
|
+
const paths = this.paths;
|
|
1379
|
+
mkdirSync(paths.kernelDir, { recursive: true });
|
|
1380
|
+
mkdirSync(paths.rootfsDir, { recursive: true });
|
|
1381
|
+
mkdirSync(paths.socketDir, { recursive: true });
|
|
1382
|
+
if (!existsSync(paths.kernelPath)) linkSync(config.kernelSrc, paths.kernelPath);
|
|
1383
|
+
copyFileSync(config.rootfsSrc, paths.rootfsPath);
|
|
1384
|
+
if (typeof config.diskSizeGb === "number" && Number.isFinite(config.diskSizeGb)) {
|
|
1385
|
+
const targetBytes = Math.trunc(config.diskSizeGb * 1024 * 1024 * 1024);
|
|
1386
|
+
if (targetBytes > statSync(paths.rootfsPath).size) {
|
|
1387
|
+
execSync(`truncate -s ${targetBytes} "${paths.rootfsPath}"`, { stdio: "pipe" });
|
|
1388
|
+
execSync(`sudo e2fsck -fy "${paths.rootfsPath}"; [ $? -lt 4 ]`, { stdio: "pipe" });
|
|
1389
|
+
execSync(`sudo resize2fs "${paths.rootfsPath}"`, { stdio: "pipe" });
|
|
1390
|
+
execSync(`sudo tune2fs -m 0 "${paths.rootfsPath}"`, { stdio: "pipe" });
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const tmpMount = join(paths.rootDir, "tmp-mount");
|
|
1394
|
+
mkdirSync(tmpMount, { recursive: true });
|
|
1395
|
+
try {
|
|
1396
|
+
execSync(`sudo mount -o loop "${paths.rootfsPath}" "${tmpMount}"`, { stdio: "pipe" });
|
|
1397
|
+
execSync(`rm -f "${tmpMount}/etc/resolv.conf" && ln -s /proc/net/pnp "${tmpMount}/etc/resolv.conf"`, { stdio: "pipe" });
|
|
1398
|
+
if (config.welcomePage) {
|
|
1399
|
+
const { vmId: welcomeVmId, ports: welcomePorts } = config.welcomePage;
|
|
1400
|
+
const welcomeDir = join(tmpMount, "opt", "vmsan", "welcome");
|
|
1401
|
+
mkdirSync(welcomeDir, { recursive: true });
|
|
1402
|
+
writeFileSync(join(welcomeDir, "index.html"), generateWelcomeHtml(welcomeVmId, welcomePorts));
|
|
1403
|
+
writeFileSync(join(welcomeDir, "server.js"), generateWelcomeServer(welcomePorts));
|
|
1404
|
+
const systemdDir = join(tmpMount, "etc", "systemd", "system");
|
|
1405
|
+
mkdirSync(systemdDir, { recursive: true });
|
|
1406
|
+
writeFileSync(join(systemdDir, "vmsan-welcome.service"), generateWelcomeService(welcomePorts));
|
|
1407
|
+
const wantsDir = join(systemdDir, "multi-user.target.wants");
|
|
1408
|
+
mkdirSync(wantsDir, { recursive: true });
|
|
1409
|
+
execSync(`ln -sf /etc/systemd/system/vmsan-welcome.service "${join(wantsDir, "vmsan-welcome.service")}"`, { stdio: "pipe" });
|
|
1410
|
+
}
|
|
1411
|
+
if (config.agent) {
|
|
1412
|
+
const agentDst = join(tmpMount, "usr", "local", "bin", "vmsan-agent");
|
|
1413
|
+
mkdirSync(join(tmpMount, "usr", "local", "bin"), { recursive: true });
|
|
1414
|
+
copyFileSync(config.agent.binaryPath, agentDst);
|
|
1415
|
+
execSync(`chmod 755 "${agentDst}"`, { stdio: "pipe" });
|
|
1416
|
+
const envDir = join(tmpMount, "etc", "vmsan");
|
|
1417
|
+
mkdirSync(envDir, { recursive: true });
|
|
1418
|
+
writeFileSync(join(envDir, "agent.env"), generateAgentEnv(config.agent.token, config.agent.port, config.agent.vmId));
|
|
1419
|
+
const systemdDir = join(tmpMount, "etc", "systemd", "system");
|
|
1420
|
+
mkdirSync(systemdDir, { recursive: true });
|
|
1421
|
+
writeFileSync(join(systemdDir, "vmsan-agent.service"), generateAgentService());
|
|
1422
|
+
const wantsDir = join(systemdDir, "multi-user.target.wants");
|
|
1423
|
+
mkdirSync(wantsDir, { recursive: true });
|
|
1424
|
+
execSync(`ln -sf /etc/systemd/system/vmsan-agent.service "${join(wantsDir, "vmsan-agent.service")}"`, { stdio: "pipe" });
|
|
1425
|
+
}
|
|
1426
|
+
execSync(`sudo umount "${tmpMount}"`, { stdio: "pipe" });
|
|
1427
|
+
} catch {
|
|
1428
|
+
try {
|
|
1429
|
+
execSync(`sudo umount "${tmpMount}" 2>/dev/null`, { stdio: "pipe" });
|
|
1430
|
+
} catch {}
|
|
1431
|
+
}
|
|
1432
|
+
try {
|
|
1433
|
+
execSync(`rm -rf "${tmpMount}"`, { stdio: "pipe" });
|
|
1434
|
+
} catch {}
|
|
1435
|
+
if (config.snapshot) {
|
|
1436
|
+
mkdirSync(paths.snapshotDir, { recursive: true });
|
|
1437
|
+
copyFileSync(config.snapshot.snapshotFile, join(paths.snapshotDir, "snapshot_file"));
|
|
1438
|
+
copyFileSync(config.snapshot.memFile, join(paths.snapshotDir, "mem_file"));
|
|
1439
|
+
}
|
|
1440
|
+
return paths;
|
|
1441
|
+
}
|
|
1442
|
+
spawn(config) {
|
|
1443
|
+
const uid = config.uid ?? 0;
|
|
1444
|
+
const gid = config.gid ?? 0;
|
|
1445
|
+
const args = [
|
|
1446
|
+
config.jailerBin,
|
|
1447
|
+
"--exec-file",
|
|
1448
|
+
config.firecrackerBin,
|
|
1449
|
+
"--id",
|
|
1450
|
+
this.vmId,
|
|
1451
|
+
"--uid",
|
|
1452
|
+
String(uid),
|
|
1453
|
+
"--gid",
|
|
1454
|
+
String(gid),
|
|
1455
|
+
"--chroot-base-dir",
|
|
1456
|
+
config.chrootBase,
|
|
1457
|
+
"--daemonize"
|
|
1458
|
+
];
|
|
1459
|
+
if (config.newPidNs !== false) args.push("--new-pid-ns");
|
|
1460
|
+
if (config.netns) args.push("--netns", `/var/run/netns/${config.netns}`);
|
|
1461
|
+
if (config.cgroup) if (detectCgroupVersion() === 2) {
|
|
1462
|
+
args.push("--cgroup-version", "2");
|
|
1463
|
+
args.push("--cgroup", `cpu.max=${config.cgroup.cpuQuotaUs} ${config.cgroup.cpuPeriodUs}`);
|
|
1464
|
+
args.push("--cgroup", `memory.max=${config.cgroup.memoryBytes}`);
|
|
1465
|
+
} else {
|
|
1466
|
+
args.push("--cgroup", `cpu.cfs_quota_us=${config.cgroup.cpuQuotaUs}`);
|
|
1467
|
+
args.push("--cgroup", `cpu.cfs_period_us=${config.cgroup.cpuPeriodUs}`);
|
|
1468
|
+
args.push("--cgroup", `memory.limit_in_bytes=${config.cgroup.memoryBytes}`);
|
|
1469
|
+
}
|
|
1470
|
+
args.push("--", "--api-sock", "run/firecracker.socket");
|
|
1471
|
+
if (config.seccompFilter && existsSync(config.seccompFilter)) args.push("--seccomp-filter", config.seccompFilter);
|
|
1472
|
+
else args.push("--no-seccomp");
|
|
1473
|
+
execFileSync("sudo", args, { stdio: "pipe" });
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
function validateEnvironment(baseDir) {
|
|
1477
|
+
const firecrackerPath = join(baseDir, "bin", "firecracker");
|
|
1478
|
+
const jailerPath = join(baseDir, "bin", "jailer");
|
|
1479
|
+
if (!existsSync(firecrackerPath)) throw missingBinaryError("Firecracker", firecrackerPath);
|
|
1480
|
+
if (!existsSync(jailerPath)) throw missingBinaryError("Jailer", jailerPath);
|
|
1481
|
+
}
|
|
1482
|
+
function findKernel(baseDir) {
|
|
1483
|
+
const kernelDir = join(baseDir, "kernels");
|
|
1484
|
+
if (!existsSync(kernelDir)) throw noKernelDirError();
|
|
1485
|
+
const files = readdirSync(kernelDir).filter((fileName) => fileName.startsWith("vmlinux"));
|
|
1486
|
+
if (files.length === 0) throw noKernelError();
|
|
1487
|
+
return join(kernelDir, files.sort().at(-1));
|
|
1488
|
+
}
|
|
1489
|
+
function findRootfs(baseDir) {
|
|
1490
|
+
const rootfsDir = join(baseDir, "rootfs");
|
|
1491
|
+
if (!existsSync(rootfsDir)) throw noRootfsDirError();
|
|
1492
|
+
const files = readdirSync(rootfsDir).filter((fileName) => fileName.endsWith(".ext4"));
|
|
1493
|
+
if (files.length === 0) throw noExt4RootfsError();
|
|
1494
|
+
return join(rootfsDir, files.sort().at(-1));
|
|
1495
|
+
}
|
|
1496
|
+
async function waitForSocket(socketPath, timeoutMs = 5e3) {
|
|
1497
|
+
const start = Date.now();
|
|
1498
|
+
while (Date.now() - start < timeoutMs) {
|
|
1499
|
+
if (existsSync(socketPath)) {
|
|
1500
|
+
if (await new Promise((resolve) => {
|
|
1501
|
+
const socket = connect(socketPath);
|
|
1502
|
+
socket.on("connect", () => {
|
|
1503
|
+
socket.destroy();
|
|
1504
|
+
resolve(true);
|
|
1505
|
+
});
|
|
1506
|
+
socket.on("error", () => resolve(false));
|
|
1507
|
+
})) return;
|
|
1508
|
+
}
|
|
1509
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1510
|
+
}
|
|
1511
|
+
throw socketTimeoutError(socketPath);
|
|
1512
|
+
}
|
|
1513
|
+
function getVmPid(vmId) {
|
|
1514
|
+
try {
|
|
1515
|
+
const entries = readdirSync("/proc").filter((entry) => /^\d+$/.test(entry));
|
|
1516
|
+
for (const entry of entries) try {
|
|
1517
|
+
const cmdline = readFileSync(`/proc/${entry}/cmdline`, "utf-8");
|
|
1518
|
+
if (cmdline.includes("firecracker") && cmdline.includes(vmId)) return Number(entry);
|
|
1519
|
+
} catch {}
|
|
1520
|
+
} catch {}
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
function getVmJailerPid(vmId) {
|
|
1524
|
+
try {
|
|
1525
|
+
const entries = readdirSync("/proc").filter((entry) => /^\d+$/.test(entry));
|
|
1526
|
+
for (const entry of entries) try {
|
|
1527
|
+
const cmdline = readFileSync(`/proc/${entry}/cmdline`, "utf-8");
|
|
1528
|
+
if (cmdline.includes("jailer") && cmdline.includes(vmId)) return Number(entry);
|
|
1529
|
+
} catch {}
|
|
1530
|
+
} catch {}
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
function assertSnapshotExists(snapshotId, paths) {
|
|
1534
|
+
const snapshotDir = join(paths.snapshotsDir, snapshotId);
|
|
1535
|
+
if (!existsSync(join(snapshotDir, "snapshot_file")) || !existsSync(join(snapshotDir, "mem_file"))) throw snapshotNotFoundError(snapshotId);
|
|
1536
|
+
}
|
|
1537
|
+
function killOrphanVmProcess(vmId) {
|
|
1538
|
+
const orphanPid = getVmPid(vmId);
|
|
1539
|
+
const orphanJailerPid = getVmJailerPid(vmId);
|
|
1540
|
+
if (orphanPid) safeKill(orphanPid, "SIGKILL");
|
|
1541
|
+
if (orphanJailerPid) safeKill(orphanJailerPid, "SIGKILL");
|
|
1542
|
+
}
|
|
1543
|
+
function markVmAsError(vmId, error, paths) {
|
|
1544
|
+
try {
|
|
1545
|
+
new FileVmStateStore(paths.vmsDir).update(vmId, {
|
|
1546
|
+
status: "error",
|
|
1547
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1548
|
+
});
|
|
1549
|
+
} catch {}
|
|
1550
|
+
}
|
|
1551
|
+
function cleanupNetwork(networkConfig) {
|
|
1552
|
+
if (!networkConfig) return;
|
|
1553
|
+
try {
|
|
1554
|
+
NetworkManager.fromConfig(networkConfig).teardown();
|
|
1555
|
+
} catch {}
|
|
1556
|
+
}
|
|
1557
|
+
function cleanupChroot(chrootDir) {
|
|
1558
|
+
if (!chrootDir) return;
|
|
1559
|
+
const vmJailerDir = dirname(chrootDir);
|
|
1560
|
+
try {
|
|
1561
|
+
rmSync(chrootDir, {
|
|
1562
|
+
recursive: true,
|
|
1563
|
+
force: true
|
|
1564
|
+
});
|
|
1565
|
+
} catch {}
|
|
1566
|
+
try {
|
|
1567
|
+
rmSync(vmJailerDir, {
|
|
1568
|
+
recursive: true,
|
|
1569
|
+
force: true
|
|
1570
|
+
});
|
|
1571
|
+
} catch {}
|
|
1572
|
+
}
|
|
1573
|
+
function buildInitialVmState(input) {
|
|
1574
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1575
|
+
return {
|
|
1576
|
+
id: input.vmId,
|
|
1577
|
+
project: input.project,
|
|
1578
|
+
runtime: input.runtime,
|
|
1579
|
+
diskSizeGb: input.diskSizeGb,
|
|
1580
|
+
status: "creating",
|
|
1581
|
+
pid: null,
|
|
1582
|
+
apiSocket: "",
|
|
1583
|
+
chrootDir: "",
|
|
1584
|
+
kernel: input.kernelPath,
|
|
1585
|
+
rootfs: input.rootfsPath,
|
|
1586
|
+
vcpuCount: input.vcpus,
|
|
1587
|
+
memSizeMib: input.memMib,
|
|
1588
|
+
network: {
|
|
1589
|
+
tapDevice: input.tapDevice,
|
|
1590
|
+
hostIp: input.hostIp,
|
|
1591
|
+
guestIp: input.guestIp,
|
|
1592
|
+
subnetMask: input.subnetMask,
|
|
1593
|
+
macAddress: input.macAddress,
|
|
1594
|
+
networkPolicy: input.networkPolicy,
|
|
1595
|
+
allowedDomains: input.domains,
|
|
1596
|
+
allowedCidrs: input.allowedCidrs,
|
|
1597
|
+
deniedCidrs: input.deniedCidrs,
|
|
1598
|
+
publishedPorts: input.ports,
|
|
1599
|
+
tunnelHostname: null,
|
|
1600
|
+
tunnelHostnames: [],
|
|
1601
|
+
bandwidthMbit: input.bandwidthMbit,
|
|
1602
|
+
netnsName: input.netnsName
|
|
1603
|
+
},
|
|
1604
|
+
snapshot: input.snapshotId,
|
|
1605
|
+
timeoutMs: input.timeoutMs,
|
|
1606
|
+
timeoutAt: input.timeoutMs ? new Date(Date.now() + input.timeoutMs).toISOString() : null,
|
|
1607
|
+
createdAt: now,
|
|
1608
|
+
error: null,
|
|
1609
|
+
agentToken: input.agentToken,
|
|
1610
|
+
agentPort: input.agentPort
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function dockerUnavailableError() {
|
|
1614
|
+
return /* @__PURE__ */ new Error("Docker is not available. Install Docker and ensure the daemon is running.");
|
|
1615
|
+
}
|
|
1616
|
+
const APT_PACKAGES = [
|
|
1617
|
+
"bind9-utils",
|
|
1618
|
+
"bzip2",
|
|
1619
|
+
"findutils",
|
|
1620
|
+
"git",
|
|
1621
|
+
"gzip",
|
|
1622
|
+
"iputils-ping",
|
|
1623
|
+
"libicu-dev",
|
|
1624
|
+
"libjpeg-dev",
|
|
1625
|
+
"libpng-dev",
|
|
1626
|
+
"ncurses-base",
|
|
1627
|
+
"libssl-dev",
|
|
1628
|
+
"openssh-server",
|
|
1629
|
+
"openssl",
|
|
1630
|
+
"procps",
|
|
1631
|
+
"sudo",
|
|
1632
|
+
"tar",
|
|
1633
|
+
"unzip",
|
|
1634
|
+
"debianutils",
|
|
1635
|
+
"whois",
|
|
1636
|
+
"zstd"
|
|
1637
|
+
];
|
|
1638
|
+
const DNF_PACKAGES = [
|
|
1639
|
+
"bind-utils",
|
|
1640
|
+
"bzip2",
|
|
1641
|
+
"findutils",
|
|
1642
|
+
"git",
|
|
1643
|
+
"gzip",
|
|
1644
|
+
"iputils",
|
|
1645
|
+
"libicu",
|
|
1646
|
+
"libjpeg",
|
|
1647
|
+
"libpng",
|
|
1648
|
+
"ncurses-libs",
|
|
1649
|
+
"openssh-server",
|
|
1650
|
+
"openssl",
|
|
1651
|
+
"openssl-libs",
|
|
1652
|
+
"procps",
|
|
1653
|
+
"sudo",
|
|
1654
|
+
"tar",
|
|
1655
|
+
"unzip",
|
|
1656
|
+
"which",
|
|
1657
|
+
"whois",
|
|
1658
|
+
"zstd"
|
|
1659
|
+
];
|
|
1660
|
+
const APK_PACKAGES = [
|
|
1661
|
+
"bash",
|
|
1662
|
+
"bind-tools",
|
|
1663
|
+
"bzip2",
|
|
1664
|
+
"findutils",
|
|
1665
|
+
"git",
|
|
1666
|
+
"gzip",
|
|
1667
|
+
"iputils",
|
|
1668
|
+
"icu-libs",
|
|
1669
|
+
"libjpeg-turbo",
|
|
1670
|
+
"libpng",
|
|
1671
|
+
"ncurses-libs",
|
|
1672
|
+
"openrc",
|
|
1673
|
+
"openssh",
|
|
1674
|
+
"openssl",
|
|
1675
|
+
"procps",
|
|
1676
|
+
"sudo",
|
|
1677
|
+
"tar",
|
|
1678
|
+
"unzip",
|
|
1679
|
+
"whois",
|
|
1680
|
+
"zstd"
|
|
1681
|
+
];
|
|
1682
|
+
function generateDockerfile(baseImage) {
|
|
1683
|
+
return `FROM ${baseImage}
|
|
1684
|
+
RUN if command -v apt-get >/dev/null 2>&1; then ${`apt-get update && apt-get install -y --no-install-recommends ${APT_PACKAGES.join(" ")} && rm -rf /var/lib/apt/lists/*`}; \\
|
|
1685
|
+
elif command -v dnf >/dev/null 2>&1; then ${`dnf install -y ${DNF_PACKAGES.join(" ")} && dnf clean all`}; \\
|
|
1686
|
+
elif command -v yum >/dev/null 2>&1; then ${`yum install -y ${DNF_PACKAGES.join(" ")} && yum clean all`}; \\
|
|
1687
|
+
elif command -v apk >/dev/null 2>&1; then ${`apk add --no-cache ${APK_PACKAGES.join(" ")}`}; \\
|
|
1688
|
+
fi
|
|
1689
|
+
RUN if command -v apk >/dev/null 2>&1; then \\
|
|
1690
|
+
id -u ubuntu >/dev/null 2>&1 || adduser -D -s /bin/bash ubuntu; \\
|
|
1691
|
+
else \\
|
|
1692
|
+
id -u ubuntu >/dev/null 2>&1 || useradd -m -s /bin/bash ubuntu; \\
|
|
1693
|
+
fi; \\
|
|
1694
|
+
echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu; \\
|
|
1695
|
+
chmod 440 /etc/sudoers.d/ubuntu; \\
|
|
1696
|
+
mkdir -p /home/ubuntu/.ssh && chown -R ubuntu:ubuntu /home/ubuntu
|
|
1697
|
+
RUN ssh-keygen -A 2>/dev/null || true; \\
|
|
1698
|
+
mkdir -p /root/.ssh && chmod 700 /root/.ssh; \\
|
|
1699
|
+
if [ -f /etc/ssh/sshd_config ]; then \\
|
|
1700
|
+
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config; \\
|
|
1701
|
+
fi; \\
|
|
1702
|
+
if command -v rc-update >/dev/null 2>&1; then \\
|
|
1703
|
+
rc-update add devfs sysinit 2>/dev/null || true; \\
|
|
1704
|
+
rc-update add mdev sysinit 2>/dev/null || true; \\
|
|
1705
|
+
rc-update add hwdrivers sysinit 2>/dev/null || true; \\
|
|
1706
|
+
rc-update add modules boot 2>/dev/null || true; \\
|
|
1707
|
+
rc-update add sysctl boot 2>/dev/null || true; \\
|
|
1708
|
+
rc-update add hostname boot 2>/dev/null || true; \\
|
|
1709
|
+
rc-update add bootmisc boot 2>/dev/null || true; \\
|
|
1710
|
+
rc-update add networking boot 2>/dev/null || true; \\
|
|
1711
|
+
rc-update add sshd default 2>/dev/null || true; \\
|
|
1712
|
+
printf '%s\\n' '::sysinit:/sbin/openrc sysinit' '::sysinit:/sbin/openrc boot' '::wait:/sbin/openrc default' '::shutdown:/sbin/openrc shutdown' 'ttyS0::respawn:/sbin/getty 115200 ttyS0' > /etc/inittab; \\
|
|
1713
|
+
fi; \\
|
|
1714
|
+
if command -v systemctl >/dev/null 2>&1; then systemctl enable sshd 2>/dev/null || systemctl enable ssh 2>/dev/null || true; fi
|
|
1715
|
+
`;
|
|
1716
|
+
}
|
|
1717
|
+
function verifyDocker() {
|
|
1718
|
+
try {
|
|
1719
|
+
execSync("docker info", { stdio: "pipe" });
|
|
1720
|
+
} catch {
|
|
1721
|
+
throw dockerUnavailableError();
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
function buildImageRootfs(imageRef, cacheDir) {
|
|
1725
|
+
const ext4Path = join(cacheDir, "rootfs.ext4");
|
|
1726
|
+
verifyDocker();
|
|
1727
|
+
const buildTag = `vmsan-rootfs-${imageRef.name.replace(/[^a-z0-9._-]/gi, "-")}:${imageRef.tag}`;
|
|
1728
|
+
const containerName = `vmsan-export-${Date.now()}`;
|
|
1729
|
+
const tmpTar = join(cacheDir, "rootfs.tar");
|
|
1730
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
1731
|
+
try {
|
|
1732
|
+
consola.start(`Building image from ${imageRef.full}...`);
|
|
1733
|
+
execSync(`docker build -t "${buildTag}" -f - . <<'DOCKERFILE'\n${generateDockerfile(imageRef.full)}\nDOCKERFILE`, {
|
|
1734
|
+
stdio: "pipe",
|
|
1735
|
+
shell: "/bin/bash"
|
|
1736
|
+
});
|
|
1737
|
+
consola.start("Exporting filesystem...");
|
|
1738
|
+
execSync(`docker create --name "${containerName}" "${buildTag}"`, { stdio: "pipe" });
|
|
1739
|
+
execSync(`docker export "${containerName}" -o "${tmpTar}"`, { stdio: "pipe" });
|
|
1740
|
+
consola.start("Converting to ext4...");
|
|
1741
|
+
const tarSizeOutput = execSync(`stat -c %s "${tmpTar}"`, { encoding: "utf-8" }).trim();
|
|
1742
|
+
const tarMb = Number(tarSizeOutput) / 1024 / 1024;
|
|
1743
|
+
const imageSizeMb = Math.max(1024, Math.ceil(tarMb + 512));
|
|
1744
|
+
execSync(`dd if=/dev/zero of="${ext4Path}" bs=1M count=${imageSizeMb} 2>/dev/null`, { stdio: "pipe" });
|
|
1745
|
+
execSync(`mkfs.ext4 -q "${ext4Path}"`, { stdio: "pipe" });
|
|
1746
|
+
execSync(`tune2fs -m 0 "${ext4Path}"`, { stdio: "pipe" });
|
|
1747
|
+
const tmpMount = join(cacheDir, "mnt");
|
|
1748
|
+
mkdirSync(tmpMount, { recursive: true });
|
|
1749
|
+
execSync(`mount -o loop "${ext4Path}" "${tmpMount}"`, { stdio: "pipe" });
|
|
1750
|
+
try {
|
|
1751
|
+
execSync(`tar -xf "${tmpTar}" -C "${tmpMount}"`, { stdio: "pipe" });
|
|
1752
|
+
} finally {
|
|
1753
|
+
execSync(`umount "${tmpMount}"`, { stdio: "pipe" });
|
|
1754
|
+
execSync(`rm -rf "${tmpMount}"`, { stdio: "pipe" });
|
|
1755
|
+
}
|
|
1756
|
+
writeFileSync(join(cacheDir, "metadata.json"), JSON.stringify({
|
|
1757
|
+
image: imageRef.full,
|
|
1758
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1759
|
+
}, null, 2));
|
|
1760
|
+
consola.success(`Rootfs built from ${imageRef.full} (${imageSizeMb} MB)`);
|
|
1761
|
+
return ext4Path;
|
|
1762
|
+
} finally {
|
|
1763
|
+
try {
|
|
1764
|
+
execSync(`docker rm -f "${containerName}" 2>/dev/null`, { stdio: "pipe" });
|
|
1765
|
+
} catch {}
|
|
1766
|
+
try {
|
|
1767
|
+
execSync(`rm -f "${tmpTar}"`, { stdio: "pipe" });
|
|
1768
|
+
} catch {}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function resolveImageRootfs(imageRef, registryDir) {
|
|
1772
|
+
const cacheDir = join(registryDir, imageRef.cacheKey);
|
|
1773
|
+
const ext4Path = join(cacheDir, "rootfs.ext4");
|
|
1774
|
+
if (existsSync(ext4Path)) {
|
|
1775
|
+
consola.info(`Using cached rootfs for ${imageRef.full}`);
|
|
1776
|
+
return ext4Path;
|
|
1777
|
+
}
|
|
1778
|
+
return buildImageRootfs(imageRef, cacheDir);
|
|
1779
|
+
}
|
|
1780
|
+
const _dirname = dirname(fileURLToPath(import.meta.url));
|
|
1781
|
+
const VALID_ARCHES = ["x86_64", "aarch64"];
|
|
1782
|
+
const MAX_FILTER_SIZE = 1048576;
|
|
1783
|
+
/**
|
|
1784
|
+
* Compile a Firecracker seccomp JSON filter to BPF using seccompiler-bin.
|
|
1785
|
+
* Falls back to using the JSON filter directly if seccompiler-bin is not available.
|
|
1786
|
+
*/
|
|
1787
|
+
function compileSeccompFilter(jsonPath, outputPath, arch) {
|
|
1788
|
+
const targetArch = arch ?? "x86_64";
|
|
1789
|
+
if (!VALID_ARCHES.includes(targetArch)) throw new Error(`unsupported seccomp arch: ${targetArch} (allowed: ${VALID_ARCHES.join(", ")})`);
|
|
1790
|
+
const stat = statSync(jsonPath);
|
|
1791
|
+
if (stat.size > MAX_FILTER_SIZE) throw new Error(`seccomp filter too large: ${stat.size} bytes (max ${MAX_FILTER_SIZE})`);
|
|
1792
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
1793
|
+
execFileSync("seccompiler-bin", [
|
|
1794
|
+
"--input-file",
|
|
1795
|
+
jsonPath,
|
|
1796
|
+
"--target-arch",
|
|
1797
|
+
targetArch,
|
|
1798
|
+
"--output-file",
|
|
1799
|
+
outputPath
|
|
1800
|
+
], { stdio: "pipe" });
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Ensure a seccomp filter is available for Firecracker.
|
|
1804
|
+
*
|
|
1805
|
+
* 1. If a compiled BPF exists at paths.seccompDir/default.bpf, return it.
|
|
1806
|
+
* 2. If the JSON source exists, try to compile it; return BPF path on success.
|
|
1807
|
+
* 3. If compilation fails (seccompiler-bin not installed), return null
|
|
1808
|
+
* (Firecracker requires compiled BPF, not raw JSON).
|
|
1809
|
+
* 4. If no filter source exists at all, return null.
|
|
1810
|
+
*/
|
|
1811
|
+
function ensureSeccompFilter(paths) {
|
|
1812
|
+
const bpfPath = join(paths.seccompDir, "default.bpf");
|
|
1813
|
+
if (existsSync(bpfPath)) {
|
|
1814
|
+
consola.debug(`seccomp: using compiled BPF filter at ${bpfPath}`);
|
|
1815
|
+
return bpfPath;
|
|
1816
|
+
}
|
|
1817
|
+
const bundledJson = join(dirname(dirname(_dirname)), "seccomp", "default.json");
|
|
1818
|
+
const userJson = paths.seccompFilter;
|
|
1819
|
+
let sourceJson = null;
|
|
1820
|
+
try {
|
|
1821
|
+
const mode = statSync(userJson).mode;
|
|
1822
|
+
if (mode & 18) consola.warn(`seccomp: filter at ${userJson} is group/world writable (mode ${(mode & 511).toString(8)}); consider restricting permissions`);
|
|
1823
|
+
consola.debug(`seccomp: using user filter at ${userJson}`);
|
|
1824
|
+
sourceJson = userJson;
|
|
1825
|
+
} catch {}
|
|
1826
|
+
if (!sourceJson && existsSync(bundledJson)) {
|
|
1827
|
+
mkdirSync(paths.seccompDir, { recursive: true });
|
|
1828
|
+
copyFileSync(bundledJson, userJson);
|
|
1829
|
+
consola.debug(`seccomp: copied bundled filter to ${userJson}`);
|
|
1830
|
+
sourceJson = userJson;
|
|
1831
|
+
}
|
|
1832
|
+
if (!sourceJson) return null;
|
|
1833
|
+
try {
|
|
1834
|
+
compileSeccompFilter(sourceJson, bpfPath);
|
|
1835
|
+
consola.debug(`seccomp: compiled BPF filter at ${bpfPath}`);
|
|
1836
|
+
return bpfPath;
|
|
1837
|
+
} catch {
|
|
1838
|
+
consola.warn("seccomp: BPF compilation failed (seccompiler-bin not available?); seccomp filtering disabled. Install seccompiler-bin for seccomp support.");
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
var VMService = class {
|
|
1843
|
+
paths;
|
|
1844
|
+
store;
|
|
1845
|
+
hooks;
|
|
1846
|
+
logger;
|
|
1847
|
+
constructor(ctx) {
|
|
1848
|
+
this.paths = ctx.paths;
|
|
1849
|
+
this.store = ctx.store;
|
|
1850
|
+
this.hooks = ctx.hooks;
|
|
1851
|
+
this.logger = ctx.logger;
|
|
1852
|
+
}
|
|
1853
|
+
list() {
|
|
1854
|
+
return this.store.list().sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1855
|
+
}
|
|
1856
|
+
get(vmId) {
|
|
1857
|
+
return this.store.load(vmId);
|
|
1858
|
+
}
|
|
1859
|
+
async create(opts) {
|
|
1860
|
+
const { logger, paths, hooks } = this;
|
|
1861
|
+
const vmId = generateVmId();
|
|
1862
|
+
const log = logger.withTag(vmId);
|
|
1863
|
+
let networkConfig;
|
|
1864
|
+
let chrootDir;
|
|
1865
|
+
try {
|
|
1866
|
+
validateEnvironment(paths.baseDir);
|
|
1867
|
+
if (opts.fromImage && opts.rootfsPath) throw mutuallyExclusiveFlagsError("--from-image", "--rootfs");
|
|
1868
|
+
const vcpus = opts.vcpus ?? 1;
|
|
1869
|
+
const memMib = opts.memMib ?? 128;
|
|
1870
|
+
const diskSizeGb = opts.diskSizeGb ?? 10;
|
|
1871
|
+
const runtime = opts.runtime ?? "base";
|
|
1872
|
+
const networkPolicy = opts.networkPolicy ?? "allow-all";
|
|
1873
|
+
const domains = opts.domains ?? [];
|
|
1874
|
+
const allowedCidrs = opts.allowedCidrs ?? [];
|
|
1875
|
+
const deniedCidrs = opts.deniedCidrs ?? [];
|
|
1876
|
+
const ports = opts.ports ?? [];
|
|
1877
|
+
const bandwidthMbit = opts.bandwidthMbit;
|
|
1878
|
+
const snapshotId = opts.snapshotId ?? null;
|
|
1879
|
+
const timeoutMs = opts.timeoutMs ?? null;
|
|
1880
|
+
await hooks.callHook("vm:beforeCreate", {
|
|
1881
|
+
vmId,
|
|
1882
|
+
options: opts
|
|
1883
|
+
});
|
|
1884
|
+
const kernelPath = opts.kernelPath ?? findKernel(paths.baseDir);
|
|
1885
|
+
logger.debug(`Kernel resolved: ${kernelPath}`);
|
|
1886
|
+
let rootfsPath;
|
|
1887
|
+
if (opts.fromImage) rootfsPath = resolveImageRootfs(opts.fromImage, paths.registryDir);
|
|
1888
|
+
else rootfsPath = opts.rootfsPath ?? findRootfs(paths.baseDir);
|
|
1889
|
+
logger.debug(`Rootfs resolved: ${rootfsPath}`);
|
|
1890
|
+
const netnsName = opts.disableNetns ? void 0 : `vmsan-${vmId}`;
|
|
1891
|
+
const agentToken = existsSync(paths.agentBin) ? randomBytes(32).toString("hex") : null;
|
|
1892
|
+
log.start(`Creating VM ${vmId}...`);
|
|
1893
|
+
const { net } = new FileLock(join(paths.vmsDir, ".slot-lock"), "slot-alloc").run(() => {
|
|
1894
|
+
const slot = this.store.allocateNetworkSlot();
|
|
1895
|
+
logger.debug(`Network slot allocated: ${slot}`);
|
|
1896
|
+
const net = new NetworkManager(slot, networkPolicy, domains, allowedCidrs, deniedCidrs, ports, bandwidthMbit, netnsName);
|
|
1897
|
+
networkConfig = net.config;
|
|
1898
|
+
const state = buildInitialVmState({
|
|
1899
|
+
vmId,
|
|
1900
|
+
project: opts.project || "",
|
|
1901
|
+
runtime,
|
|
1902
|
+
diskSizeGb,
|
|
1903
|
+
kernelPath,
|
|
1904
|
+
rootfsPath,
|
|
1905
|
+
vcpus,
|
|
1906
|
+
memMib,
|
|
1907
|
+
networkPolicy,
|
|
1908
|
+
domains,
|
|
1909
|
+
allowedCidrs,
|
|
1910
|
+
deniedCidrs,
|
|
1911
|
+
ports,
|
|
1912
|
+
tapDevice: net.config.tapDevice,
|
|
1913
|
+
hostIp: net.config.hostIp,
|
|
1914
|
+
guestIp: net.config.guestIp,
|
|
1915
|
+
subnetMask: net.config.subnetMask,
|
|
1916
|
+
macAddress: net.config.macAddress,
|
|
1917
|
+
snapshotId,
|
|
1918
|
+
timeoutMs,
|
|
1919
|
+
agentToken,
|
|
1920
|
+
agentPort: paths.agentPort,
|
|
1921
|
+
bandwidthMbit,
|
|
1922
|
+
netnsName
|
|
1923
|
+
});
|
|
1924
|
+
this.store.save(state);
|
|
1925
|
+
return { net };
|
|
1926
|
+
});
|
|
1927
|
+
const netCfg = networkConfig;
|
|
1928
|
+
log.start("Setting up networking...");
|
|
1929
|
+
await net.setup();
|
|
1930
|
+
log.success(`Network: TAP ${netCfg.tapDevice}, Host ${netCfg.hostIp}, Guest ${netCfg.guestIp}`);
|
|
1931
|
+
await hooks.callHook("network:afterSetup", {
|
|
1932
|
+
vmId,
|
|
1933
|
+
slot: netCfg.slot,
|
|
1934
|
+
networkConfig: netCfg,
|
|
1935
|
+
domains,
|
|
1936
|
+
networkPolicy
|
|
1937
|
+
});
|
|
1938
|
+
log.start("Preparing chroot...");
|
|
1939
|
+
const snapshotConfig = snapshotId ? {
|
|
1940
|
+
snapshotFile: join(paths.snapshotsDir, snapshotId, "snapshot_file"),
|
|
1941
|
+
memFile: join(paths.snapshotsDir, snapshotId, "mem_file")
|
|
1942
|
+
} : void 0;
|
|
1943
|
+
const jailer = new Jailer(vmId, paths.jailerBaseDir);
|
|
1944
|
+
const welcomePage = runtime === "node22-demo" && ports.length > 0 ? {
|
|
1945
|
+
vmId,
|
|
1946
|
+
ports
|
|
1947
|
+
} : void 0;
|
|
1948
|
+
const agentConfig = agentToken ? {
|
|
1949
|
+
binaryPath: paths.agentBin,
|
|
1950
|
+
token: agentToken,
|
|
1951
|
+
port: paths.agentPort,
|
|
1952
|
+
vmId
|
|
1953
|
+
} : void 0;
|
|
1954
|
+
const jailerPaths = jailer.prepare({
|
|
1955
|
+
kernelSrc: kernelPath,
|
|
1956
|
+
rootfsSrc: rootfsPath,
|
|
1957
|
+
diskSizeGb,
|
|
1958
|
+
snapshot: snapshotConfig,
|
|
1959
|
+
welcomePage,
|
|
1960
|
+
agent: agentConfig
|
|
1961
|
+
});
|
|
1962
|
+
chrootDir = jailerPaths.chrootDir;
|
|
1963
|
+
this.store.update(vmId, {
|
|
1964
|
+
chrootDir: jailerPaths.chrootDir,
|
|
1965
|
+
apiSocket: jailerPaths.socketPath
|
|
1966
|
+
});
|
|
1967
|
+
logger.debug(`Jailer chroot: ${jailerPaths.chrootDir}`);
|
|
1968
|
+
logger.debug(`API socket path: ${jailerPaths.socketPath}`);
|
|
1969
|
+
log.start("Spawning Firecracker via jailer...");
|
|
1970
|
+
const firecrackerBin = join(paths.binDir, "firecracker");
|
|
1971
|
+
const jailerBin = join(paths.binDir, "jailer");
|
|
1972
|
+
const seccompFilter = opts.disableSeccomp ? void 0 : ensureSeccompFilter(paths);
|
|
1973
|
+
if (seccompFilter) logger.debug(`Seccomp filter: ${seccompFilter}`);
|
|
1974
|
+
const cgroup = opts.disableCgroup ? void 0 : this.buildCgroupConfig(vcpus, memMib);
|
|
1975
|
+
jailer.spawn({
|
|
1976
|
+
firecrackerBin,
|
|
1977
|
+
jailerBin,
|
|
1978
|
+
chrootBase: jailerPaths.chrootBase,
|
|
1979
|
+
seccompFilter: seccompFilter ?? void 0,
|
|
1980
|
+
newPidNs: !opts.disablePidNs,
|
|
1981
|
+
cgroup,
|
|
1982
|
+
netns: netnsName
|
|
1983
|
+
});
|
|
1984
|
+
log.start("Waiting for API socket...");
|
|
1985
|
+
await waitForSocket(jailerPaths.socketPath, 5e3);
|
|
1986
|
+
log.success("API socket ready");
|
|
1987
|
+
if (snapshotId) {
|
|
1988
|
+
log.start("Restoring from snapshot...");
|
|
1989
|
+
const vm = new FirecrackerClient(jailerPaths.socketPath);
|
|
1990
|
+
await vm.loadSnapshot("snapshot/snapshot_file", "snapshot/mem_file");
|
|
1991
|
+
await vm.resume();
|
|
1992
|
+
log.success("Snapshot restored and VM resumed");
|
|
1993
|
+
} else {
|
|
1994
|
+
await this.bootVm(jailerPaths.socketPath, netCfg, vcpus, memMib);
|
|
1995
|
+
log.start("Starting VM...");
|
|
1996
|
+
await new FirecrackerClient(jailerPaths.socketPath).start();
|
|
1997
|
+
}
|
|
1998
|
+
const pid = getVmPid(vmId);
|
|
1999
|
+
logger.debug(`Firecracker PID: ${pid ?? "unknown"}`);
|
|
2000
|
+
this.store.update(vmId, {
|
|
2001
|
+
status: "running",
|
|
2002
|
+
pid
|
|
2003
|
+
});
|
|
2004
|
+
log.success(`VM ${vmId} is running (PID: ${pid || "unknown"})`);
|
|
2005
|
+
if (timeoutMs && pid) spawnTimeoutKiller({
|
|
2006
|
+
vmId,
|
|
2007
|
+
pid,
|
|
2008
|
+
timeoutMs,
|
|
2009
|
+
stateFile: join(paths.vmsDir, `${vmId}.json`)
|
|
2010
|
+
});
|
|
2011
|
+
const finalState = this.store.load(vmId);
|
|
2012
|
+
await hooks.callHook("vm:afterCreate", finalState);
|
|
2013
|
+
return {
|
|
2014
|
+
vmId,
|
|
2015
|
+
pid,
|
|
2016
|
+
state: finalState
|
|
2017
|
+
};
|
|
2018
|
+
} catch (error) {
|
|
2019
|
+
if (vmId) await hooks.callHook("vm:error", {
|
|
2020
|
+
vmId,
|
|
2021
|
+
error: toError(error),
|
|
2022
|
+
phase: "create"
|
|
2023
|
+
});
|
|
2024
|
+
killOrphanVmProcess(vmId);
|
|
2025
|
+
this.markAsError(vmId, error);
|
|
2026
|
+
cleanupNetwork(networkConfig);
|
|
2027
|
+
cleanupChroot(chrootDir);
|
|
2028
|
+
throw error;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
async start(vmId) {
|
|
2032
|
+
const { logger, paths, hooks } = this;
|
|
2033
|
+
const log = logger.withTag(vmId);
|
|
2034
|
+
let networkConfig;
|
|
2035
|
+
try {
|
|
2036
|
+
const state = this.store.load(vmId);
|
|
2037
|
+
if (!state) throw vmNotFoundError(vmId);
|
|
2038
|
+
if (state.status !== "stopped") throw vmNotStoppedError(vmId, state.status);
|
|
2039
|
+
if (!state.chrootDir || !existsSync(state.chrootDir)) throw chrootNotFoundError(vmId);
|
|
2040
|
+
validateEnvironment(paths.baseDir);
|
|
2041
|
+
await hooks.callHook("vm:beforeStart", {
|
|
2042
|
+
vmId,
|
|
2043
|
+
state
|
|
2044
|
+
});
|
|
2045
|
+
log.start(`Starting VM ${vmId}...`);
|
|
2046
|
+
const mgr = NetworkManager.fromVmNetwork(state.network);
|
|
2047
|
+
networkConfig = mgr.config;
|
|
2048
|
+
logger.debug(`Reconstructed network config: slot=${networkConfig.slot}, tap=${networkConfig.tapDevice}, host=${networkConfig.hostIp}, guest=${networkConfig.guestIp}`);
|
|
2049
|
+
log.start("Setting up networking...");
|
|
2050
|
+
await mgr.setup();
|
|
2051
|
+
log.success(`Network: TAP ${networkConfig.tapDevice}, Host ${networkConfig.hostIp}, Guest ${networkConfig.guestIp}`);
|
|
2052
|
+
await hooks.callHook("network:afterSetup", {
|
|
2053
|
+
vmId,
|
|
2054
|
+
slot: networkConfig.slot,
|
|
2055
|
+
networkConfig,
|
|
2056
|
+
domains: state.network.allowedDomains,
|
|
2057
|
+
networkPolicy: state.network.networkPolicy
|
|
2058
|
+
});
|
|
2059
|
+
const vmRootCandidates = Array.from(new Set([
|
|
2060
|
+
join(state.chrootDir, "root"),
|
|
2061
|
+
state.chrootDir,
|
|
2062
|
+
dirname(dirname(state.apiSocket))
|
|
2063
|
+
]));
|
|
2064
|
+
const removeStaleFirecrackerFiles = () => {
|
|
2065
|
+
for (const rootDir of vmRootCandidates) {
|
|
2066
|
+
rmSync(join(rootDir, "firecracker"), { force: true });
|
|
2067
|
+
rmSync(join(rootDir, "firecracker.pid"), { force: true });
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
removeStaleFirecrackerFiles();
|
|
2071
|
+
const socketPath = state.apiSocket;
|
|
2072
|
+
rmSync(socketPath, { force: true });
|
|
2073
|
+
const removeStaleDevTrees = () => {
|
|
2074
|
+
for (const rootDir of vmRootCandidates) rmSync(join(rootDir, "dev"), {
|
|
2075
|
+
recursive: true,
|
|
2076
|
+
force: true
|
|
2077
|
+
});
|
|
2078
|
+
};
|
|
2079
|
+
const removeStaleDeviceNodes = () => {
|
|
2080
|
+
const staleNodes = [
|
|
2081
|
+
"dev/net/tun",
|
|
2082
|
+
"dev/kvm",
|
|
2083
|
+
"dev/userfaultfd",
|
|
2084
|
+
"dev/urandom"
|
|
2085
|
+
];
|
|
2086
|
+
for (const rootDir of vmRootCandidates) for (const rel of staleNodes) rmSync(join(rootDir, rel), {
|
|
2087
|
+
recursive: true,
|
|
2088
|
+
force: true
|
|
2089
|
+
});
|
|
2090
|
+
};
|
|
2091
|
+
const firecrackerBin = join(paths.binDir, "firecracker");
|
|
2092
|
+
const jailerBin = join(paths.binDir, "jailer");
|
|
2093
|
+
const jailer = new Jailer(vmId, paths.jailerBaseDir);
|
|
2094
|
+
let socketReady = false;
|
|
2095
|
+
const startTag = `[start:${vmId}]`;
|
|
2096
|
+
const logAttemptError = (attempt, error) => {
|
|
2097
|
+
logger.error(`${startTag} ${attempt} failed: ${toError(error).message}`);
|
|
2098
|
+
};
|
|
2099
|
+
logger.debug(`Stale file cleanup: checked ${vmRootCandidates.length} root candidates`);
|
|
2100
|
+
const logDiagnostics = () => {
|
|
2101
|
+
const socketExists = existsSync(socketPath);
|
|
2102
|
+
const devState = vmRootCandidates.map((rootDir) => `${join(rootDir, "dev")}=${existsSync(join(rootDir, "dev"))}`).join(", ");
|
|
2103
|
+
const firecrackerPid = getVmPid(vmId);
|
|
2104
|
+
const jailerPid = getVmJailerPid(vmId);
|
|
2105
|
+
log.error(`${startTag} diagnostics: socketExists=${socketExists}; firecrackerPid=${firecrackerPid ?? "none"}; jailerPid=${jailerPid ?? "none"}; devDirs=[${devState}]`);
|
|
2106
|
+
};
|
|
2107
|
+
const isRecoverableStartError = (message) => {
|
|
2108
|
+
if (message.includes("Timeout waiting for API socket")) return true;
|
|
2109
|
+
if (message.includes("mknod inside the jail") && message.includes("File exists")) return true;
|
|
2110
|
+
if (message.includes("MknodDev(") && message.includes("os error 17")) return true;
|
|
2111
|
+
return false;
|
|
2112
|
+
};
|
|
2113
|
+
const cgroup = this.buildCgroupConfig(state.vcpuCount, state.memSizeMib);
|
|
2114
|
+
const spawnAndWait = async (timeoutMs) => {
|
|
2115
|
+
log.start("Spawning Firecracker via jailer...");
|
|
2116
|
+
logger.debug(`Jailer spawn: firecracker=${firecrackerBin}, jailer=${jailerBin}, chrootBase=${jailer.paths.chrootBase}`);
|
|
2117
|
+
jailer.spawn({
|
|
2118
|
+
firecrackerBin,
|
|
2119
|
+
jailerBin,
|
|
2120
|
+
chrootBase: jailer.paths.chrootBase,
|
|
2121
|
+
newPidNs: true,
|
|
2122
|
+
cgroup,
|
|
2123
|
+
netns: state.network.netnsName
|
|
2124
|
+
});
|
|
2125
|
+
log.start("Waiting for API socket...");
|
|
2126
|
+
await waitForSocket(socketPath, timeoutMs);
|
|
2127
|
+
};
|
|
2128
|
+
try {
|
|
2129
|
+
removeStaleDeviceNodes();
|
|
2130
|
+
await spawnAndWait(1e4);
|
|
2131
|
+
socketReady = true;
|
|
2132
|
+
} catch (firstStartError) {
|
|
2133
|
+
const message = toError(firstStartError).message;
|
|
2134
|
+
if (!isRecoverableStartError(message)) {
|
|
2135
|
+
logAttemptError("initial attempt", firstStartError);
|
|
2136
|
+
logDiagnostics();
|
|
2137
|
+
throw firstStartError;
|
|
2138
|
+
}
|
|
2139
|
+
logAttemptError("initial attempt", firstStartError);
|
|
2140
|
+
killOrphanVmProcess(vmId);
|
|
2141
|
+
rmSync(socketPath, { force: true });
|
|
2142
|
+
removeStaleDeviceNodes();
|
|
2143
|
+
removeStaleDevTrees();
|
|
2144
|
+
removeStaleFirecrackerFiles();
|
|
2145
|
+
try {
|
|
2146
|
+
await spawnAndWait(15e3);
|
|
2147
|
+
socketReady = true;
|
|
2148
|
+
} catch (retryError) {
|
|
2149
|
+
logAttemptError("retry attempt", retryError);
|
|
2150
|
+
logDiagnostics();
|
|
2151
|
+
throw new Error(`${startTag} retry failed after cleanup. First error: ${message}. Retry error: ${toError(retryError).message}`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (!socketReady) throw new Error(`Timeout waiting for API socket at ${socketPath}`);
|
|
2155
|
+
log.success("API socket ready");
|
|
2156
|
+
await this.bootVm(socketPath, networkConfig, state.vcpuCount, state.memSizeMib);
|
|
2157
|
+
log.start("Starting VM...");
|
|
2158
|
+
await new FirecrackerClient(socketPath).start();
|
|
2159
|
+
const pid = getVmPid(vmId);
|
|
2160
|
+
this.store.update(vmId, {
|
|
2161
|
+
status: "running",
|
|
2162
|
+
pid
|
|
2163
|
+
});
|
|
2164
|
+
log.success(`VM ${vmId} is running (PID: ${pid || "unknown"})`);
|
|
2165
|
+
const finalState = this.store.load(vmId);
|
|
2166
|
+
await hooks.callHook("vm:afterStart", finalState);
|
|
2167
|
+
return {
|
|
2168
|
+
vmId,
|
|
2169
|
+
pid,
|
|
2170
|
+
state: finalState,
|
|
2171
|
+
success: true
|
|
2172
|
+
};
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
await hooks.callHook("vm:error", {
|
|
2175
|
+
vmId,
|
|
2176
|
+
error: toError(error),
|
|
2177
|
+
phase: "start"
|
|
2178
|
+
});
|
|
2179
|
+
killOrphanVmProcess(vmId);
|
|
2180
|
+
this.markAsError(vmId, error);
|
|
2181
|
+
cleanupNetwork(networkConfig);
|
|
2182
|
+
return {
|
|
2183
|
+
vmId,
|
|
2184
|
+
pid: null,
|
|
2185
|
+
state: null,
|
|
2186
|
+
success: false,
|
|
2187
|
+
error: error instanceof VmsanError ? error : void 0
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
async stop(vmId) {
|
|
2192
|
+
const state = this.store.load(vmId);
|
|
2193
|
+
if (!state) return {
|
|
2194
|
+
vmId,
|
|
2195
|
+
success: false,
|
|
2196
|
+
error: vmNotFoundError(vmId)
|
|
2197
|
+
};
|
|
2198
|
+
if (state.status === "stopped") return {
|
|
2199
|
+
vmId,
|
|
2200
|
+
success: true,
|
|
2201
|
+
alreadyStopped: true
|
|
2202
|
+
};
|
|
2203
|
+
try {
|
|
2204
|
+
await this.hooks.callHook("vm:beforeStop", {
|
|
2205
|
+
vmId,
|
|
2206
|
+
state
|
|
2207
|
+
});
|
|
2208
|
+
const previousStatus = state.status;
|
|
2209
|
+
if (state.pid) safeKill(state.pid, "SIGKILL");
|
|
2210
|
+
const orphanPid = getVmPid(vmId);
|
|
2211
|
+
if (orphanPid) safeKill(orphanPid, "SIGKILL");
|
|
2212
|
+
const orphanJailerPid = getVmJailerPid(vmId);
|
|
2213
|
+
if (orphanJailerPid) safeKill(orphanJailerPid, "SIGKILL");
|
|
2214
|
+
if (state.network) {
|
|
2215
|
+
const netCfg = NetworkManager.fromVmNetwork(state.network);
|
|
2216
|
+
try {
|
|
2217
|
+
netCfg.teardown();
|
|
2218
|
+
} catch {}
|
|
2219
|
+
await this.hooks.callHook("network:afterTeardown", {
|
|
2220
|
+
vmId,
|
|
2221
|
+
networkConfig: netCfg.config
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
this.store.update(vmId, {
|
|
2225
|
+
status: "stopped",
|
|
2226
|
+
pid: null
|
|
2227
|
+
});
|
|
2228
|
+
await this.hooks.callHook("vm:afterStop", {
|
|
2229
|
+
vmId,
|
|
2230
|
+
previousStatus
|
|
2231
|
+
});
|
|
2232
|
+
return {
|
|
2233
|
+
vmId,
|
|
2234
|
+
success: true
|
|
2235
|
+
};
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
await this.hooks.callHook("vm:error", {
|
|
2238
|
+
vmId,
|
|
2239
|
+
error: toError(err),
|
|
2240
|
+
phase: "stop"
|
|
2241
|
+
});
|
|
2242
|
+
return {
|
|
2243
|
+
vmId,
|
|
2244
|
+
success: false,
|
|
2245
|
+
error: err instanceof VmsanError ? err : void 0
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
async updateNetworkPolicy(vmId, policy, domains, allowedCidrs, deniedCidrs) {
|
|
2250
|
+
return new FileLock(join(this.paths.vmsDir, `${vmId}.json`), `update-policy-${vmId}`).runAsync(async () => {
|
|
2251
|
+
const state = this.store.load(vmId);
|
|
2252
|
+
if (!state) return {
|
|
2253
|
+
vmId,
|
|
2254
|
+
success: false,
|
|
2255
|
+
previousPolicy: policy,
|
|
2256
|
+
newPolicy: policy,
|
|
2257
|
+
error: vmNotFoundError(vmId)
|
|
2258
|
+
};
|
|
2259
|
+
const previousPolicy = state.network.networkPolicy;
|
|
2260
|
+
if (state.status !== "running") return {
|
|
2261
|
+
vmId,
|
|
2262
|
+
success: false,
|
|
2263
|
+
previousPolicy,
|
|
2264
|
+
newPolicy: policy,
|
|
2265
|
+
error: vmNotRunningError(vmId)
|
|
2266
|
+
};
|
|
2267
|
+
try {
|
|
2268
|
+
NetworkManager.fromVmNetwork(state.network).updatePolicy(policy, domains, allowedCidrs, deniedCidrs);
|
|
2269
|
+
this.store.update(vmId, { network: {
|
|
2270
|
+
...state.network,
|
|
2271
|
+
networkPolicy: policy,
|
|
2272
|
+
allowedDomains: domains,
|
|
2273
|
+
allowedCidrs,
|
|
2274
|
+
deniedCidrs
|
|
2275
|
+
} });
|
|
2276
|
+
await this.hooks.callHook("network:policyChange", {
|
|
2277
|
+
vmId,
|
|
2278
|
+
previousPolicy,
|
|
2279
|
+
newPolicy: policy
|
|
2280
|
+
});
|
|
2281
|
+
return {
|
|
2282
|
+
vmId,
|
|
2283
|
+
success: true,
|
|
2284
|
+
previousPolicy,
|
|
2285
|
+
newPolicy: policy
|
|
2286
|
+
};
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
return {
|
|
2289
|
+
vmId,
|
|
2290
|
+
success: false,
|
|
2291
|
+
previousPolicy,
|
|
2292
|
+
newPolicy: policy,
|
|
2293
|
+
error: err instanceof VmsanError ? err : void 0
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
async remove(vmId, opts) {
|
|
2299
|
+
const state = this.store.load(vmId);
|
|
2300
|
+
if (!state) return {
|
|
2301
|
+
vmId,
|
|
2302
|
+
success: false,
|
|
2303
|
+
error: vmNotFoundError(vmId)
|
|
2304
|
+
};
|
|
2305
|
+
try {
|
|
2306
|
+
const force = opts?.force ?? false;
|
|
2307
|
+
await this.hooks.callHook("vm:beforeRemove", {
|
|
2308
|
+
vmId,
|
|
2309
|
+
state,
|
|
2310
|
+
force
|
|
2311
|
+
});
|
|
2312
|
+
if (state.status !== "stopped") {
|
|
2313
|
+
if (!force) return {
|
|
2314
|
+
vmId,
|
|
2315
|
+
success: false,
|
|
2316
|
+
error: vmNotStoppedError(vmId, state.status)
|
|
2317
|
+
};
|
|
2318
|
+
const stopResult = await this.stop(vmId);
|
|
2319
|
+
if (!stopResult.success) return stopResult;
|
|
2320
|
+
}
|
|
2321
|
+
cleanupChroot(state.chrootDir);
|
|
2322
|
+
this.store.delete(vmId);
|
|
2323
|
+
await this.hooks.callHook("vm:afterRemove", { vmId });
|
|
2324
|
+
return {
|
|
2325
|
+
vmId,
|
|
2326
|
+
success: true
|
|
2327
|
+
};
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
await this.hooks.callHook("vm:error", {
|
|
2330
|
+
vmId,
|
|
2331
|
+
error: toError(err),
|
|
2332
|
+
phase: "remove"
|
|
2333
|
+
});
|
|
2334
|
+
return {
|
|
2335
|
+
vmId,
|
|
2336
|
+
success: false,
|
|
2337
|
+
error: err instanceof VmsanError ? err : void 0
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
buildCgroupConfig(vcpus, memMib) {
|
|
2342
|
+
return {
|
|
2343
|
+
cpuQuotaUs: vcpus * 1e5,
|
|
2344
|
+
cpuPeriodUs: 1e5,
|
|
2345
|
+
memoryBytes: (memMib + CGROUP_VMM_OVERHEAD_MIB) * 1024 * 1024
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
async bootVm(socketPath, netCfg, vcpus, memMib) {
|
|
2349
|
+
const vm = new FirecrackerClient(socketPath);
|
|
2350
|
+
const bootArgs = NetworkManager.bootArgs(netCfg.slot);
|
|
2351
|
+
this.logger.debug(`Boot args: ${bootArgs}`);
|
|
2352
|
+
await vm.boot("kernel/vmlinux", bootArgs);
|
|
2353
|
+
await vm.addDrive("rootfs", "rootfs/rootfs.ext4", true, false);
|
|
2354
|
+
await vm.configure(vcpus, memMib);
|
|
2355
|
+
await vm.addNetwork("eth0", netCfg.tapDevice, netCfg.macAddress);
|
|
2356
|
+
}
|
|
2357
|
+
markAsError(vmId, error) {
|
|
2358
|
+
try {
|
|
2359
|
+
this.store.update(vmId, {
|
|
2360
|
+
status: "error",
|
|
2361
|
+
error: toError(error).message
|
|
2362
|
+
});
|
|
2363
|
+
} catch {}
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
async function createVmsan(options) {
|
|
2367
|
+
const paths = options?.paths === void 0 ? vmsanPaths() : typeof options.paths === "string" ? vmsanPaths(options.paths) : options.paths;
|
|
2368
|
+
const store = options?.store ?? new FileVmStateStore(paths.vmsDir);
|
|
2369
|
+
const logger = options?.logger ?? createDefaultLogger();
|
|
2370
|
+
const ctx = {
|
|
2371
|
+
paths,
|
|
2372
|
+
store,
|
|
2373
|
+
hooks: createHooks(),
|
|
2374
|
+
logger
|
|
2375
|
+
};
|
|
2376
|
+
const vmsan = new VMService(ctx);
|
|
2377
|
+
if (options?.plugins) for (const plugin of options.plugins) await plugin.setup(ctx);
|
|
2378
|
+
return vmsan;
|
|
2379
|
+
}
|
|
2380
|
+
export { createSilentLogger as C, createDefaultLogger as S, waitForSocket as _, resolveImageRootfs as a, FileLock as b, cleanupNetwork as c, assertSnapshotExists as d, findKernel as f, validateEnvironment as g, getVmPid as h, ensureSeccompFilter as i, killOrphanVmProcess as l, getVmJailerPid as m, VMService as n, buildInitialVmState as o, findRootfs as p, compileSeccompFilter as r, cleanupChroot as s, createVmsan as t, markVmAsError as u, Jailer as v, NetworkManager as x, detectCgroupVersion as y };
|