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