{ system ? builtins.currentSystem, config ? {}, pkgs ? import ../.. { inherit system config; }, debug ? false, enableUnfree ? false, # Nested KVM virtualization (https://www.linux-kvm.org/page/Nested_Guests) # requires a modprobe flag on the build machine: (kvm-amd for AMD CPUs) # boot.extraModprobeConfig = "options kvm-intel nested=Y"; # Without this VirtualBox will use SW virtualization and will only be able # to run 32-bit guests. useKvmNestedVirt ? false, # Whether to run 64-bit guests instead of 32-bit. Requires nested KVM. use64bitGuest ? false }: assert use64bitGuest -> useKvmNestedVirt; with import ../lib/testing.nix { inherit system pkgs; }; with pkgs.lib; let testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions; miniInit = '' #!${pkgs.stdenv.shell} -xe export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.utillinux ]}" mkdir -p /run/dbus cat > /etc/passwd < /etc/group <execute(ru '${logcmd} & disown');"; testVM = vmName: vmScript: let cfg = (import ../lib/eval-config.nix { system = if use64bitGuest then "x86_64-linux" else "i686-linux"; modules = [ ../modules/profiles/minimal.nix (testVMConfig vmName vmScript) ]; }).config; in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" { preVM = '' mkdir -p "$out" diskImage="$(pwd)/qimage" ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M ''; postVM = '' echo "creating VirtualBox disk image..." ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \ "$diskImage" "$out/disk.vdi" ''; buildInputs = [ pkgs.utillinux pkgs.perl ]; } '' ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1 mkdir /mnt mount /dev/vda1 /mnt cp "${cfg.system.build.kernel}/bzImage" /mnt/linux cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda cat > /mnt/grub/grub.cfg <execute(ru $cmd); return $status == 0; } sub cleanup_${name} { $machine->execute(ru "VBoxManage controlvm ${name} poweroff") if checkRunning_${name}; $machine->succeed("rm -rf ${sharePath}"); $machine->succeed("mkdir -p ${sharePath}"); $machine->succeed("chown alice.users ${sharePath}"); } sub createVM_${name} { vbm("createvm --name ${name} ${createFlags}"); vbm("modifyvm ${name} ${vmFlags}"); vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1"); vbm("storagectl ${name} ${controllerFlags}"); vbm("storageattach ${name} ${diskFlags}"); vbm("sharedfolder add ${name} ${sharedFlags}"); vbm("sharedfolder add ${name} ${nixstoreFlags}"); cleanup_${name}; ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"} } sub destroyVM_${name} { cleanup_${name}; vbm("unregistervm ${name} --delete"); } sub waitForVMBoot_${name} { $machine->execute(ru( 'set -e; i=0; '. 'while ! test -e ${sharePath}/boot-done; do '. 'sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; '. 'VBoxManage list runningvms | grep -q "^\"${name}\""; '. 'done' )); } sub waitForIP_${name} ($) { my $property = "/VirtualBox/GuestInfo/Net/$_[0]/V4/IP"; my $getip = "VBoxManage guestproperty get ${name} $property | ". "sed -n -e 's/^Value: //p'"; my $ip = $machine->succeed(ru( 'for i in $(seq 1000); do '. 'if ipaddr="$('.$getip.')" && [ -n "$ipaddr" ]; then '. 'echo "$ipaddr"; exit 0; '. 'fi; '. 'sleep 1; '. 'done; '. 'echo "Could not get IPv4 address for ${name}!" >&2; '. 'exit 1' )); chomp $ip; return $ip; } sub waitForStartup_${name} { for (my $i = 0; $i <= 120; $i += 10) { $machine->sleep(10); return if checkRunning_${name}; eval { $_[0]->() } if defined $_[0]; } die "VirtualBox VM didn't start up within 2 minutes"; } sub waitForShutdown_${name} { for (my $i = 0; $i <= 120; $i += 10) { $machine->sleep(10); return unless checkRunning_${name}; } die "VirtualBox VM didn't shut down within 2 minutes"; } sub shutdownVM_${name} { $machine->succeed(ru "touch ${sharePath}/shutdown"); $machine->execute( 'set -e; i=0; '. 'while test -e ${sharePath}/shutdown '. ' -o -e ${sharePath}/boot-done; do '. 'sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; '. 'done' ); waitForShutdown_${name}; } ''; }; hostonlyVMFlags = [ "--nictype1 virtio" "--nictype2 virtio" "--nic2 hostonly" "--hostonlyadapter2 vboxnet0" ]; # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI). enableExtensionPackVMFlags = [ "--usbxhci on" ]; dhcpScript = pkgs: '' ${pkgs.dhcp}/bin/dhclient \ -lf /run/dhcp.leases \ -pf /run/dhclient.pid \ -v eth0 eth1 otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)" ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP" echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || : ''; sysdDetectVirt = pkgs: '' ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result ''; vboxVMs = mapAttrs createVM { simple = {}; detectvirt.vmScript = sysdDetectVirt; test1.vmFlags = hostonlyVMFlags; test1.vmScript = dhcpScript; test2.vmFlags = hostonlyVMFlags; test2.vmScript = dhcpScript; headless.virtualisation.virtualbox.headless = true; headless.services.xserver.enable = false; }; vboxVMsWithExtpack = mapAttrs createVM { testExtensionPack.vmFlags = enableExtensionPackVMFlags; }; mkVBoxTest = useExtensionPack: vms: name: testScript: makeTest { name = "virtualbox-${name}"; machine = { lib, config, ... }: { imports = let mkVMConf = name: val: val.machine // { key = "${name}-config"; }; vmConfigs = mapAttrsToList mkVMConf vms; in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs; virtualisation.memorySize = 2048; virtualisation.qemu.options = if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else []; virtualisation.virtualbox.host.enable = true; services.xserver.displayManager.auto.user = "alice"; users.users.alice.extraGroups = let inherit (config.virtualisation.virtualbox.host) enableHardening; in lib.mkIf enableHardening (lib.singleton "vboxusers"); virtualisation.virtualbox.host.enableExtensionPack = useExtensionPack; nixpkgs.config.allowUnfree = useExtensionPack; }; testScript = '' sub ru ($) { my $esc = $_[0] =~ s/'/'\\${"'"}'/gr; return "su - alice -c '$esc'"; } sub vbm { $machine->succeed(ru("VBoxManage ".$_[0])); }; sub removeUUIDs { return join("\n", grep { $_ !~ /^UUID:/ } split(/\n/, $_[0]))."\n"; } ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)} $machine->waitForX; ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"} ${testScript} ''; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ aszlig cdepillabout ]; }; }; unfreeTests = mapAttrs (mkVBoxTest true vboxVMsWithExtpack) { enable-extension-pack = '' createVM_testExtensionPack; vbm("startvm testExtensionPack"); waitForStartup_testExtensionPack; $machine->screenshot("cli_started"); waitForVMBoot_testExtensionPack; $machine->screenshot("cli_booted"); $machine->nest("Checking for privilege escalation", sub { $machine->fail("test -e '/root/VirtualBox VMs'"); $machine->fail("test -e '/root/.config/VirtualBox'"); $machine->succeed("test -e '/home/alice/VirtualBox VMs'"); }); shutdownVM_testExtensionPack; destroyVM_testExtensionPack; ''; }; in mapAttrs (mkVBoxTest false vboxVMs) { simple-gui = '' createVM_simple; $machine->succeed(ru "VirtualBox &"); $machine->waitUntilSucceeds( ru "xprop -name 'Oracle VM VirtualBox Manager'" ); $machine->sleep(5); $machine->screenshot("gui_manager_started"); # Home to select Tools, down to move to the VM, enter to start it. $machine->sendKeys("home"); $machine->sendKeys("down"); $machine->sendKeys("ret"); $machine->screenshot("gui_manager_sent_startup"); waitForStartup_simple (sub { $machine->sendKeys("home"); $machine->sendKeys("down"); $machine->sendKeys("ret"); }); $machine->screenshot("gui_started"); waitForVMBoot_simple; $machine->screenshot("gui_booted"); shutdownVM_simple; $machine->sleep(5); $machine->screenshot("gui_stopped"); $machine->sendKeys("ctrl-q"); $machine->sleep(5); $machine->screenshot("gui_manager_stopped"); destroyVM_simple; ''; simple-cli = '' createVM_simple; vbm("startvm simple"); waitForStartup_simple; $machine->screenshot("cli_started"); waitForVMBoot_simple; $machine->screenshot("cli_booted"); $machine->nest("Checking for privilege escalation", sub { $machine->fail("test -e '/root/VirtualBox VMs'"); $machine->fail("test -e '/root/.config/VirtualBox'"); $machine->succeed("test -e '/home/alice/VirtualBox VMs'"); }); shutdownVM_simple; destroyVM_simple; ''; headless = '' createVM_headless; $machine->succeed(ru("VBoxHeadless --startvm headless & disown %1")); waitForStartup_headless; waitForVMBoot_headless; shutdownVM_headless; destroyVM_headless; ''; host-usb-permissions = '' my $userUSB = removeUUIDs vbm("list usbhost"); print STDERR $userUSB; my $rootUSB = removeUUIDs $machine->succeed("VBoxManage list usbhost"); print STDERR $rootUSB; die "USB host devices differ for root and normal user" if $userUSB ne $rootUSB; die "No USB host devices found" if $userUSB =~ //; ''; systemd-detect-virt = '' createVM_detectvirt; vbm("startvm detectvirt"); waitForStartup_detectvirt; waitForVMBoot_detectvirt; shutdownVM_detectvirt; my $result = $machine->succeed("cat '$detectvirt_sharepath/result'"); chomp $result; destroyVM_detectvirt; die "systemd-detect-virt returned \"$result\" instead of \"oracle\"" if $result ne "oracle"; ''; net-hostonlyif = '' createVM_test1; createVM_test2; vbm("startvm test1"); waitForStartup_test1; waitForVMBoot_test1; vbm("startvm test2"); waitForStartup_test2; waitForVMBoot_test2; $machine->screenshot("net_booted"); my $test1IP = waitForIP_test1 1; my $test2IP = waitForIP_test2 1; $machine->succeed("echo '$test2IP' | nc -N '$test1IP' 1234"); $machine->succeed("echo '$test1IP' | nc -N '$test2IP' 1234"); $machine->waitUntilSucceeds("nc -N '$test1IP' 5678 < /dev/null >&2"); $machine->waitUntilSucceeds("nc -N '$test2IP' 5678 < /dev/null >&2"); shutdownVM_test1; shutdownVM_test2; destroyVM_test1; destroyVM_test2; ''; } // (if enableUnfree then unfreeTests else {})