Skip to content

Commit e4c9e88

Browse files
committed
Add Darwin support via vfkit hypervisor
Implements basic vfkit hypervisor runner to enable running Linux VMs on macOS using Apple's Virtualization.framework. vfkit is already in nixpkgs and is production-ready (used by minikube, podman, CRC). Features: - Kernel + initrd booting with proper console configuration - NAT networking (user mode) for basic connectivity - virtiofs shares for /nix/store (fast host directory sharing) - Volume support via virtio-blk - Graceful shutdown via Unix socket - Serial console support Limitations: - Darwin-only (requires macOS) - No bridge networking yet (NAT only; tap unavailable on macOS) - No device passthrough (Virtualization.framework limitation) - No vsock support yet (can be added later) The implementation follows existing hypervisor runner patterns and includes comprehensive feature validation with helpful error messages guiding users to supported alternatives.
1 parent e3e2220 commit e4c9e88

File tree

4 files changed

+189
-32
lines changed

4 files changed

+189
-32
lines changed

flake.nix

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@
160160
# currently broken:
161161
# "crosvm"
162162
];
163-
hypervisorsWithUserNet = [ "qemu" "kvmtool" ];
163+
hypervisorsWithUserNet = [ "qemu" "kvmtool" "vfkit" ];
164+
hypervisorsDarwinOnly = [ "vfkit" ];
164165
makeExample = { system, hypervisor, config ? {} }:
165166
lib.nixosSystem {
166167
system = lib.replaceString "-darwin" "-linux" system;
@@ -176,12 +177,21 @@
176177
nixpkgs.overlays = [ self.overlay ];
177178
microvm = {
178179
inherit hypervisor;
179-
# share the host's /nix/store if the hypervisor can do 9p
180-
shares = lib.optional (builtins.elem hypervisor hypervisorsWith9p) {
181-
tag = "ro-store";
182-
source = "/nix/store";
183-
mountPoint = "/nix/.ro-store";
184-
};
180+
# share the host's /nix/store if the hypervisor supports it
181+
shares =
182+
if builtins.elem hypervisor hypervisorsWith9p then [{
183+
tag = "ro-store";
184+
source = "/nix/store";
185+
mountPoint = "/nix/.ro-store";
186+
proto = "9p";
187+
}]
188+
else if hypervisor == "vfkit" then [{
189+
tag = "ro-store";
190+
source = "/nix/store";
191+
mountPoint = "/nix/.ro-store";
192+
proto = "virtiofs";
193+
}]
194+
else [];
185195
# writableStoreOverlay = "/nix/.rw-store";
186196
# volumes = [ {
187197
# image = "nix-store-overlay.img";
@@ -212,34 +222,44 @@
212222
};
213223
in
214224
(builtins.foldl' (results: system:
215-
builtins.foldl' ({ result, n }: hypervisor: {
216-
result = result // {
217-
"${system}-${hypervisor}-example" = makeExample {
218-
inherit system hypervisor;
219-
};
220-
} //
221-
lib.optionalAttrs (builtins.elem hypervisor self.lib.hypervisorsWithNetwork) {
222-
"${system}-${hypervisor}-example-with-tap" = makeExample {
223-
inherit system hypervisor;
224-
config = _: {
225-
microvm.interfaces = [ {
226-
type = "tap";
227-
id = "vm-${builtins.substring 0 4 hypervisor}";
228-
mac = "02:00:00:01:01:0${toString n}";
229-
} ];
230-
networking = {
231-
interfaces.eth0.useDHCP = true;
232-
firewall.allowedTCPPorts = [ 22 ];
233-
};
234-
services.openssh = {
235-
enable = true;
236-
settings.PermitRootLogin = "yes";
225+
builtins.foldl' ({ result, n }: hypervisor:
226+
let
227+
# Skip darwin-only hypervisors on Linux systems
228+
isDarwinOnly = builtins.elem hypervisor hypervisorsDarwinOnly;
229+
isDarwinSystem = lib.hasSuffix "-darwin" system;
230+
shouldSkip = isDarwinOnly && !isDarwinSystem;
231+
in
232+
if shouldSkip then { inherit result n; }
233+
else {
234+
result = result // {
235+
"${system}-${hypervisor}-example" = makeExample {
236+
inherit system hypervisor;
237+
};
238+
} //
239+
# Skip tap example for darwin-only hypervisors (vfkit doesn't support tap)
240+
lib.optionalAttrs (builtins.elem hypervisor self.lib.hypervisorsWithNetwork && !isDarwinOnly) {
241+
"${system}-${hypervisor}-example-with-tap" = makeExample {
242+
inherit system hypervisor;
243+
config = _: {
244+
microvm.interfaces = [ {
245+
type = "tap";
246+
id = "vm-${builtins.substring 0 4 hypervisor}";
247+
mac = "02:00:00:01:01:0${toString n}";
248+
} ];
249+
networking = {
250+
interfaces.eth0.useDHCP = true;
251+
firewall.allowedTCPPorts = [ 22 ];
252+
};
253+
services.openssh = {
254+
enable = true;
255+
settings.PermitRootLogin = "yes";
256+
};
237257
};
238258
};
239259
};
240-
};
241-
n = n + 1;
242-
}) results self.lib.hypervisors
260+
n = n + 1;
261+
}
262+
) results self.lib.hypervisors
243263
) { result = {}; n = 1; } systems).result;
244264
};
245265
}

