vmsan 0.1.0-alpha.25 → 0.1.0-alpha.26

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 CHANGED
@@ -180,7 +180,7 @@ docs/ Documentation site (vmsan.dev)
180
180
  ### How it works
181
181
 
182
182
  1. **vmsan** uses [Firecracker](https://github.com/firecracker-microvm/firecracker) to create lightweight microVMs with a jailer for security isolation
183
- 2. Each VM gets a TAP network device with its own `/30` subnet (`172.16.{slot}.0/30`)
183
+ 2. Each VM gets a TAP network device with its own `/30` subnet (`198.19.{slot}.0/30`)
184
184
  3. A Go-based **agent** runs inside the VM, exposing an HTTP API for command execution, file operations, and shell access
185
185
  4. The CLI communicates with the agent over the host-guest network
186
186
 
@@ -1,6 +1,6 @@
1
1
  import { n as vmsanPaths } from "./paths.mjs";
2
2
  import { A as vmNotRunningError, B as invalidDomainPatternError, D as snapshotNotFoundError, F as invalidCidrOctetError, G as invalidNetworkPolicyError, H as invalidImageRefEmptyError, I as invalidCidrPrefixError, J as mutuallyExclusiveFlagsError, K as invalidPortError, L as invalidDiskSizeFormatError, P as invalidCidrFormatError, R as invalidDiskSizeRangeError, T as chrootNotFoundError, U as invalidImageRefTagError, W as invalidIntegerFlagError, X as portConflictError, Z as VmsanError, a as cloudflareNotConfiguredError, c as cloudflaredStartFailedError, d as missingBinaryError, f as noExt4RootfsError, h as noRootfsDirError, i as cloudflareNoZoneError, j as vmNotStoppedError, k as vmNotFoundError, m as noKernelError, n as cloudflareConfigNotFoundError, o as cloudflareTunnelNoIdError, p as noKernelDirError, q as invalidRuntimeError, r as cloudflareNoAccountsError, s as cloudflaredNotFoundError, u as SetupError, v as lockTimeoutError, x as defaultInterfaceNotFoundError, y as socketTimeoutError, z as invalidDomainError } from "./errors.mjs";
3
- import { c as mkdirSecure, d as sleepSync, g as writeSecure, h as toError, n as waitForAgent, o as generateVmId, r as FileVmStateStore, u as safeKill } from "./vm-context.mjs";
3
+ import { S as vmLinkCidrFromIp, _ as SUPPORTED_VM_ADDRESS_BLOCKS, b as vmGuestIp, c as mkdirSecure, d as sleepSync, g as writeSecure, h as toError, n as waitForAgent, o as generateVmId, r as FileVmStateStore, u as safeKill, v as VM_SUBNET_MASK, x as vmHostIp, y as slotFromVmHostIp } from "./vm-context.mjs";
4
4
  import { t as FirecrackerClient } from "./firecracker.mjs";
5
5
  import { t as spawnTimeoutKiller } from "./timeout-killer.mjs";
6
6
  import { t as AgentClient } from "./agent.mjs";
@@ -101,9 +101,9 @@ var NetworkManager = class NetworkManager {
101
101
  this.config = {
102
102
  slot,
103
103
  tapDevice: `fhvm${slot}`,
104
- hostIp: `172.16.${slot}.1`,
105
- guestIp: `172.16.${slot}.2`,
106
- subnetMask: "255.255.255.252",
104
+ hostIp: vmHostIp(slot),
105
+ guestIp: vmGuestIp(slot),
106
+ subnetMask: VM_SUBNET_MASK,
107
107
  macAddress: `AA:FC:00:00:00:${(slot + 1).toString(16).padStart(2, "0").toUpperCase()}`,
108
108
  networkPolicy,
109
109
  allowedDomains,
@@ -115,8 +115,8 @@ var NetworkManager = class NetworkManager {
115
115
  skipDnat
116
116
  };
117
117
  }
118
- static bootArgs(slot) {
119
- 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]}`;
118
+ static bootArgs(config) {
119
+ return `console=ttyS0 reboot=k panic=1 pci=off ip=${config.guestIp}::${config.hostIp}:${config.subnetMask}::eth0:off:${DNS_RESOLVERS[0]}`;
120
120
  }
121
121
  static fromConfig(config) {
122
122
  const mgr = Object.create(NetworkManager.prototype);
@@ -124,8 +124,12 @@ var NetworkManager = class NetworkManager {
124
124
  return mgr;
125
125
  }
126
126
  static fromVmNetwork(network) {
127
- const slot = Number(network.hostIp.split(".")[2]);
128
- if (!Number.isInteger(slot)) throw new Error(`invalid network slot derived from hostIp: ${network.hostIp}`);
127
+ let slot;
128
+ try {
129
+ slot = slotFromVmHostIp(network.hostIp);
130
+ } catch {
131
+ throw new Error(`invalid network slot derived from hostIp: ${network.hostIp}`);
132
+ }
129
133
  return NetworkManager.fromConfig({
130
134
  slot,
131
135
  tapDevice: network.tapDevice,
@@ -149,8 +153,9 @@ var NetworkManager = class NetworkManager {
149
153
  else sudo(args);
150
154
  }
151
155
  setupNamespace() {
152
- const { slot, netnsName } = this.config;
156
+ const { guestIp, netnsName } = this.config;
153
157
  if (!netnsName) return;
158
+ const slot = this.config.slot;
154
159
  const vethHost = `veth-h-${slot}`;
155
160
  const vethGuest = `veth-g-${slot}`;
156
161
  const transitHostIp = `10.200.${slot}.1`;
@@ -244,7 +249,7 @@ var NetworkManager = class NetworkManager {
244
249
  "ip",
245
250
  "route",
246
251
  "add",
247
- `172.16.${slot}.0/30`,
252
+ vmLinkCidrFromIp(guestIp),
248
253
  "via",
249
254
  transitGuestIp
250
255
  ]);
@@ -255,7 +260,7 @@ var NetworkManager = class NetworkManager {
255
260
  ]);
256
261
  }
257
262
  teardownNamespace() {
258
- const { slot, netnsName } = this.config;
263
+ const { guestIp, netnsName } = this.config;
259
264
  if (!netnsName) return;
260
265
  const tryRun = (args) => {
261
266
  try {
@@ -268,7 +273,7 @@ var NetworkManager = class NetworkManager {
268
273
  "ip",
269
274
  "route",
270
275
  "del",
271
- `172.16.${slot}.0/30`
276
+ vmLinkCidrFromIp(guestIp)
272
277
  ]);
273
278
  tryRun([
274
279
  "ip",
@@ -347,10 +352,45 @@ var NetworkManager = class NetworkManager {
347
352
  }
348
353
  }
349
354
  setupRules() {
350
- const { tapDevice, hostIp, guestIp, publishedPorts } = this.config;
355
+ const { tapDevice, guestIp, publishedPorts } = this.config;
351
356
  const policy = effectivePolicy(this.config);
357
+ const vethGuest = this.config.netnsName ? `veth-g-${this.config.slot}` : void 0;
352
358
  const fwd = this.nsRun.bind(this);
359
+ sudo([
360
+ "iptables",
361
+ "-I",
362
+ "OUTPUT",
363
+ "1",
364
+ "-d",
365
+ guestIp,
366
+ "-j",
367
+ "ACCEPT"
368
+ ]);
369
+ sudo([
370
+ "iptables",
371
+ "-I",
372
+ "INPUT",
373
+ "1",
374
+ "-s",
375
+ guestIp,
376
+ "-j",
377
+ "ACCEPT"
378
+ ]);
353
379
  if (policy === "deny-all") {
380
+ if (vethGuest) fwd([
381
+ "iptables",
382
+ "-I",
383
+ "FORWARD",
384
+ "1",
385
+ "-i",
386
+ vethGuest,
387
+ "-o",
388
+ tapDevice,
389
+ "-d",
390
+ guestIp,
391
+ "-j",
392
+ "ACCEPT"
393
+ ]);
354
394
  fwd([
355
395
  "iptables",
356
396
  "-I",
@@ -546,14 +586,14 @@ var NetworkManager = class NetworkManager {
546
586
  "DROP"
547
587
  ]);
548
588
  }
549
- fwd([
589
+ for (const vmAddressBlock of SUPPORTED_VM_ADDRESS_BLOCKS) fwd([
550
590
  "iptables",
551
591
  "-A",
552
592
  "FORWARD",
553
593
  "-i",
554
594
  tapDevice,
555
595
  "-d",
556
- "172.16.0.0/16",
596
+ vmAddressBlock,
557
597
  "-j",
558
598
  "DROP"
559
599
  ]);
@@ -694,14 +734,14 @@ var NetworkManager = class NetworkManager {
694
734
  "DROP"
695
735
  ]);
696
736
  }
697
- fwd([
737
+ for (const vmAddressBlock of SUPPORTED_VM_ADDRESS_BLOCKS) fwd([
698
738
  "iptables",
699
739
  "-A",
700
740
  "FORWARD",
701
741
  "-i",
702
742
  tapDevice,
703
743
  "-d",
704
- "172.16.0.0/16",
744
+ vmAddressBlock,
705
745
  "-j",
706
746
  "DROP"
707
747
  ]);
@@ -761,6 +801,20 @@ var NetworkManager = class NetworkManager {
761
801
  "ACCEPT"
762
802
  ]);
763
803
  }
804
+ if (vethGuest) fwd([
805
+ "iptables",
806
+ "-I",
807
+ "FORWARD",
808
+ "1",
809
+ "-i",
810
+ vethGuest,
811
+ "-o",
812
+ tapDevice,
813
+ "-d",
814
+ guestIp,
815
+ "-j",
816
+ "ACCEPT"
817
+ ]);
764
818
  }
765
819
  setupThrottle() {
766
820
  const { tapDevice, bandwidthMbit } = this.config;
@@ -799,7 +853,8 @@ var NetworkManager = class NetworkManager {
799
853
  }
800
854
  }
801
855
  teardownRules() {
802
- const { tapDevice, hostIp, guestIp, publishedPorts, netnsName } = this.config;
856
+ const { tapDevice, guestIp, publishedPorts, netnsName } = this.config;
857
+ const vethGuest = netnsName ? `veth-g-${this.config.slot}` : void 0;
803
858
  let defaultIface;
804
859
  try {
805
860
  defaultIface = getDefaultInterface();
@@ -813,6 +868,24 @@ var NetworkManager = class NetworkManager {
813
868
  consola.debug(`iptables host cleanup failed (${args.slice(0, 4).join(" ")}): ${toError(err).message}`);
814
869
  }
815
870
  };
871
+ tryRun([
872
+ "iptables",
873
+ "-D",
874
+ "OUTPUT",
875
+ "-d",
876
+ guestIp,
877
+ "-j",
878
+ "ACCEPT"
879
+ ]);
880
+ tryRun([
881
+ "iptables",
882
+ "-D",
883
+ "INPUT",
884
+ "-s",
885
+ guestIp,
886
+ "-j",
887
+ "ACCEPT"
888
+ ]);
816
889
  const tryFwd = (args) => {
817
890
  try {
818
891
  this.nsRun(args);
@@ -992,14 +1065,14 @@ var NetworkManager = class NetworkManager {
992
1065
  "DROP"
993
1066
  ]);
994
1067
  }
995
- tryFwd([
1068
+ for (const vmAddressBlock of SUPPORTED_VM_ADDRESS_BLOCKS) tryFwd([
996
1069
  "iptables",
997
1070
  "-D",
998
1071
  "FORWARD",
999
1072
  "-i",
1000
1073
  tapDevice,
1001
1074
  "-d",
1002
- "172.16.0.0/16",
1075
+ vmAddressBlock,
1003
1076
  "-j",
1004
1077
  "DROP"
1005
1078
  ]);
@@ -1044,6 +1117,19 @@ var NetworkManager = class NetworkManager {
1044
1117
  "DROP"
1045
1118
  ]);
1046
1119
  }
1120
+ if (vethGuest) tryFwd([
1121
+ "iptables",
1122
+ "-D",
1123
+ "FORWARD",
1124
+ "-i",
1125
+ vethGuest,
1126
+ "-o",
1127
+ tapDevice,
1128
+ "-d",
1129
+ guestIp,
1130
+ "-j",
1131
+ "ACCEPT"
1132
+ ]);
1047
1133
  if (netnsName && defaultIface) {
1048
1134
  const vethHost = `veth-h-${this.config.slot}`;
1049
1135
  tryRun([
@@ -1284,6 +1370,7 @@ var Jailer = class {
1284
1370
  }
1285
1371
  const tmpMount = join(paths.rootDir, "tmp-mount");
1286
1372
  mkdirSync(tmpMount, { recursive: true });
1373
+ let prepareError;
1287
1374
  try {
1288
1375
  execSync(`sudo mount -o loop "${paths.rootfsPath}" "${tmpMount}"`, { stdio: "pipe" });
1289
1376
  execSync(`rm -f "${tmpMount}/etc/resolv.conf" && ln -s /proc/net/pnp "${tmpMount}/etc/resolv.conf"`, { stdio: "pipe" });
@@ -1321,7 +1408,8 @@ var Jailer = class {
1321
1408
  }
1322
1409
  }
1323
1410
  execSync(`sudo umount "${tmpMount}"`, { stdio: "pipe" });
1324
- } catch {
1411
+ } catch (error) {
1412
+ prepareError = error;
1325
1413
  try {
1326
1414
  execSync(`sudo umount "${tmpMount}" 2>/dev/null`, { stdio: "pipe" });
1327
1415
  } catch {}
@@ -1329,6 +1417,7 @@ var Jailer = class {
1329
1417
  try {
1330
1418
  execSync(`rm -rf "${tmpMount}"`, { stdio: "pipe" });
1331
1419
  } catch {}
1420
+ if (prepareError) throw prepareError;
1332
1421
  if (config.snapshot) {
1333
1422
  mkdirSync(paths.snapshotDir, { recursive: true });
1334
1423
  copyFileSync(config.snapshot.snapshotFile, join(paths.snapshotDir, "snapshot_file"));
@@ -2419,7 +2508,7 @@ var VMService = class {
2419
2508
  }
2420
2509
  async bootVm(socketPath, netCfg, vcpus, memMib) {
2421
2510
  const vm = new FirecrackerClient(socketPath);
2422
- const bootArgs = NetworkManager.bootArgs(netCfg.slot);
2511
+ const bootArgs = NetworkManager.bootArgs(netCfg);
2423
2512
  this.logger.debug(`Boot args: ${bootArgs}`);
2424
2513
  await vm.boot("kernel/vmlinux", bootArgs);
2425
2514
  await vm.addDrive("rootfs", "rootfs/rootfs.ext4", true, false);
@@ -13,7 +13,7 @@ function shellEscape(s) {
13
13
  if (/^[a-zA-Z0-9._\-/=:@]+$/.test(s)) return s;
14
14
  return "'" + s.replace(/'/g, "'\\''") + "'";
15
15
  }
16
- function parseEnvFlags(targetCommand) {
16
+ function parseEnvFlags(_targetCommand) {
17
17
  const env = {};
18
18
  const argv = process.argv;
19
19
  let positionalCount = 0;
@@ -4,6 +4,50 @@ import { execSync } from "node:child_process";
4
4
  import { chmodSync, chownSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { stripAnsi } from "consola/utils";
7
+ const VM_NETWORK_FIRST_OCTET = 198;
8
+ const VM_NETWORK_SECOND_OCTET = 19;
9
+ const VM_SUBNET_MASK = "255.255.255.252";
10
+ const VM_NETWORK_PREFIX = `${VM_NETWORK_FIRST_OCTET}.${VM_NETWORK_SECOND_OCTET}`;
11
+ const SUPPORTED_VM_ADDRESS_BLOCKS = ["198.19.0.0/16", "172.16.0.0/16"];
12
+ function assertValidSlot(slot) {
13
+ if (!Number.isInteger(slot) || slot < 0 || slot > 254) throw new Error(`invalid VM network slot: ${slot}`);
14
+ }
15
+ function parseIpv4OrNull(ip) {
16
+ const octets = ip.split(".");
17
+ if (octets.length !== 4 || octets.some((part) => !/^\d+$/.test(part))) return null;
18
+ const parts = octets.map((part) => Number.parseInt(part, 10));
19
+ if (parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
20
+ return parts;
21
+ }
22
+ function parseIpv4(ip) {
23
+ const parts = parseIpv4OrNull(ip);
24
+ if (!parts) throw new Error(`invalid IPv4 address: ${ip}`);
25
+ return parts;
26
+ }
27
+ function vmHostIp(slot) {
28
+ assertValidSlot(slot);
29
+ return `${VM_NETWORK_PREFIX}.${slot}.1`;
30
+ }
31
+ function vmGuestIp(slot) {
32
+ assertValidSlot(slot);
33
+ return `${VM_NETWORK_PREFIX}.${slot}.2`;
34
+ }
35
+ function slotFromVmHostIpOrNull(hostIp) {
36
+ const parts = parseIpv4OrNull(hostIp);
37
+ if (!parts) return null;
38
+ const slot = parts[2];
39
+ if (!Number.isInteger(slot) || slot < 0 || slot > 254) return null;
40
+ return slot;
41
+ }
42
+ function slotFromVmHostIp(hostIp) {
43
+ const slot = slotFromVmHostIpOrNull(hostIp);
44
+ if (slot === null) throw new Error(`invalid VM host IP: ${hostIp}`);
45
+ return slot;
46
+ }
47
+ function vmLinkCidrFromIp(ip) {
48
+ const [first, second, third] = parseIpv4(ip);
49
+ return `${first}.${second}.${third}.0/30`;
50
+ }
7
51
  /**
8
52
  * Resolve the UID/GID of the real (non-root) user when running under sudo.
9
53
  * Returns null when not running under sudo or when env vars are missing.
@@ -183,10 +227,12 @@ function table(opts) {
183
227
  return [`\x1b[1m${titles.map(padded).join(sep)}\x1b[0m`, ...data.map((row) => row.map(padded).join(sep))].join("\n");
184
228
  }
185
229
  function findFreeNetworkSlot(states) {
186
- const usedSlots = new Set(states.filter((s) => s.status === "running" || s.status === "creating").map((s) => {
187
- const parts = s.network.hostIp.split(".");
188
- return Number(parts[2]);
189
- }));
230
+ const usedSlots = /* @__PURE__ */ new Set();
231
+ for (const state of states) {
232
+ if (state.status === "error") continue;
233
+ const slot = slotFromVmHostIpOrNull(state.network.hostIp);
234
+ if (slot !== null) usedSlots.add(slot);
235
+ }
190
236
  for (const slot of getActiveTapSlots()) usedSlots.add(slot);
191
237
  for (let slot = 0; slot <= 254; slot++) if (!usedSlots.has(slot)) return slot;
192
238
  throw networkSlotsExhaustedError();
@@ -270,4 +316,4 @@ async function waitForAgent(guestIp, port, timeoutMs = 6e4) {
270
316
  }
271
317
  throw agentTimeoutError(guestIp, timeoutMs);
272
318
  }
273
- export { getActiveTapSlots as a, mkdirSecure as c, sleepSync as d, table as f, writeSecure as g, toError as h, findFreeNetworkSlot as i, parseDuration as l, timeRemaining as m, waitForAgent as n, generateVmId as o, timeAgo as p, FileVmStateStore as r, isProcessAlive as s, resolveVmState as t, safeKill as u };
319
+ export { vmLinkCidrFromIp as S, SUPPORTED_VM_ADDRESS_BLOCKS as _, getActiveTapSlots as a, vmGuestIp as b, mkdirSecure as c, sleepSync as d, table as f, writeSecure as g, toError as h, findFreeNetworkSlot as i, parseDuration as l, timeRemaining as m, waitForAgent as n, generateVmId as o, timeAgo as p, FileVmStateStore as r, isProcessAlive as s, resolveVmState as t, safeKill as u, VM_SUBNET_MASK as v, vmHostIp as x, slotFromVmHostIp as y };
package/dist/index.d.mts CHANGED
@@ -102,7 +102,7 @@ interface NetworkConfig {
102
102
  declare class NetworkManager {
103
103
  config: NetworkConfig;
104
104
  constructor(slot: number, networkPolicy: string, allowedDomains: string[], allowedCidrs: string[], deniedCidrs: string[], publishedPorts: number[], bandwidthMbit?: number, netnsName?: string, skipDnat?: boolean);
105
- static bootArgs(slot: number): string;
105
+ static bootArgs(config: Pick<NetworkConfig, "guestIp" | "hostIp" | "subnetMask">): string;
106
106
  static fromConfig(config: NetworkConfig): NetworkManager;
107
107
  static fromVmNetwork(network: VmNetwork): NetworkManager;
108
108
  private nsRun;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vmsan",
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.26",
4
4
  "description": "Firecracker microVM sandbox toolkit",
5
5
  "homepage": "https://github.com/angelorc/vmsan",
6
6
  "bugs": "https://github.com/angelorc/vmsan/issues",
@@ -27,10 +27,10 @@
27
27
  "scripts": {
28
28
  "build": "obuild",
29
29
  "dev": "obuild --stub",
30
- "lint": "oxlint . && oxfmt --check '!**/*.md' '!docs/**' .",
31
- "lint:fix": "oxlint . --fix && oxfmt '!**/*.md' '!docs/**' .",
32
- "fmt": "oxfmt '!**/*.md' '!docs/**' .",
33
- "fmt:check": "oxfmt --check '!**/*.md' '!docs/**' .",
30
+ "lint": "bunx oxlint . && bunx oxfmt --check '!**/*.md' '!docs/**' '!.changeset/pre.json' .",
31
+ "lint:fix": "bunx oxlint . --fix && bunx oxfmt '!**/*.md' '!docs/**' '!.changeset/pre.json' .",
32
+ "fmt": "bunx oxfmt '!**/*.md' '!docs/**' '!.changeset/pre.json' .",
33
+ "fmt:check": "bunx oxfmt --check '!**/*.md' '!docs/**' '!.changeset/pre.json' .",
34
34
  "test": "vitest run --passWithNoTests",
35
35
  "typecheck": "tsc --noEmit",
36
36
  "prepack": "bun run build",