Skip to content

Commit 6e7d7cb

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 1d05a3c commit 6e7d7cb

File tree

4 files changed

+197
-32
lines changed

4 files changed

+197
-32
lines changed

flake.nix

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@
156156
# currently broken:
157157
# "crosvm"
158158
];
159-
hypervisorsWithUserNet = [ "qemu" "kvmtool" ];
159+
hypervisorsWithUserNet = [ "qemu" "kvmtool" "vfkit" ];
160+
hypervisorsDarwinOnly = [ "vfkit" ];
160161
makeExample = { system, hypervisor, config ? {} }:
161162
nixpkgs.lib.nixosSystem {
162163
system = nixpkgs.lib.replaceString "-darwin" "-linux" system;
@@ -172,12 +173,21 @@
172173
nixpkgs.overlays = [ self.overlay ];
173174
microvm = {
174175
inherit hypervisor;
175-
# share the host's /nix/store if the hypervisor can do 9p
176-
shares = lib.optional (builtins.elem hypervisor hypervisorsWith9p) {
177-
tag = "ro-store";
178-
source = "/nix/store";
179-
mountPoint = "/nix/.ro-store";
180-
};
176+
# share the host's /nix/store if the hypervisor supports it
177+
shares =
178+
if builtins.elem hypervisor hypervisorsWith9p then [{
179+
tag = "ro-store";
180+
source = "/nix/store";
181+
mountPoint = "/nix/.ro-store";
182+
proto = "9p";
183+
}]
184+
else if hypervisor == "vfkit" then [{
185+
tag = "ro-store";
186+
source = "/nix/store";
187+
mountPoint = "/nix/.ro-store";
188+
proto = "virtiofs";
189+
}]
190+
else [];
181191
# writableStoreOverlay = "/nix/.rw-store";
182192
# volumes = [ {
183193
# image = "nix-store-overlay.img";
@@ -208,34 +218,44 @@
208218
};
209219
in
210220
(builtins.foldl' (results: system:
211-
builtins.foldl' ({ result, n }: hypervisor: {
212-
result = result // {
213-
"${system}-${hypervisor}-example" = makeExample {
214-
inherit system hypervisor;
215-
};
216-
} //
217-
nixpkgs.lib.optionalAttrs (builtins.elem hypervisor self.lib.hypervisorsWithNetwork) {
218-
"${system}-${hypervisor}-example-with-tap" = makeExample {
219-
inherit system hypervisor;
220-
config = _: {
221-
microvm.interfaces = [ {
222-
type = "tap";
223-
id = "vm-${builtins.substring 0 4 hypervisor}";
224-
mac = "02:00:00:01:01:0${toString n}";
225-
} ];
226-
networking = {
227-
interfaces.eth0.useDHCP = true;
228-
firewall.allowedTCPPorts = [ 22 ];
229-
};
230-
services.openssh = {
231-
enable = true;
232-
settings.PermitRootLogin = "yes";
221+
builtins.foldl' ({ result, n }: hypervisor:
222+
let
223+
# Skip darwin-only hypervisors on Linux systems
224+
isDarwinOnly = builtins.elem hypervisor hypervisorsDarwinOnly;
225+
isDarwinSystem = nixpkgs.lib.hasSuffix "-darwin" system;
226+
shouldSkip = isDarwinOnly && !isDarwinSystem;
227+
in
228+
if shouldSkip then { inherit result n; }
229+
else {
230+
result = result // {
231+
"${system}-${hypervisor}-example" = makeExample {
232+
inherit system hypervisor;
233+
};
234+
} //
235+
# Skip tap example for darwin-only hypervisors (vfkit doesn't support tap)
236+
nixpkgs.lib.optionalAttrs (builtins.elem hypervisor self.lib.hypervisorsWithNetwork && !isDarwinOnly) {
237+
"${system}-${hypervisor}-example-with-tap" = makeExample {
238+
inherit system hypervisor;
239+
config = _: {
240+
microvm.interfaces = [ {
241+
type = "tap";
242+
id = "vm-${builtins.substring 0 4 hypervisor}";
243+
mac = "02:00:00:01:01:0${toString n}";
244+
} ];
245+
networking = {
246+
interfaces.eth0.useDHCP = true;
247+
firewall.allowedTCPPorts = [ 22 ];
248+
};
249+
services.openssh = {
250+
enable = true;
251+
settings.PermitRootLogin = "yes";
252+
};
233253
};
234254
};
235255
};
236-
};
237-
n = n + 1;
238-
}) results self.lib.hypervisors
256+
n = n + 1;
257+
}
258+
) results self.lib.hypervisors
239259
) { result = {}; n = 1; } systems).result;
240260
};
241261
}

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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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=\"${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+
++
43+
(builtins.concatMap ({ proto, source, tag, ... }:
44+
if proto == "virtiofs" then [
45+
"--device" "virtio-fs,sharedDir=${source},mountTag=${tag}"
46+
]
47+
else if proto == "9p" then
48+
throw "vfkit does not support 9p shares on macOS. Use proto = \"virtiofs\" instead."
49+
else
50+
throw "Unknown share protocol: ${proto}"
51+
) shares)
52+
++
53+
(builtins.concatMap ({ type, id, mac, ... }:
54+
if type == "user" then [
55+
"--device" "virtio-net,nat,mac=${mac}"
56+
]
57+
else if type == "tap" then
58+
throw "vfkit does not support tap networking on macOS. Use type = \"user\" for NAT networking."
59+
else if type == "bridge" then
60+
throw "vfkit bridge networking requires vmnet-helper which is not yet implemented. Use type = \"user\" for NAT networking."
61+
else if type == "macvtap" then
62+
throw "vfkit does not support macvtap networking on macOS. Use type = \"user\" for NAT networking."
63+
else
64+
throw "Unknown network interface type: ${type}"
65+
) interfaces);
66+
67+
allArgsWithoutSocket = [
68+
"${vfkit}/bin/vfkit"
69+
"--cpus" (toString vcpu)
70+
"--memory" (toString mem)
71+
]
72+
++ lib.optionals (logLevel != null) [
73+
"--log-level" logLevel
74+
]
75+
++ bootloaderArgs
76+
++ deviceArgs
77+
++ extraArgs;
78+
79+
in
80+
{
81+
tapMultiQueue = false;
82+
83+
preStart = lib.optionalString (socket != null) ''
84+
rm -f ${socket}
85+
'';
86+
87+
command =
88+
if !vmHostPackages.stdenv.hostPlatform.isDarwin
89+
then throw "vfkit only works on macOS (Darwin). Current host: ${system}"
90+
else if vmHostPackages.stdenv.hostPlatform.isAarch64 != pkgs.stdenv.hostPlatform.isAarch64
91+
then throw "vfkit requires matching host and guest architectures. Host: ${system}, Guest: ${pkgs.stdenv.hostPlatform.system}"
92+
else if user != null
93+
then throw "vfkit does not support changing user"
94+
else if balloon
95+
then throw "vfkit does not support memory ballooning"
96+
else if devices != []
97+
then throw "vfkit does not support device passthrough"
98+
else if credentialFiles != {}
99+
then throw "vfkit does not support credentialFiles"
100+
else if vsock.cid != null
101+
then throw "vfkit vsock support not yet implemented in microvm.nix"
102+
else if storeOnDisk
103+
then throw "vfkit does not support storeOnDisk. Use virtiofs shares instead (already configured in examples)."
104+
else
105+
let
106+
baseCmd = lib.escapeShellArgs allArgsWithoutSocket;
107+
vfkitCmd = lib.concatStringsSep " " (map lib.escapeShellArg allArgsWithoutSocket);
108+
in
109+
# vfkit requires absolute socket paths, so expand relative paths
110+
if socket != null
111+
then "bash -c ${lib.escapeShellArg ''
112+
SOCKET_ABS=${lib.escapeShellArg socket}
113+
[[ "$SOCKET_ABS" != /* ]] && SOCKET_ABS="$PWD/$SOCKET_ABS"
114+
exec ${vfkitCmd} --restful-uri "unix:///$SOCKET_ABS"
115+
''}"
116+
else baseCmd;
117+
118+
canShutdown = socket != null;
119+
120+
shutdownCommand =
121+
if socket != null
122+
then ''
123+
SOCKET_ABS="${lib.escapeShellArg socket}"
124+
[[ "$SOCKET_ABS" != /* ]] && SOCKET_ABS="$PWD/$SOCKET_ABS"
125+
echo '{"state": "Stop"}' | ${vmHostPackages.socat}/bin/socat - "UNIX-CONNECT:$SOCKET_ABS"
126+
''
127+
else throw "Cannot shutdown without socket";
128+
129+
supportsNotifySocket = false;
130+
131+
requiresMacvtapAsFds = false;
132+
}

nixos-modules/microvm/options.nix

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,18 @@ in
590590
description = "Custom CPU template passed to firecracker.";
591591
};
592592

593+
vfkit.extraArgs = mkOption {
594+
type = with types; listOf str;
595+
default = [];
596+
description = "Extra arguments to pass to vfkit.";
597+
};
598+
599+
vfkit.logLevel = mkOption {
600+
type = with types; nullOr (enum ["debug" "info" "error"]);
601+
default = "info";
602+
description = "vfkit log level.";
603+
};
604+
593605
prettyProcnames = mkOption {
594606
type = types.bool;
595607
default = true;

0 commit comments

Comments
 (0)