lib/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ rec {
88
"kvmtool"
99
"stratovirt"
1010
"alioth"
11+
"vfkit"
1112
];
1213

1314
hypervisorsWithNetwork = hypervisors;

lib/runners/vfkit.nix

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
{ pkgs
2+
, microvmConfig
3+
, macvtapFds
4+
, withDriveLetters
5+
, ...
6+
}:
7+
8+
let
9+
inherit (pkgs) lib;
10+
inherit (vmHostPackages.stdenv.hostPlatform) system;
11+
inherit (microvmConfig) vmHostPackages;
12+
13+
vfkit = vmHostPackages.vfkit;
14+
15+
inherit (microvmConfig)
16+
hostName vcpu mem user interfaces volumes shares socket
17+
storeOnDisk kernel initrdPath storeDisk kernelParams
18+
balloon devices credentialFiles vsock;
19+
20+
inherit (microvmConfig.vfkit) extraArgs logLevel;
21+
22+
volumesWithLetters = withDriveLetters microvmConfig;
23+
24+
# vfkit requires uncompressed kernel
25+
kernelPath = "${kernel.out}/${pkgs.stdenv.hostPlatform.linux-kernel.target}";
26+
27+
kernelCmdLine = "console=hvc0 reboot=t panic=-1 ${toString kernelParams}";
28+
29+
bootloaderArgs = [
30+
"--bootloader"
31+
"linux,kernel=${kernelPath},initrd=${initrdPath},cmdline=\"${builtins.concatStringsSep " " kernelCmdLine}\""
32+
];
33+
34+
deviceArgs =
35+
[ "--device" "virtio-rng" ]
36+
++
37+
[ "--device" "virtio-serial,stdio" ]
38+
++
39+
(builtins.concatMap ({ image, ... }: [
40+
"--device" "virtio-blk,path=${image}"
41+
]) volumesWithLetters)
42+
++ (builtins.concatMap ({ proto, source, tag, ... }:
43+
if proto == "virtiofs" then [
44+
"--device" "virtio-fs,sharedDir=${source},mountTag=${tag}"
45+
]
46+
else
47+
throw "vfkit does not support ${proto} share. Use proto = \"virtiofs\" instead."
48+
) shares)
49+
++ (builtins.concatMap ({ type, id, mac, ... }:
50+
if type == "user" then [
51+
"--device" "virtio-net,nat,mac=${mac}"
52+
]
53+
else if type == "bridge" then
54+
throw "vfkit bridge networking requires vmnet-helper which is not yet implemented. Use type = \"user\" for NAT networking."
55+
else
56+
throw "Unknown network interface type: ${type}"
57+
) interfaces);
58+
59+
allArgsWithoutSocket = [
60+
"${lib.getExe vfkit}"
61+
"--cpus" (toString vcpu)
62+
"--memory" (toString mem)
63+
]
64+
++ lib.optionals (logLevel != null) [
65+
"--log-level" logLevel
66+
]
67+
++ bootloaderArgs
68+
++ deviceArgs
69+
++ extraArgs;
70+
71+
in
72+
{
73+
tapMultiQueue = false;
74+
75+
preStart = lib.optionalString (socket != null) ''
76+
rm -f ${socket}
77+
'';
78+
79+
command =
80+
if !vmHostPackages.stdenv.hostPlatform.isDarwin
81+
then throw "vfkit only works on macOS (Darwin). Current host: ${system}"
82+
else if vmHostPackages.stdenv.hostPlatform.isAarch64 != pkgs.stdenv.hostPlatform.isAarch64
83+
then throw "vfkit requires matching host and guest architectures. Host: ${system}, Guest: ${pkgs.stdenv.hostPlatform.system}"
84+
else if user != null
85+
then throw "vfkit does not support changing user"
86+
else if balloon
87+
then throw "vfkit does not support memory ballooning"
88+
else if devices != []
89+
then throw "vfkit does not support device passthrough"
90+
else if credentialFiles != {}
91+
then throw "vfkit does not support credentialFiles"
92+
else if vsock.cid != null
93+
then throw "vfkit vsock support not yet implemented in microvm.nix"
94+
else if storeOnDisk
95+
then throw "vfkit does not support storeOnDisk. Use virtiofs shares instead (already configured in examples)."
96+
else
97+
let
98+
baseCmd = lib.escapeShellArgs allArgsWithoutSocket;
99+
vfkitCmd = lib.concatStringsSep " " (map lib.escapeShellArg allArgsWithoutSocket);
100+
in
101+
# vfkit requires absolute socket paths, so expand relative paths
102+
if socket != null
103+
then "bash -c ${lib.escapeShellArg ''
104+
SOCKET_ABS=${lib.escapeShellArg socket}
105+
[[ "$SOCKET_ABS" != /* ]] && SOCKET_ABS="$PWD/$SOCKET_ABS"
106+
exec ${vfkitCmd} --restful-uri "unix:///$SOCKET_ABS"
107+
''}"
108+
else baseCmd;
109+
110+
canShutdown = socket != null;
111+
112+
shutdownCommand =
113+
if socket != null
114+
then ''
115+
SOCKET_ABS="${lib.escapeShellArg socket}"
116+
[[ "$SOCKET_ABS" != /* ]] && SOCKET_ABS="$PWD/$SOCKET_ABS"
117+
echo '{"state": "Stop"}' | ${vmHostPackages.socat}/bin/socat - "UNIX-CONNECT:$SOCKET_ABS"
118+
''
119+
else throw "Cannot shutdown without socket";
120+
121+
supportsNotifySocket = false;
122+
123+
requiresMacvtapAsFds = false;
124+
}

nixos-modules/microvm/options.nix

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,18 @@ in
596596
description = "Type of IO engine to use for Firecracker drives (disks).";
597597
};
598598

599+
vfkit.extraArgs = mkOption {
600+
type = with types; listOf str;
601+
default = [];
602+
description = "Extra arguments to pass to vfkit.";
603+
};
604+
605+
vfkit.logLevel = mkOption {
606+
type = with types; nullOr (enum ["debug" "info" "error"]);
607+
default = "info";
608+
description = "vfkit log level.";
609+
};
610+
599611
prettyProcnames = mkOption {
600612
type = types.bool;
601613
default = true;

0 commit comments

Comments
 (0)