flake: { pkgs , name ? "sandbox" , user ? "dummy" , config ? { } , tools ? [] , envVars ? { } , restrictNetwork ? true # to be replaced with virtualisation.restrictNetwork , patchQemu9p ? false # until qemu 7.2.0 becomes available in nixpkgs }@params: let shellLib = flake.lib.shell { inherit pkgs; }; print = rec { printSharedDir = name: { source, target }: '' echo -en ${shellLib.fmt.keyword target}": " echo ${pkgs.lib.escapeShellArg source} ''; printSharedDirs = sharedDirs: '' ${shellLib.fmt.printSectionTitle "SHARED DIRECTORIES (guest: host)"} ${shellLib.mapAttrsToLines printSharedDir sharedDirs} ''; formatAddrPort = { address, port }: "${address}:${toString port}"; printForwardedPort = { from, proto, host, guest, }: if from == "host" then '' echo -n ${proto}' ' echo -en ${shellLib.fmt.keyword (formatAddrPort host)} echo -n ' -> ' echo ${formatAddrPort guest} '' else '' echo -n ${proto}' ' echo -n ${formatAddrPort host} echo -n ' <- ' echo -e ${shellLib.fmt.keyword (formatAddrPort guest)} ''; printForwardedPorts = portForwards: '' ${shellLib.fmt.printSectionTitle "FORWARDED PORTS (host <-> guest)"} ${pkgs.lib.concatMapStringsSep "\n" printForwardedPort portForwards} ''; printNetworkRestricted = isRestricted: if isRestricted then "Restricted" else "Unrestricted"; printNetworkAllowed = isRestricted: if isRestricted then "disallowed" else "allowed"; printRestrictedNetwork = isRestricted: '' ${shellLib.fmt.printSectionTitle "NETWORK ACCESS"} echo -en ${shellLib.fmt.keyword (printNetworkRestricted isRestricted)} echo -n ': local network and internet access ' echo ${printNetworkAllowed isRestricted}. ''; }; in rec { nixosConfigurations.${name} = pkgs.nixos ({ modulesPath, config, lib, pkgs, ... }: { imports = [ (modulesPath + "/profiles/minimal.nix") { environment.noXlibs = false; } # avoid mass rebuild (modulesPath + "/profiles/qemu-guest.nix") (modulesPath + "/virtualisation/qemu-vm.nix") params.config ]; system.stateVersion = lib.mkDefault lib.trivial.release; networking = { hostName = name; firewall.enable = lib.mkDefault false; }; users.users.${user} = { isNormalUser = lib.mkDefault true; password = lib.mkDefault ""; extraGroups = lib.mkDefault [ "wheel" ]; }; security.sudo.wheelNeedsPassword = lib.mkDefault false; services.getty = { autologinUser = lib.mkDefault user; helpLine = lib.mkDefault '' Press to terminate the virtual machine. ''; }; environment = { variables = envVars; systemPackages = tools; interactiveShellInit = lib.mkBefore '' ${shellLib.ifSomeAttrs envVars shellLib.printEnvVars} ${shellLib.ifSomeList tools shellLib.printBins} ${shellLib.ifSomeAttrs config.virtualisation.sharedDirectories print.printSharedDirs} ${shellLib.ifSomeList config.virtualisation.forwardPorts print.printForwardedPorts} ${print.printRestrictedNetwork restrictNetwork} ''; }; virtualisation = { graphics = lib.mkDefault false; diskImage = lib.mkDefault "$TMP_DISK"; sharedDirectories.host = { source = "$SHARED_CWD"; target = "/mnt"; }; # Uncomment when this is merged: # https://github.com/NixOS/nixpkgs/pull/200225 #restrictNetwork = lib.mkDefault true; # Patched QEMU to fix slow 9p file share. # https://linus.schreibt.jetzt/posts/qemu-9p-performance.html qemu.package = lib.mkDefault ( if patchQemu9p then assert !(pkgs.lib.versionAtLeast pkgs.qemu_kvm.version "7.2.0"); pkgs.qemu_kvm.overrideAttrs (o: { patches = o.patches ++ [ (pkgs.fetchpatch { name = "qemu-9p-performance-fix.patch"; url = "https://github.com/qemu/qemu/commit/f5265c8.patch"; sha256 = "sha256-PSOv0dhiEq9g6B1uIbs6vbhGr7BQWCtAoLHnk4vnvVg="; }) ]; }) else pkgs.qemu_kvm ); }; }); packages.${name} = nixosConfigurations.${name}.config.system.build.vm; apps.${name} = { type = "app"; program = toString (pkgs.writeShellScript "sandbox-vm" ( (pkgs.lib.optionalString restrictNetwork '' # Isolate from network # Stopgap solution until this is merged: # https://github.com/NixOS/nixpkgs/pull/200225 QEMU_NET_OPTS="restrict=yes,''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}" export QEMU_NET_OPTS '') + '' # Save current directory for mounting in VM SHARED_CWD=$PWD export SHARED_CWD TMP_DISK="$(mktemp).qcow2" export TMP_DISK trap "rm -f \"$TMP_DISK\"" EXIT ${packages.${name}}/bin/run-${name}-vm reset echo "Exited virtual machine." '')); }; }