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