1{ system ? builtins.currentSystem, debug ? false }:
2
3with import ../lib/testing.nix { inherit system; };
4with pkgs.lib;
5
6let
7 testVMConfig = vmName: attrs: { config, pkgs, ... }: let
8 guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
9
10 miniInit = ''
11 #!${pkgs.stdenv.shell} -xe
12 export PATH="${pkgs.coreutils}/bin:${pkgs.utillinux}/bin"
13
14 mkdir -p /etc/dbus-1 /var/run/dbus
15 cat > /etc/passwd <<EOF
16 root:x:0:0::/root:/bin/false
17 messagebus:x:1:1::/var/run/dbus:/bin/false
18 EOF
19 cat > /etc/group <<EOF
20 root:x:0:
21 messagebus:x:1:
22 EOF
23 cp -v "${pkgs.dbus.daemon}/etc/dbus-1/system.conf" \
24 /etc/dbus-1/system.conf
25 "${pkgs.dbus.daemon}/bin/dbus-daemon" --fork --system
26
27 ${guestAdditions}/bin/VBoxService
28 ${(attrs.vmScript or (const "")) pkgs}
29
30 i=0
31 while [ ! -e /mnt-root/shutdown ]; do
32 sleep 10
33 i=$(($i + 10))
34 [ $i -le 120 ] || fail
35 done
36
37 rm -f /mnt-root/boot-done /mnt-root/shutdown
38 '';
39 in {
40 boot.kernelParams = [
41 "console=tty0" "console=ttyS0" "ignore_loglevel"
42 "boot.trace" "panic=1" "boot.panic_on_fail"
43 "init=${pkgs.writeScript "mini-init.sh" miniInit}"
44 ];
45
46 fileSystems."/" = {
47 device = "vboxshare";
48 fsType = "vboxsf";
49 };
50
51 virtualisation.virtualbox.guest.enable = true;
52
53 boot.initrd.kernelModules = [
54 "af_packet" "vboxsf"
55 "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest"
56 ];
57
58 boot.initrd.extraUtilsCommands = ''
59 copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
60 copy_bin_and_libs "${pkgs.utillinux}/bin/unshare"
61 ${(attrs.extraUtilsCommands or (const "")) pkgs}
62 '';
63
64 boot.initrd.postMountCommands = ''
65 touch /mnt-root/boot-done
66 hostname "${vmName}"
67 mkdir -p /nix/store
68 unshare -m "@shell@" -c '
69 mount -t vboxsf nixstore /nix/store
70 exec "$stage2Init"
71 '
72 poweroff -f
73 '';
74
75 system.requiredKernelConfig = with config.lib.kernelConfig; [
76 (isYes "SERIAL_8250_CONSOLE")
77 (isYes "SERIAL_8250")
78 ];
79 };
80
81 mkLog = logfile: tag: let
82 rotated = map (i: "${logfile}.${toString i}") (range 1 9);
83 all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated);
84 logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
85 in optionalString debug "$machine->execute(ru '${logcmd} & disown');";
86
87 testVM = vmName: vmScript: let
88 cfg = (import ../lib/eval-config.nix {
89 system = "i686-linux";
90 modules = [
91 ../modules/profiles/minimal.nix
92 (testVMConfig vmName vmScript)
93 ];
94 }).config;
95 in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
96 preVM = ''
97 mkdir -p "$out"
98 diskImage="$(pwd)/qimage"
99 ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
100 '';
101
102 postVM = ''
103 echo "creating VirtualBox disk image..."
104 ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
105 "$diskImage" "$out/disk.vdi"
106 '';
107
108 buildInputs = [ pkgs.utillinux pkgs.perl ];
109 } ''
110 ${pkgs.parted}/sbin/parted /dev/vda mklabel msdos
111 ${pkgs.parted}/sbin/parted /dev/vda -- mkpart primary ext2 1M -1s
112 . /sys/class/block/vda1/uevent
113 mknod /dev/vda1 b $MAJOR $MINOR
114
115 ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
116 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
117 mkdir /mnt
118 mount /dev/vda1 /mnt
119 cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
120 cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
121
122 ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
123
124 cat > /mnt/grub/grub.cfg <<GRUB
125 set root=hd0,1
126 linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
127 initrd /initrd
128 boot
129 GRUB
130 umount /mnt
131 '');
132
133 createVM = name: attrs: let
134 mkFlags = concatStringsSep " ";
135
136 sharePath = "/home/alice/vboxshare-${name}";
137
138 createFlags = mkFlags [
139 "--ostype Linux26"
140 "--register"
141 ];
142
143 vmFlags = mkFlags ([
144 "--uart1 0x3F8 4"
145 "--uartmode1 client /run/virtualbox-log-${name}.sock"
146 "--memory 768"
147 ] ++ (attrs.vmFlags or []));
148
149 controllerFlags = mkFlags [
150 "--name SATA"
151 "--add sata"
152 "--bootable on"
153 "--hostiocache on"
154 ];
155
156 diskFlags = mkFlags [
157 "--storagectl SATA"
158 "--port 0"
159 "--device 0"
160 "--type hdd"
161 "--mtype immutable"
162 "--medium ${testVM name attrs}/disk.vdi"
163 ];
164
165 sharedFlags = mkFlags [
166 "--name vboxshare"
167 "--hostpath ${sharePath}"
168 ];
169
170 nixstoreFlags = mkFlags [
171 "--name nixstore"
172 "--hostpath /nix/store"
173 "--readonly"
174 ];
175 in {
176 machine = {
177 systemd.sockets."vboxtestlog-${name}" = {
178 description = "VirtualBox Test Machine Log Socket For ${name}";
179 wantedBy = [ "sockets.target" ];
180 before = [ "multi-user.target" ];
181 socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
182 socketConfig.Accept = true;
183 };
184
185 systemd.services."vboxtestlog-${name}@" = {
186 description = "VirtualBox Test Machine Log For ${name}";
187 serviceConfig.StandardInput = "socket";
188 serviceConfig.StandardOutput = "syslog";
189 serviceConfig.SyslogIdentifier = "GUEST-${name}";
190 serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
191 };
192 };
193
194 testSubs = ''
195 my ${"$" + name}_sharepath = '${sharePath}';
196
197 sub checkRunning_${name} {
198 my $cmd = 'VBoxManage list runningvms | grep -q "^\"${name}\""';
199 my ($status, $out) = $machine->execute(ru $cmd);
200 return $status == 0;
201 }
202
203 sub cleanup_${name} {
204 $machine->execute(ru "VBoxManage controlvm ${name} poweroff")
205 if checkRunning_${name};
206 $machine->succeed("rm -rf ${sharePath}");
207 $machine->succeed("mkdir -p ${sharePath}");
208 $machine->succeed("chown alice.users ${sharePath}");
209 }
210
211 sub createVM_${name} {
212 vbm("createvm --name ${name} ${createFlags}");
213 vbm("modifyvm ${name} ${vmFlags}");
214 vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1");
215 vbm("storagectl ${name} ${controllerFlags}");
216 vbm("storageattach ${name} ${diskFlags}");
217 vbm("sharedfolder add ${name} ${sharedFlags}");
218 vbm("sharedfolder add ${name} ${nixstoreFlags}");
219 cleanup_${name};
220
221 ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
222 }
223
224 sub destroyVM_${name} {
225 cleanup_${name};
226 vbm("unregistervm ${name} --delete");
227 }
228
229 sub waitForVMBoot_${name} {
230 $machine->execute(ru(
231 'set -e; i=0; '.
232 'while ! test -e ${sharePath}/boot-done; do '.
233 'sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; '.
234 'VBoxManage list runningvms | grep -q "^\"${name}\""; '.
235 'done'
236 ));
237 }
238
239 sub waitForIP_${name} ($) {
240 my $property = "/VirtualBox/GuestInfo/Net/$_[0]/V4/IP";
241 my $getip = "VBoxManage guestproperty get ${name} $property | ".
242 "sed -n -e 's/^Value: //p'";
243 my $ip = $machine->succeed(ru(
244 'for i in $(seq 1000); do '.
245 'if ipaddr="$('.$getip.')" && [ -n "$ipaddr" ]; then '.
246 'echo "$ipaddr"; exit 0; '.
247 'fi; '.
248 'sleep 1; '.
249 'done; '.
250 'echo "Could not get IPv4 address for ${name}!" >&2; '.
251 'exit 1'
252 ));
253 chomp $ip;
254 return $ip;
255 }
256
257 sub waitForStartup_${name} {
258 for (my $i = 0; $i <= 120; $i += 10) {
259 $machine->sleep(10);
260 return if checkRunning_${name};
261 eval { $_[0]->() } if defined $_[0];
262 }
263 die "VirtualBox VM didn't start up within 2 minutes";
264 }
265
266 sub waitForShutdown_${name} {
267 for (my $i = 0; $i <= 120; $i += 10) {
268 $machine->sleep(10);
269 return unless checkRunning_${name};
270 }
271 die "VirtualBox VM didn't shut down within 2 minutes";
272 }
273
274 sub shutdownVM_${name} {
275 $machine->succeed(ru "touch ${sharePath}/shutdown");
276 $machine->waitUntilSucceeds(
277 "test ! -e ${sharePath}/shutdown ".
278 " -a ! -e ${sharePath}/boot-done"
279 );
280 waitForShutdown_${name};
281 }
282 '';
283 };
284
285 hostonlyVMFlags = [
286 "--nictype1 virtio"
287 "--nictype2 virtio"
288 "--nic2 hostonly"
289 "--hostonlyadapter2 vboxnet0"
290 ];
291
292 dhcpScript = pkgs: ''
293 ${pkgs.dhcp}/bin/dhclient \
294 -lf /run/dhcp.leases \
295 -pf /run/dhclient.pid \
296 -v eth0 eth1
297
298 otherIP="$(${pkgs.netcat}/bin/netcat -clp 1234 || :)"
299 ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
300 echo "$otherIP reachable" | ${pkgs.netcat}/bin/netcat -clp 5678 || :
301 '';
302
303 sysdDetectVirt = pkgs: ''
304 ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
305 '';
306
307 vboxVMs = mapAttrs createVM {
308 simple = {};
309
310 detectvirt.vmScript = sysdDetectVirt;
311
312 test1.vmFlags = hostonlyVMFlags;
313 test1.vmScript = dhcpScript;
314
315 test2.vmFlags = hostonlyVMFlags;
316 test2.vmScript = dhcpScript;
317 };
318
319 mkVBoxTest = name: testScript: makeTest {
320 name = "virtualbox-${name}";
321
322 machine = { lib, config, ... }: {
323 imports = let
324 mkVMConf = name: val: val.machine // { key = "${name}-config"; };
325 vmConfigs = mapAttrsToList mkVMConf vboxVMs;
326 in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
327 virtualisation.memorySize = 2048;
328 virtualisation.virtualbox.host.enable = true;
329 users.extraUsers.alice.extraGroups = let
330 inherit (config.virtualisation.virtualbox.host) enableHardening;
331 in lib.mkIf enableHardening (lib.singleton "vboxusers");
332 };
333
334 testScript = ''
335 sub ru ($) {
336 my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
337 return "su - alice -c '$esc'";
338 }
339
340 sub vbm {
341 $machine->succeed(ru("VBoxManage ".$_[0]));
342 };
343
344 sub removeUUIDs {
345 return join("\n", grep { $_ !~ /^UUID:/ } split(/\n/, $_[0]))."\n";
346 }
347
348 ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vboxVMs)}
349
350 $machine->waitForX;
351
352 ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
353
354 ${testScript}
355 '';
356
357 meta = with pkgs.stdenv.lib.maintainers; {
358 maintainers = [ aszlig wkennington ];
359 };
360 };
361
362in mapAttrs mkVBoxTest {
363 simple-gui = ''
364 createVM_simple;
365 $machine->succeed(ru "VirtualBox &");
366 $machine->waitForWindow(qr/Oracle VM VirtualBox Manager/);
367 $machine->sleep(5);
368 $machine->screenshot("gui_manager_started");
369 $machine->sendKeys("ret");
370 $machine->screenshot("gui_manager_sent_startup");
371 waitForStartup_simple (sub {
372 $machine->sendKeys("ret");
373 });
374 $machine->screenshot("gui_started");
375 waitForVMBoot_simple;
376 $machine->screenshot("gui_booted");
377 shutdownVM_simple;
378 $machine->sleep(5);
379 $machine->screenshot("gui_stopped");
380 $machine->sendKeys("ctrl-q");
381 $machine->sleep(5);
382 $machine->screenshot("gui_manager_stopped");
383 '';
384
385 simple-cli = ''
386 createVM_simple;
387 vbm("startvm simple");
388 waitForStartup_simple;
389 $machine->screenshot("cli_started");
390 waitForVMBoot_simple;
391 $machine->screenshot("cli_booted");
392
393 $machine->nest("Checking for privilege escalation", sub {
394 $machine->fail("test -e '/root/VirtualBox VMs'");
395 $machine->fail("test -e '/root/.config/VirtualBox'");
396 $machine->succeed("test -e '/home/alice/VirtualBox VMs'");
397 });
398
399 shutdownVM_simple;
400 '';
401
402 host-usb-permissions = ''
403 my $userUSB = removeUUIDs vbm("list usbhost");
404 print STDERR $userUSB;
405 my $rootUSB = removeUUIDs $machine->succeed("VBoxManage list usbhost");
406 print STDERR $rootUSB;
407
408 die "USB host devices differ for root and normal user"
409 if $userUSB ne $rootUSB;
410 die "No USB host devices found" if $userUSB =~ /<none>/;
411 '';
412
413 systemd-detect-virt = ''
414 createVM_detectvirt;
415 vbm("startvm detectvirt");
416 waitForStartup_detectvirt;
417 waitForVMBoot_detectvirt;
418 shutdownVM_detectvirt;
419 my $result = $machine->succeed("cat '$detectvirt_sharepath/result'");
420 chomp $result;
421 destroyVM_detectvirt;
422 die "systemd-detect-virt returned \"$result\" instead of \"oracle\""
423 if $result ne "oracle";
424 '';
425
426 net-hostonlyif = ''
427 createVM_test1;
428 createVM_test2;
429
430 vbm("startvm test1");
431 waitForStartup_test1;
432 waitForVMBoot_test1;
433
434 vbm("startvm test2");
435 waitForStartup_test2;
436 waitForVMBoot_test2;
437
438 $machine->screenshot("net_booted");
439
440 my $test1IP = waitForIP_test1 1;
441 my $test2IP = waitForIP_test2 1;
442
443 $machine->succeed("echo '$test2IP' | netcat -c '$test1IP' 1234");
444 $machine->succeed("echo '$test1IP' | netcat -c '$test2IP' 1234");
445
446 $machine->waitUntilSucceeds("netcat -c '$test1IP' 5678 >&2");
447 $machine->waitUntilSucceeds("netcat -c '$test2IP' 5678 >&2");
448
449 shutdownVM_test1;
450 shutdownVM_test2;
451
452 destroyVM_test1;
453 destroyVM_test2;
454 '';
455}