From 94f299bb8103ec9824296c07aa51c07b9fcf9227 Mon Sep 17 00:00:00 2001 From: Git Sagar Date: Sat, 23 May 2026 19:16:35 -0300 Subject: [PATCH] sync with labv2.nix + standalone flake with toDisk app Previous history: - https://git.sagar.ch/dotfiles/labv2.nix/commit/c359054 daku working! - https://git.sagar.ch/dotfiles/labv2.nix/commit/8de5cff fix integer overflow in vmix network lib - https://git.sagar.ch/dotfiles/labv2.nix/commit/9c25a66 daku on 25.05. with ollama - https://git.sagar.ch/dotfiles/labv2.nix/commit/385a3bf vmix enables relaxed sandbox - https://git.sagar.ch/dotfiles/labv2.nix/commit/c363da1 restructure vmixLib into linux/windows subattrs with OS-specific customizeImage - https://git.sagar.ch/dotfiles/labv2.nix/commit/edd4dc2 vmix: port namespace model and module improvements from conf.nix - https://git.sagar.ch/dotfiles/labv2.nix/commit/6666ecf vmix: add SPICE support, install virtio guest tools with SPICE agent - https://git.sagar.ch/dotfiles/labv2.nix/commit/46f5671 vmix: add QEMU guest agent channel for Windows VMs - https://git.sagar.ch/dotfiles/labv2.nix/commit/e1fea34 vmix: add Win11 LTSC 2024 image, refactor VirtIO driver selection - https://git.sagar.ch/dotfiles/labv2.nix/commit/c27ae68 vmix: make customizeImage chroot-sandboxed by default, opt-in impure - https://git.sagar.ch/dotfiles/labv2.nix/commit/305fbac virt customize needs chroot for now due to usr bin env things. could be fixed later - https://git.sagar.ch/dotfiles/labv2.nix/commit/264d30f vmix: add win10 VM on desk, disable SMB signing for guest Samba access - https://git.sagar.ch/dotfiles/labv2.nix/commit/9b64f51 vmix: split Windows templates into per-category files, add comprehensive debloat - https://git.sagar.ch/dotfiles/labv2.nix/commit/ef91bf8 vmix: fix missing parent registry keys in Windows templates - https://git.sagar.ch/dotfiles/labv2.nix/commit/f87f340 win10 VM on panda with AMD GPU + USB passthrough - https://git.sagar.ch/dotfiles/labv2.nix/commit/38e474f vmix: split Windows build into Audit Mode install + composable templates - https://git.sagar.ch/dotfiles/labv2.nix/commit/a6a8db3 vmix: win11 support, remove build VNC, switch VMs to SPICE - https://git.sagar.ch/dotfiles/labv2.nix/commit/6cf5a21 generalize stage sets bg color, accent color and sets visual effects to performance - https://git.sagar.ch/dotfiles/labv2.nix/commit/a84849f remove rdp template since it doesn't even work - https://git.sagar.ch/dotfiles/labv2.nix/commit/5245263 vmix: best performance template + generalize cleanup - https://git.sagar.ch/dotfiles/labv2.nix/commit/ab12dd3 vmix: use CopyProfile for best performance visual effects - https://git.sagar.ch/dotfiles/labv2.nix/commit/bce3326 vmix: CopyProfile for best performance visual effects - https://git.sagar.ch/dotfiles/labv2.nix/commit/2496107 vmix: add app templates (7zip, VLC, ImageGlass, Edge WebView, VC++ runtimes) - https://git.sagar.ch/dotfiles/labv2.nix/commit/29a6123 wip: debug default associations xml - https://git.sagar.ch/dotfiles/labv2.nix/commit/2a2e5f5 vmix: fix DefaultAssociations.xml cmd.exe escaping - https://git.sagar.ch/dotfiles/labv2.nix/commit/cc6ff9d vmix: move DefaultAssociations.xml to template only - https://git.sagar.ch/dotfiles/labv2.nix/commit/a4a78ec vmix: add removeWMP template to remove Windows Media Player - https://git.sagar.ch/dotfiles/labv2.nix/commit/3fe56de vmix: improved Edge removal (files, shortcuts, scheduled tasks) - https://git.sagar.ch/dotfiles/labv2.nix/commit/a491767 vmix: fully remove Edge via post-oobe AppxPackage removal - https://git.sagar.ch/dotfiles/labv2.nix/commit/6ca1619 vmix: remove Edge DevToolsClient SystemApps + AppxPackage - https://git.sagar.ch/dotfiles/labv2.nix/commit/0c1ec35 vmix: sandboxie windows app template - https://git.sagar.ch/dotfiles/labv2.nix/commit/628bbd2 vmix: add Sandboxie-Plus template - https://git.sagar.ch/dotfiles/labv2.nix/commit/f055a41 vmix: reorganize templates, add file associations, remove Paint - https://git.sagar.ch/dotfiles/labv2.nix/commit/34326f4 vmix: set Thorium as default browser via PS-SFTA in post-oobe - https://git.sagar.ch/dotfiles/labv2.nix/commit/86af258 vmix: Active Setup for default browser (all users, no post-oobe needed) - https://git.sagar.ch/dotfiles/labv2.nix/commit/35b8cb0 remove vnc display from thorium template - https://git.sagar.ch/dotfiles/labv2.nix/commit/c7e0af6 vmix: fix Win11 generalize timeout + UCPD disable for URL associations - https://git.sagar.ch/dotfiles/labv2.nix/commit/43a1345 vmix: add Office 2024 template + Ohook activation in generalize - https://git.sagar.ch/dotfiles/labv2.nix/commit/03bbce0 vmix: updated office installation xml. more privacy options enabled - https://git.sagar.ch/dotfiles/labv2.nix/commit/790a0ee vmix: thorium installation - hide SFTA window - https://git.sagar.ch/dotfiles/labv2.nix/commit/a0e5c18 vmix: fix office install.bat call + add privacy registry policies - https://git.sagar.ch/dotfiles/labv2.nix/commit/3df38ca vmix: fix Ohook activation + suppress Office theme dialog - https://git.sagar.ch/dotfiles/labv2.nix/commit/df39ba3 vmix: remove sandboxie shortcut from desktop - https://git.sagar.ch/dotfiles/labv2.nix/commit/50d5972 vmix: skip Sandboxie desktop shortcut via installer flag - https://git.sagar.ch/dotfiles/labv2.nix/commit/ee2fa0f vmix: fix win10 default browser - https://git.sagar.ch/dotfiles/labv2.nix/commit/938315b vmix: windows: set accent color to automatic. remove accent color from unnecessary elements - https://git.sagar.ch/dotfiles/labv2.nix/commit/beceda8 vmix: allow ISO-only VMs without OS disk, add WinPE VM to panda Flake outputs: overlays.default, nixosModules.default, lib, apps.toDisk Co-Authored-By: Claude Opus 4.6 (1M context) --- flake.nix | 95 +++++- lib/default.nix | 3 +- lib/images/commons/default.nix | 6 - lib/images/default.nix | 8 +- .../{commons => linux}/customizeImage.nix | 22 +- lib/images/{ => linux}/debian/default.nix | 10 +- .../customs.nix => linux/debian/images.nix} | 8 +- lib/images/{ => linux}/debian/templates.nix | 66 +++- lib/images/{ => linux}/debian/upstream.json | 6 +- lib/images/linux/default.nix | 14 + .../{commons => linux}/scripts-n-files.nix | 10 + lib/images/windows/default.nix | 32 ++ .../windows/drivers/amd-gpu-drivers.nix | 5 + lib/images/windows/drivers/default.nix | 4 + .../windows/drivers/virtio-iso-w10-11.nix | 6 + lib/images/windows/helpers/customizeImage.nix | 113 +++++++ .../helpers/makeAuditModeAutounattend.nix | 174 ++++++++++ lib/images/windows/helpers/makeFilesISO.nix | 23 ++ lib/images/windows/helpers/makeImage.nix | 69 ++++ lib/images/windows/helpers/makeWinISO.nix | 40 +++ .../templates/apps/7zip-file-associations.reg | Bin 0 -> 36166 bytes lib/images/windows/templates/apps/7zip.nix | 20 ++ .../windows/templates/apps/edge-webview.nix | 18 + .../apps/imageglass-file-associations.reg | Bin 0 -> 105440 bytes .../windows/templates/apps/imageglass.nix | 26 ++ lib/images/windows/templates/apps/office.nix | 29 ++ .../windows/templates/apps/sandboxie.nix | 16 + lib/images/windows/templates/apps/thorium.nix | 40 +++ lib/images/windows/templates/apps/vlc.nix | 19 ++ lib/images/windows/templates/default-apps.nix | 49 +++ lib/images/windows/templates/default.nix | 43 +++ .../templates/essentials/amd-gpu-drivers.nix | 11 + .../templates/essentials/best-performance.nix | 46 +++ .../essentials/clear-file-associations.nix | 13 + .../templates/essentials/remove-edge.nix | 50 +++ .../templates/essentials/remove-ie.nix | 12 + .../templates/essentials/remove-paint.nix | 20 ++ .../templates/essentials/remove-wmp.nix | 11 + .../templates/essentials/vcpp-runtimes.nix | 18 + .../templates/essentials/virtio-tools.nix | 18 + lib/images/windows/templates/generalize.nix | 168 ++++++++++ lib/images/windows/templates/registry/ai.nix | 10 + .../windows/templates/registry/consumer.nix | 50 +++ .../windows/templates/registry/default.nix | 63 ++++ .../windows/templates/registry/defender.nix | 11 + .../templates/registry/disable-ucpd.nix | 10 + .../templates/registry/error-reporting.nix | 9 + .../templates/registry/hibernation.nix | 9 + .../templates/registry/insecure-samba.nix | 12 + .../templates/registry/performance.nix | 34 ++ .../windows/templates/registry/privacy.nix | 44 +++ .../templates/registry/smart-screen.nix | 6 + .../templates/registry/system-restore.nix | 8 + .../windows/templates/registry/telemetry.nix | 15 + .../windows/templates/registry/updates.nix | 21 ++ lib/images/windows/win10/default.nix | 12 + lib/images/windows/win10/images.nix | 34 ++ lib/images/windows/win10/upstream.json | 11 + lib/images/windows/win11/default.nix | 12 + lib/images/windows/win11/images.nix | 38 +++ lib/images/windows/win11/upstream.json | 11 + lib/network.nix | 33 +- module.nix | 6 + nixos/default.nix | 12 +- nixos/namespaceSubmoduleOptions.nix | 13 + nixos/network/config.nix | 215 ------------ nixos/network/options.nix | 186 ----------- nixos/networks/config.nix | 290 ++++++++++++++++ nixos/{network => networks}/default.nix | 6 - nixos/networks/options.nix | 137 ++++++++ nixos/vm/config.nix | 150 --------- nixos/vm/default.nix | 11 - nixos/vm/options.nix | 167 ---------- nixos/vms/config.nix | 272 +++++++++++++++ nixos/vms/default.nix | 5 + nixos/vms/submoduleOptions.nix | 309 ++++++++++++++++++ overlay.nix | 8 +- 77 files changed, 2785 insertions(+), 796 deletions(-) delete mode 100644 lib/images/commons/default.nix rename lib/images/{commons => linux}/customizeImage.nix (76%) rename lib/images/{ => linux}/debian/default.nix (59%) rename lib/images/{debian/customs.nix => linux/debian/images.nix} (77%) rename lib/images/{ => linux}/debian/templates.nix (51%) rename lib/images/{ => linux}/debian/upstream.json (78%) create mode 100644 lib/images/linux/default.nix rename lib/images/{commons => linux}/scripts-n-files.nix (84%) create mode 100644 lib/images/windows/default.nix create mode 100644 lib/images/windows/drivers/amd-gpu-drivers.nix create mode 100644 lib/images/windows/drivers/default.nix create mode 100644 lib/images/windows/drivers/virtio-iso-w10-11.nix create mode 100644 lib/images/windows/helpers/customizeImage.nix create mode 100644 lib/images/windows/helpers/makeAuditModeAutounattend.nix create mode 100644 lib/images/windows/helpers/makeFilesISO.nix create mode 100644 lib/images/windows/helpers/makeImage.nix create mode 100644 lib/images/windows/helpers/makeWinISO.nix create mode 100644 lib/images/windows/templates/apps/7zip-file-associations.reg create mode 100644 lib/images/windows/templates/apps/7zip.nix create mode 100644 lib/images/windows/templates/apps/edge-webview.nix create mode 100644 lib/images/windows/templates/apps/imageglass-file-associations.reg create mode 100644 lib/images/windows/templates/apps/imageglass.nix create mode 100644 lib/images/windows/templates/apps/office.nix create mode 100644 lib/images/windows/templates/apps/sandboxie.nix create mode 100644 lib/images/windows/templates/apps/thorium.nix create mode 100644 lib/images/windows/templates/apps/vlc.nix create mode 100644 lib/images/windows/templates/default-apps.nix create mode 100644 lib/images/windows/templates/default.nix create mode 100644 lib/images/windows/templates/essentials/amd-gpu-drivers.nix create mode 100644 lib/images/windows/templates/essentials/best-performance.nix create mode 100644 lib/images/windows/templates/essentials/clear-file-associations.nix create mode 100644 lib/images/windows/templates/essentials/remove-edge.nix create mode 100644 lib/images/windows/templates/essentials/remove-ie.nix create mode 100644 lib/images/windows/templates/essentials/remove-paint.nix create mode 100644 lib/images/windows/templates/essentials/remove-wmp.nix create mode 100644 lib/images/windows/templates/essentials/vcpp-runtimes.nix create mode 100644 lib/images/windows/templates/essentials/virtio-tools.nix create mode 100644 lib/images/windows/templates/generalize.nix create mode 100644 lib/images/windows/templates/registry/ai.nix create mode 100644 lib/images/windows/templates/registry/consumer.nix create mode 100644 lib/images/windows/templates/registry/default.nix create mode 100644 lib/images/windows/templates/registry/defender.nix create mode 100644 lib/images/windows/templates/registry/disable-ucpd.nix create mode 100644 lib/images/windows/templates/registry/error-reporting.nix create mode 100644 lib/images/windows/templates/registry/hibernation.nix create mode 100644 lib/images/windows/templates/registry/insecure-samba.nix create mode 100644 lib/images/windows/templates/registry/performance.nix create mode 100644 lib/images/windows/templates/registry/privacy.nix create mode 100644 lib/images/windows/templates/registry/smart-screen.nix create mode 100644 lib/images/windows/templates/registry/system-restore.nix create mode 100644 lib/images/windows/templates/registry/telemetry.nix create mode 100644 lib/images/windows/templates/registry/updates.nix create mode 100644 lib/images/windows/win10/default.nix create mode 100644 lib/images/windows/win10/images.nix create mode 100644 lib/images/windows/win10/upstream.json create mode 100644 lib/images/windows/win11/default.nix create mode 100644 lib/images/windows/win11/images.nix create mode 100644 lib/images/windows/win11/upstream.json create mode 100644 module.nix create mode 100644 nixos/namespaceSubmoduleOptions.nix delete mode 100644 nixos/network/config.nix delete mode 100644 nixos/network/options.nix create mode 100644 nixos/networks/config.nix rename nixos/{network => networks}/default.nix (69%) create mode 100644 nixos/networks/options.nix delete mode 100644 nixos/vm/config.nix delete mode 100644 nixos/vm/default.nix delete mode 100644 nixos/vm/options.nix create mode 100644 nixos/vms/config.nix create mode 100644 nixos/vms/default.nix create mode 100644 nixos/vms/submoduleOptions.nix diff --git a/flake.nix b/flake.nix index 8ad9dec..efb2bad 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,96 @@ { - description = "builds a qemu image and qemu command scripts to run with systemd services"; + description = "vmix — composable QEMU VM image building and orchestration"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; }; outputs = { self, nixpkgs, ... }: let system = "x86_64-linux"; - pkgs = nixpkgs.legacyPackages.${system}; + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; lib = pkgs.lib; - vmixLib = (import ./lib { inherit pkgs lib system; }); + vmixLib = import ./lib { inherit pkgs lib system; }; in { - packages."${system}" = with vmixLib; rec { - playfuldeb = customizeImage images.debian.v12.play { - name = "playfulness"; - }; + overlays.default = import ./overlay.nix; - nixmox = customizeImage images.debian.v12.proxmox (images.debian.templates.rooted // { - name = "nixmox"; - }); + nixosModules.default = import ./module.nix; - default = playfuldeb; + lib.${system} = vmixLib; + + packages.${system} = { + toDisk = pkgs.writeShellScriptBin "vmix-to-disk" '' + set -euo pipefail + + usage() { + echo "Usage: vmix-to-disk " + echo "" + echo "Write a vmix qcow2 image to a physical disk and expand the Windows partition." + echo "" + echo "Examples:" + echo " vmix-to-disk /nix/store/...-win10-ltsc-vmix.qcow2 /dev/sda" + echo " vmix-to-disk ./my-image.qcow2 /dev/nvme0n1" + exit 1 + } + + [[ ''${#} -ne 2 ]] && usage + + IMAGE="$1" + DISK="$2" + + [[ $EUID -ne 0 ]] && { echo "Error: must run as root"; exit 1; } + [[ ! -f "$IMAGE" ]] && { echo "Error: image not found: $IMAGE"; exit 1; } + [[ ! -b "$DISK" ]] && { echo "Error: not a block device: $DISK"; exit 1; } + + if ${pkgs.util-linux}/bin/mount | grep -q "^''${DISK}"; then + echo "Error: $DISK has mounted partitions — unmount first" + exit 1 + fi + + DISK_SIZE=$(${pkgs.util-linux}/bin/blockdev --getsize64 "$DISK") + DISK_SIZE_GB=$(( DISK_SIZE / 1024 / 1024 / 1024 )) + + echo "=== vmix to physical disk ===" + echo "Image: $IMAGE" + echo "Target: $DISK ($DISK_SIZE_GB GB)" + echo "" + echo "WARNING: This will DESTROY all data on $DISK" + read -rp "Continue? [y/N] " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } + + echo "" + echo "[1/4] Writing image to disk..." + ${pkgs.qemu}/bin/qemu-img convert -p -f qcow2 -O raw "$IMAGE" "$DISK" + + echo "[2/4] Fixing GPT backup header..." + ${pkgs.gptfdisk}/bin/sgdisk -e "$DISK" + + echo "[3/4] Expanding Windows partition (partition 3) to fill disk..." + ${pkgs.parted}/bin/parted -s "$DISK" resizepart 3 100% + + # detect partition device name (nvme/mmcblk use p3, others use 3) + if [[ "$DISK" == *nvme* ]] || [[ "$DISK" == *mmcblk* ]]; then + WIN_PART="''${DISK}p3" + else + WIN_PART="''${DISK}3" + fi + + echo "[4/4] Expanding NTFS filesystem on $WIN_PART..." + ${pkgs.ntfs3g}/bin/ntfsresize --force --no-action "$WIN_PART" + echo "y" | ${pkgs.ntfs3g}/bin/ntfsresize --force "$WIN_PART" + + echo "" + echo "Done. Windows partition expanded to fill $DISK." + echo "You can now boot from the disk." + ''; + }; + + apps.${system}.toDisk = { + type = "app"; + program = "${self.packages.${system}.toDisk}/bin/vmix-to-disk"; }; }; -} \ No newline at end of file +} diff --git a/lib/default.nix b/lib/default.nix index a0082ef..2f1f2ed 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -4,7 +4,6 @@ let network = import ./network.nix { inherit pkgs lib; }; in { - inherit images; - inherit (images.commons) customizeImage customizeImageFold; + inherit (images) linux windows; inherit network; } \ No newline at end of file diff --git a/lib/images/commons/default.nix b/lib/images/commons/default.nix deleted file mode 100644 index 0b3d2f5..0000000 --- a/lib/images/commons/default.nix +++ /dev/null @@ -1,6 +0,0 @@ -{ pkgs, lib, ... }: rec { - # basic scripts and files used across various OS images - scriptsNFiles = (import ./scripts-n-files.nix) { inherit pkgs lib; }; - customizeImage = (import ./customizeImage.nix) { inherit pkgs lib; }; - customizeImageFold = builtins.foldl' customizeImage; -} \ No newline at end of file diff --git a/lib/images/default.nix b/lib/images/default.nix index 1d4cbbc..aecd364 100644 --- a/lib/images/default.nix +++ b/lib/images/default.nix @@ -1,7 +1,5 @@ { pkgs, lib, system, ... }: -let - commons = (import ./commons) { inherit pkgs lib system; }; - debian = (import ./debian) { inherit pkgs lib system commons; }; -in { - inherit commons debian; +{ + linux = (import ./linux) { inherit pkgs lib system; }; + windows = (import ./windows) { inherit pkgs lib system; }; } \ No newline at end of file diff --git a/lib/images/commons/customizeImage.nix b/lib/images/linux/customizeImage.nix similarity index 76% rename from lib/images/commons/customizeImage.nix rename to lib/images/linux/customizeImage.nix index de22cbd..a87b0f2 100644 --- a/lib/images/commons/customizeImage.nix +++ b/lib/images/linux/customizeImage.nix @@ -1,4 +1,4 @@ -# wrapper function around virt-customize to create custom OS image from an original OS image +# wrapper function around virt-customize to create custom Linux image from an original OS image { pkgs, lib, ... }: originalImage: { name ? "", @@ -10,19 +10,16 @@ install ? [], run ? "", commands ? "", - osType ? "linux", - debug ? false + debug ? false, + impure ? true, + # Linux-only: script to run on first boot (via systemd --firstboot) + firstboot ? "" }: let originalImageName = lib.strings.removeSuffix "-vmix" (lib.strings.removeSuffix ".qcow2" originalImage.name); customImageName = (if name != "" then name else "custom") + "-${originalImageName}-vmix.qcow2"; resultImg = "./disk.qcow2"; - qemuWrapperScript = (pkgs.writeShellScript "qemu-wrapper-script" '' - export PATH="${pkgs.qemu}/bin:$PATH" - exec qemu-kvm -nic user,model=virtio-net-pci "$@" - ''); - setHostname = if hostname != "" then hostname else if nameToHostname then name else ""; virtCustomizeArgsHostname = if setHostname != "" then "--hostname '${setHostname}'" else ""; @@ -30,6 +27,9 @@ virtCustomizeArgsCommandsFile = if commands != "" then ("--commands-from-file " + pkgs.writeText "${name}-virt-customize-commands-file" commands) else ""; virtCustomizeArgsRun = if run != "" then ("--run " + pkgs.writeScript "${name}-virt-customize-run-script" "${run}") else ""; + virtCustomizeArgsFirstboot = lib.optionalString (firstboot != "") + ("--firstboot " + pkgs.writeScript "${name}-firstboot" firstboot); + builderCommand = '' export PATH="${pkgs.qemu}/bin:${pkgs.curl}/bin:$PATH" @@ -39,7 +39,6 @@ # run script inside image using virt-customize export LIBGUESTFS_APPEND="ipv6.disable=1" - #export LIBGUESTFS_HV="${qemuWrapperScript}" ${pkgs.guestfs-tools}/bin/virt-customize \ ${lib.optionalString debug "-v"} \ @@ -48,10 +47,13 @@ --memsize ${builtins.toString memSize} \ ${virtCustomizeArgsHostname} \ ${virtCustomizeArgsInstall} \ + ${virtCustomizeArgsFirstboot} \ ${virtCustomizeArgsCommandsFile} \ ${virtCustomizeArgsRun} mv ${resultImg} $out ''; + + builtImage = pkgs.runCommand customImageName (lib.optionalAttrs impure { __noChroot = true; }) builderCommand; in - pkgs.runCommand customImageName { __noChroot = true; } builderCommand + builtImage // { _vmixOsType = "linux"; } diff --git a/lib/images/debian/default.nix b/lib/images/linux/debian/default.nix similarity index 59% rename from lib/images/debian/default.nix rename to lib/images/linux/debian/default.nix index 2a7eeb8..7d22a5d 100644 --- a/lib/images/debian/default.nix +++ b/lib/images/linux/debian/default.nix @@ -1,10 +1,10 @@ -{ pkgs, lib, system, commons, ... }: +{ pkgs, lib, system, linux, ... }: let # upstream distro images upstreamImagesJSON = lib.importJSON ./upstream.json; - upstreamImages = lib.mapAttrs (name: src: pkgs.fetchurl src) upstreamImagesJSON.${system}; - templates = (import ./templates.nix) { inherit pkgs lib system commons; }; - customs = (import ./customs.nix) { inherit pkgs lib system commons upstreamImages templates; }; + upstreamImages = lib.mapAttrs (name: src: (pkgs.fetchurl src) // { _vmixOsType = "linux"; }) upstreamImagesJSON.${system}; + templates = (import ./templates.nix) { inherit pkgs lib system linux; }; + customs = (import ./images.nix) { inherit pkgs lib system linux upstreamImages templates; }; mergeUpstreamAndCustomImages = name: upstreamImage: let @@ -13,4 +13,4 @@ let customImages // { upstream = upstreamImage; }; images = lib.mapAttrs mergeUpstreamAndCustomImages upstreamImages; -in images // { inherit templates; } \ No newline at end of file +in images // { inherit templates; } diff --git a/lib/images/debian/customs.nix b/lib/images/linux/debian/images.nix similarity index 77% rename from lib/images/debian/customs.nix rename to lib/images/linux/debian/images.nix index 1b6932c..d06cd99 100644 --- a/lib/images/debian/customs.nix +++ b/lib/images/linux/debian/images.nix @@ -1,6 +1,6 @@ # create additional useful customized images from templates and upstream images -{ pkgs, lib, system, commons, upstreamImages, templates, ... }: -with commons; +{ pkgs, lib, system, linux, upstreamImages, templates, ... }: +with linux; with scriptsNFiles; let upstreamImageName = "v12"; @@ -20,8 +20,8 @@ in }); # proxmox - proxmox = customizeImage default (templates.proxmoxOnDebian12 // { + proxmox = customizeImage upstreamImages.${upstreamImageName} (templates.proxmoxOnDebian12 // { name = "proxmox"; }); }; -} \ No newline at end of file +} diff --git a/lib/images/debian/templates.nix b/lib/images/linux/debian/templates.nix similarity index 51% rename from lib/images/debian/templates.nix rename to lib/images/linux/debian/templates.nix index 7eaa3f8..1fffeb3 100644 --- a/lib/images/debian/templates.nix +++ b/lib/images/linux/debian/templates.nix @@ -1,15 +1,17 @@ # ready to use customization templates to apply on images -{ pkgs, lib, system, commons, ... }: -with commons; +{ pkgs, lib, system, linux, ... }: +with linux; with scriptsNFiles; { # essential functionalities like ssh, networking etc essentials = { + impure = true; install = [ "htop" "openssh-server" "inetutils-ping" "dnsutils" "cloud-guest-utils" "qemu-guest-agent" ]; commands = '' upload ${grub-ifnames-0}:/etc/default/grub.d/90-ifnames-0.cfg upload ${grub-disable-microcode}:/etc/default/grub.d/00-disable-microcode.cfg - run-command mount /boot/efi && update-grub + run-command mountpoint -q /boot/efi || mount /boot/efi + run-command update-grub upload ${dhcp-network-for-iface { iface = "eth0"; }}:/etc/systemd/network/00-eth0-dhcp.network run ${ssh-service-override-conf-create} upload ${grow-root-sh}:/usr/local/sbin/grow-root.sh @@ -22,6 +24,7 @@ with scriptsNFiles; # set easy root access rooted = { + impure = true; install = [ "openssh-server" ]; commands = '' run ${ssh-service-override-conf-create} @@ -36,17 +39,43 @@ with scriptsNFiles; # install proxmox proxmoxOnDebian12 = { - diskSize = "+2G"; + impure = true; + diskSize = "+3G"; smp = 4; memSize = 4096; install = [ "cloud-guest-utils" ]; - commands = '' + debug = true; + commands = + let + # proxmox makes it very hard to manually add interfaces directly on /etc/network/interfaces while the pve services are not running + # it also doesn't pick up files in interfaces.d + # so manually do that via service after boot + mergeNetIfacesDService = pkgs.writeText "manual-net-ifaces.d.service" '' + [Service] + Type = oneshot + ExecStart = /bin/bash -c "cat /etc/network/interfaces.d/* >> /etc/network/interfaces; rm /etc/network/interfaces.d/*; ifreload -a;" + After = network.target + + [Install] + WantedBy = multi-user.target + ''; + in + '' + upload ${grub-ifnames-0}:/etc/default/grub.d/90-ifnames-0.cfg + upload ${grub-disable-microcode}:/etc/default/grub.d/00-disable-microcode.cfg + + truncate /etc/machine-id + delete /var/lib/dbus/machine-id + upload ${grow-root-sh}:/usr/local/sbin/grow-root.sh upload ${grow-root-service}:/etc/systemd/system/grow-root.service run-command systemctl enable grow-root.service + + upload ${mergeNetIfacesDService}:/etc/systemd/system/manual-net-ifaces.d.service + run-command systemctl enable manual-net-ifaces.d.service ''; run = '' - # script originally taken from https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_12_Bookworm + # script originally taken and modified from https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_12_Bookworm # exit if error set -e @@ -54,7 +83,7 @@ with scriptsNFiles; /usr/local/sbin/grow-root.sh # mount efi for grub changes - mount /boot/efi + mount /boot/efi || true # add proxmox repo echo "deb [arch=amd64] http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-install-repo.list @@ -70,6 +99,29 @@ with scriptsNFiles; # remove previous kernels apt remove -y os-prober linux-image-amd64 'linux-image-6.*'; + + # otherwise grub upgrades make the device unbootable + echo 'grub-efi-amd64 grub2/force_efi_extra_removable boolean true' | debconf-set-selections -v -u + rm -rf /boot/efi/* + grub-install /dev/sda + grub-install --target=x86_64-efi --removable + + # disable subscription warning + # https://dannyda.com/2020/05/17/how-to-remove-you-do-not-have-a-valid-subscription-for-this-server-from-proxmox-virtual-environment-6-1-2-proxmox-ve-6-1-2-pve-6-1-2/ + sed -i -z "s/res === null ||\n\s* res === undefined ||\n\s* \!res ||\n\s* res.data.status.toLowerCase() \!== 'active'/false/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js + + # stop hangs due to network + systemctl disable systemd-networkd-wait-online.service + + # create vmbr0 conf, enable dhcp. this conf will be picked by manual-net-ifaces.d.service + cat >> /etc/network/interfaces.d/vmbr0.conf << EOF + auto vmbr0 + iface vmbr0 inet dhcp + bridge-ports eth0 + bridge-stp off + bridge-fd 0 + + EOF ''; }; } diff --git a/lib/images/debian/upstream.json b/lib/images/linux/debian/upstream.json similarity index 78% rename from lib/images/debian/upstream.json rename to lib/images/linux/debian/upstream.json index fda8398..c1971c0 100644 --- a/lib/images/debian/upstream.json +++ b/lib/images/linux/debian/upstream.json @@ -11,12 +11,12 @@ }, "x86_64-linux": { "v12": { - "sha256": "0inga3c772wr9b296w86n8prlqvw47wd6b5z8347pygiw810y5yq", - "url": "https://cloud.debian.org/images/cloud/bookworm/20240507-1740/debian-12-generic-amd64-20240507-1740.qcow2" + "sha256": "5fvoe45ooVSPwQ3FRn8+ge18sAvSVk+m3iUO71WTM1A=", + "url": "https://cloud.debian.org/images/cloud/bookworm/20250530-2128/debian-12-generic-amd64-20250530-2128.qcow2" }, "v13": { "sha256": "1bixl6gnzigwryac1arc3n81nv4hwdi6wxpwmvrgigzni64b3x6w", - "url": "https://cloud.debian.org/images/cloud/trixie/daily/20240512-1745/debian-13-generic-amd64-daily-20240512-1745.qcow2" + "url": "https://cloud.debian.org/images/cloud/trixie/daily/20250605-2134/debian-13-generic-amd64-daily-20250605-2134.raw" } } } \ No newline at end of file diff --git a/lib/images/linux/default.nix b/lib/images/linux/default.nix new file mode 100644 index 0000000..bc3c68e --- /dev/null +++ b/lib/images/linux/default.nix @@ -0,0 +1,14 @@ +{ pkgs, lib, system, ... }: +let + linux = rec { + # basic scripts and files used across various Linux images + scriptsNFiles = (import ./scripts-n-files.nix) { inherit pkgs lib; }; + customizeImage = (import ./customizeImage.nix) { inherit pkgs lib; }; + customizeImageFold = builtins.foldl' customizeImage; + }; + debian = (import ./debian) { inherit pkgs lib system linux; }; +in linux // { + images = { + debian = debian; + }; +} diff --git a/lib/images/commons/scripts-n-files.nix b/lib/images/linux/scripts-n-files.nix similarity index 84% rename from lib/images/commons/scripts-n-files.nix rename to lib/images/linux/scripts-n-files.nix index f0615c7..bd95343 100644 --- a/lib/images/commons/scripts-n-files.nix +++ b/lib/images/linux/scripts-n-files.nix @@ -69,4 +69,14 @@ [Install] WantedBy = multi-user.target ''; + + add-9p-mounts-to-fstab = shares: + let + shareToFstabEntry = name: share: "${name} ${share.target} 9p trans=virtio,version=9p2000.L,rw,posixacl,msize=104857600,cache=loose 0 0"; + in + pkgs.writeText "9p-to-fstab-sh" '' + cat >> /etc/fstab << EOF + ${lib.concatStringsSep "\n" (lib.mapAttrsToList shareToFstabEntry shares)} + EOF + ''; } \ No newline at end of file diff --git a/lib/images/windows/default.nix b/lib/images/windows/default.nix new file mode 100644 index 0000000..b01dcdd --- /dev/null +++ b/lib/images/windows/default.nix @@ -0,0 +1,32 @@ +{ pkgs, lib, system, ... }: +let + windows = rec { + drivers = import ./drivers { inherit pkgs system; }; + makeFilesISO = (import ./helpers/makeFilesISO.nix) { inherit pkgs; }; + customizeImage = (import ./helpers/customizeImage.nix) { inherit pkgs lib; }; + customizeImageFold = builtins.foldl' customizeImage; + templates = (import ./templates) { inherit pkgs lib system drivers makeFilesISO; }; + makeAuditModeAutounattend = (import ./helpers/makeAuditModeAutounattend.nix) { inherit pkgs lib; }; + makeWinISO = (import ./helpers/makeWinISO.nix) { inherit pkgs; }; + makeImage = (import ./helpers/makeImage.nix) { inherit pkgs lib drivers makeWinISO makeAuditModeAutounattend; }; + }; + + win10 = (import ./win10) { inherit pkgs lib system windows; }; + win11 = (import ./win11) { inherit pkgs lib system windows; }; + + # Recursively add .generalize to every derivation leaf in the image tree + addGeneralize = val: + if val ? _vmixOsType then + val // { generalize = args: + let + templateArgs = builtins.removeAttrs args [ "vncDisplay" ]; + displayArgs = lib.optionalAttrs (args ? vncDisplay) { inherit (args) vncDisplay; }; + in windows.customizeImage val (windows.templates.generalize templateArgs // displayArgs); + } + else if builtins.isAttrs val then + lib.mapAttrs (_: addGeneralize) val + else val; + +in windows // { + images = addGeneralize { inherit win10 win11; }; +} diff --git a/lib/images/windows/drivers/amd-gpu-drivers.nix b/lib/images/windows/drivers/amd-gpu-drivers.nix new file mode 100644 index 0000000..f6f3c54 --- /dev/null +++ b/lib/images/windows/drivers/amd-gpu-drivers.nix @@ -0,0 +1,5 @@ +{ pkgs }: +pkgs.fetchurl { + url = "https://github.com/Matishzz/AMD-Install-Drivers/releases/download/25.3.1/25.3.1.w11-w10.zip"; + sha256 = "0mrz3kkiavs0l4x00978vdbw9la1j16jar0y6lny0l8z59hqp9gq"; +} diff --git a/lib/images/windows/drivers/default.nix b/lib/images/windows/drivers/default.nix new file mode 100644 index 0000000..5ae1ca2 --- /dev/null +++ b/lib/images/windows/drivers/default.nix @@ -0,0 +1,4 @@ +{ pkgs, system, ... }: { + virtio-iso = import ./virtio-iso-w10-11.nix { inherit pkgs; }; + amd-gpu-zip = import ./amd-gpu-drivers.nix { inherit pkgs; }; +} diff --git a/lib/images/windows/drivers/virtio-iso-w10-11.nix b/lib/images/windows/drivers/virtio-iso-w10-11.nix new file mode 100644 index 0000000..5662851 --- /dev/null +++ b/lib/images/windows/drivers/virtio-iso-w10-11.nix @@ -0,0 +1,6 @@ +# VirtIO drivers ISO for Windows guests (stable release from Fedora) +{ pkgs, ... }: +pkgs.fetchurl { + url = " https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.285-1/virtio-win-0.1.285.iso"; + sha256 = "0cb3jyik40mkmyjwbagfj6609cnyzvysf2q7y0jykhwj8jwz4k71"; +} diff --git a/lib/images/windows/helpers/customizeImage.nix b/lib/images/windows/helpers/customizeImage.nix new file mode 100644 index 0000000..ef626f4 --- /dev/null +++ b/lib/images/windows/helpers/customizeImage.nix @@ -0,0 +1,113 @@ +# Customize Windows images via offline registry merge and/or Audit Mode script execution. +# +# Templates can provide: +# windowsRegistry — merged offline via virt-win-reg (fast, no boot needed) +# auditScript — injected into image and run in Audit Mode via QEMU boot +# cdroms — ISO files to attach as CDs when booting for auditScript +# +# Both can be combined: registry is applied first (offline), then the script runs (online). +{ pkgs, lib, ... }: + originalImage: { + name ? "", + diskSize ? "", + impure ? true, + # Offline: merge .reg file into registry via virt-win-reg + windowsRegistry ? "", + # Online: boot into Audit Mode and run this script + auditScript ? "", + # CD-ROMs to attach when booting for auditScript (e.g. VirtIO ISO) + cdroms ? [], + # Files to upload into the image before running auditScript + # List of { source = ; dest = "/Windows/path"; } + uploads ? [], + # QEMU settings for auditScript boot + vncDisplay ? null, + smp ? 4, + memSize ? 4096, + }: + let + originalImageName = lib.strings.removeSuffix "-vmix" (lib.strings.removeSuffix ".qcow2" originalImage.name); + customImageName = (if name != "" then name else "custom") + "-${originalImageName}-vmix.qcow2"; + resultImg = "./disk.qcow2"; + + hasRegistry = windowsRegistry != ""; + hasAuditScript = auditScript != ""; + + # Offline registry merge + windowsRegFile = pkgs.writeText "${name}-registry.reg" windowsRegistry; + virtWinRegMerge = lib.optionalString hasRegistry '' + + echo "=== vmix: merging registry entries (${name}) ===" + virt-win-reg --merge ${resultImg} ${windowsRegFile} + ''; + + # Audit Mode script injection + QEMU boot + auditScriptFile = pkgs.writeText "${name}-audit.cmd" auditScript; + wrapperScript = pkgs.writeText "${name}-audit-wrapper.cmd" '' + @echo off + echo === vmix audit: ${name} === + call C:\vmix-audit-script.cmd + echo === vmix audit: ${name} complete === + del /q C:\vmix-audit-script.cmd 2>nul + shutdown /s /t 5 /c "vmix: ${name} complete" 2>nul + del /q C:\vmix-audit-wrapper.cmd 2>nul + ''; + runOnceReg = pkgs.writeText "${name}-runonce.reg" (lib.concatStringsSep "\n" [ + "Windows Registry Editor Version 5.00" + "" + ''[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce]'' + ''"vmixAudit"="C:\\vmix-audit-wrapper.cmd"'' + "" + ]); + + cdromArgs = lib.concatMapStringsSep " \\\n " (cd: "-drive file=${cd},media=cdrom,readonly=on") cdroms; + + auditBootCommands = lib.optionalString hasAuditScript '' + + echo "=== vmix: injecting audit script (${name}) ===" + virt-customize -a ${resultImg} \ + --upload ${auditScriptFile}:/vmix-audit-script.cmd \ + --upload ${wrapperScript}:/vmix-audit-wrapper.cmd \ + ${lib.concatMapStringsSep " \\\n " (u: let dir = builtins.dirOf u.dest; in "${lib.optionalString (dir != "/") "--mkdir ${dir}"} --upload ${u.source}:${u.dest}") uploads} + + echo "=== vmix: adding RunOnce entry ===" + virt-win-reg --merge ${resultImg} ${runOnceReg} + + # Boot into Audit Mode to run the script + cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd vars.fd + chmod +w vars.fd + + echo "=== vmix: booting Audit Mode for ${name} (VNC: ${if vncDisplay != null then vncDisplay else "disabled"}) ===" + timeout 1800 qemu-system-x86_64 \ + ${if vncDisplay != null then "-vnc ${vncDisplay}" else "-nographic"} \ + -accel kvm \ + -m ${toString memSize} \ + -smp ${toString smp} \ + -cpu host \ + -machine type=q35 \ + -drive if=pflash,format=raw,readonly=on,file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd \ + -drive if=pflash,format=raw,file=vars.fd \ + -rtc base=localtime,clock=host \ + -device qemu-xhci -device usb-tablet \ + -global ICH9-LMB.disable_s3=1 -global ICH9-LMB.disable_s4=1 \ + -drive file=${resultImg},format=qcow2,if=virtio \ + ${cdromArgs} \ + -nic user,model=virtio-net-pci + + echo "=== vmix: audit script ${name} complete ===" + ''; + + builderCommand = '' + # create resulting image backed by original image + qemu-img create -f qcow2 -b ${originalImage} -F qcow2 ${resultImg} + [ -n "${diskSize}" ] && qemu-img resize ${resultImg} ${diskSize} + ${virtWinRegMerge} + ${auditBootCommands} + mv ${resultImg} $out + ''; + builtImage = pkgs.runCommand customImageName ({ + nativeBuildInputs = with pkgs; [ qemu perl guestfs-tools ]; + requiredSystemFeatures = [ "kvm" ]; + } // lib.optionalAttrs impure { __noChroot = true; }) builderCommand; + in + builtImage // { _vmixOsType = "windows"; } diff --git a/lib/images/windows/helpers/makeAuditModeAutounattend.nix b/lib/images/windows/helpers/makeAuditModeAutounattend.nix new file mode 100644 index 0000000..f423405 --- /dev/null +++ b/lib/images/windows/helpers/makeAuditModeAutounattend.nix @@ -0,0 +1,174 @@ +# Autounattend.xml for unattended Windows installation into Audit Mode. +# Minimal — only what's needed to install Windows and enter Audit Mode. +# All customization (hostname, timezone, telemetry, UAC, etc.) is done via templates. +{ pkgs, lib, ... }: +{ + locale ? "en-US", + productKey ? "", + diskIndex ? 0, + imageIndex ? 1, + efi ? true, + virtioDriverLetter ? "E", + bypassRequirements ? false, + windowsVersionForVirtioDrivers +}: pkgs.writeText "Autounattend.xml" '' + + + + + + + + ${locale} + + ${locale} + ${locale} + ${locale} + ${locale} + + + + + + ${virtioDriverLetter}:\amd64\${windowsVersionForVirtioDrivers} + + + + + + + ${lib.optionalString (productKey != "") '' + + ${productKey} + + ''} + true + + + + + + + /IMAGE/INDEX + ${toString imageIndex} + + + + ${toString diskIndex} + ${if efi then "3" else "1"} + + + + + + + ${toString diskIndex} + true + ${if efi then '' + + + 1 + 100 + EFI + + + 2 + 16 + MSR + + + 3 + true + Primary + + + + + 1 + 1 + FAT32 + + + + 2 + 2 + + + 3 + 3 + NTFS + + + + '' else '' + + + 1 + true + Primary + + + + + 1 + 1 + NTFS + + true + + + ''} + + + + ${lib.optionalString bypassRequirements '' + + + 1 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f + + + 2 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f + + + 3 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f + + + ''} + + + + + + + + + 1 + reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v vmixCleanup /t REG_SZ /d "cmd /c del /q C:\Windows\Panther\unattend.xml" /f + + + 2 + reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v vmixShutdown /t REG_SZ /d "shutdown /s /t 10 /c vmix-audit-ready" /f + + + + + + + + + + Audit + + + + +'' diff --git a/lib/images/windows/helpers/makeFilesISO.nix b/lib/images/windows/helpers/makeFilesISO.nix new file mode 100644 index 0000000..9b2d799 --- /dev/null +++ b/lib/images/windows/helpers/makeFilesISO.nix @@ -0,0 +1,23 @@ +# Create an ISO from a list of files. Zips are automatically extracted. +# Nix store hash prefixes are stripped from filenames. +# Usage: +# makeFilesISO { name = "my-stuff"; files = [ ./foo.exe someZip anotherFile ]; } +# makeFilesISO { name = "my-stuff"; files = [ (pkgs.fetchurl { ... }) ]; } +{ pkgs, ... }: +{ name ? "files", files }: +pkgs.runCommand "${name}.iso" { + nativeBuildInputs = with pkgs; [ unzip cdrkit file ]; +} '' + mkdir -p content + ${builtins.concatStringsSep "\n" (map (f: '' + if file --mime-type -b "${f}" | grep -q "application/zip"; then + unzip -q -o "${f}" -d content + else + # Strip nix store hash prefix (e.g. "abc123-foo.exe" -> "foo.exe") + fname=$(basename "${f}") + stripped="''${fname#*-}" + cp "${f}" "content/$stripped" + fi + '') files)} + genisoimage -o $out -joliet -joliet-long -rational-rock content +'' diff --git a/lib/images/windows/helpers/makeImage.nix b/lib/images/windows/helpers/makeImage.nix new file mode 100644 index 0000000..fa1014f --- /dev/null +++ b/lib/images/windows/helpers/makeImage.nix @@ -0,0 +1,69 @@ +# Build a pre-installed Windows qcow2 image using QEMU unattended install. +# Boots into Audit Mode after install and shuts down. +# Apply templates via customizeImageFold to install software (auditScript) +# and customize registry (windowsRegistry), then generalize when done. +{ pkgs, lib, drivers, makeWinISO, makeAuditModeAutounattend, ... }: +{ + name ? "windows", + upstreamISO, + productKey ? "", + imageIndex ? 1, + diskSize ? "64G", + bypassRequirements ? false, + locale ? "en-US", + efi ? true, + smp ? 4, + memSize ? 4096, + vncDisplay ? null, # e.g. ":10" to enable VNC on port 5910 for monitoring + windowsVersionForVirtioDrivers ? "w10", # "w10", "w11", "2k22", "2k19", etc. +}: +let + AutounattendedXml = makeAuditModeAutounattend { + inherit locale productKey imageIndex + efi bypassRequirements windowsVersionForVirtioDrivers; + diskIndex = 0; + virtioDriverLetter = "E"; + }; + + iso = makeWinISO { + iso = upstreamISO; + inherit AutounattendedXml; + }; + + drv = pkgs.runCommand "${name}-vmix.qcow2" { + __noChroot = true; + requiredSystemFeatures = [ "kvm" ]; + nativeBuildInputs = with pkgs; [ qemu ]; + } '' + # create empty disk + qemu-img create -f qcow2 disk.qcow2 ${diskSize} + + # writable UEFI NVRAM so boot order persists across reboots + cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd vars.fd + chmod +w vars.fd + + echo "=== Starting Windows unattended install (this takes 15-30 minutes) ===" + + # Windows installs unattended, reboots into Audit Mode, + # deletes cached Autounattend, shuts down → QEMU exits. + timeout 3600 qemu-system-x86_64 \ + ${if vncDisplay != null then "-vnc ${vncDisplay}" else "-nographic"} \ + -accel kvm \ + -m ${toString memSize} \ + -smp ${toString smp} \ + -cpu host \ + -machine type=q35 \ + -drive if=pflash,format=raw,readonly=on,file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd \ + -drive if=pflash,format=raw,file=vars.fd \ + -rtc base=localtime,clock=host \ + -device qemu-xhci -device usb-tablet \ + -global ICH9-LMB.disable_s3=1 -global ICH9-LMB.disable_s4=1 \ + -drive file=disk.qcow2,format=qcow2,if=virtio \ + -drive file=${iso},media=cdrom,readonly=on \ + -drive file=${drivers.virtio-iso},media=cdrom,readonly=on \ + -nic user,model=virtio-net-pci + + echo "=== Windows install complete (Audit Mode image) ===" + mv disk.qcow2 $out + ''; +in drv // { _vmixOsType = "windows"; } diff --git a/lib/images/windows/helpers/makeWinISO.nix b/lib/images/windows/helpers/makeWinISO.nix new file mode 100644 index 0000000..56c1331 --- /dev/null +++ b/lib/images/windows/helpers/makeWinISO.nix @@ -0,0 +1,40 @@ +# Remaster a Windows ISO: remove boot prompt, optionally inject Autounattend.xml and scripts +{ pkgs, ... }: +{ iso, AutounattendedXml ? null, postInstallScript ? null }: + pkgs.runCommand "windows-remastered.iso" { + nativeBuildInputs = with pkgs; [ p7zip cdrkit ]; + } '' + workdir=$(mktemp -d) + isopath="${iso}" + if [ -d "$isopath" ]; then + isopath="$(find "$isopath" -maxdepth 1 -name '*.iso' -type f | head -n1)" + if [ -z "$isopath" ]; then + echo "ERROR: no .iso file found in $isopath" + exit 1 + fi + fi + 7z x -o"$workdir" "$isopath" > /dev/null + + # remove "Press any key to boot from CD/DVD" prompt + if [ -f "$workdir/efi/microsoft/boot/efisys_noprompt.bin" ]; then + cp "$workdir/efi/microsoft/boot/efisys_noprompt.bin" \ + "$workdir/efi/microsoft/boot/efisys.bin" + fi + if [ -f "$workdir/efi/microsoft/boot/cdboot_noprompt.efi" ]; then + cp "$workdir/efi/microsoft/boot/cdboot_noprompt.efi" \ + "$workdir/efi/microsoft/boot/cdboot.efi" + fi + rm -f "$workdir/boot/bootfix.bin" + + # inject unattend and post-install script into ISO root + ${if AutounattendedXml != null then ''cp ${AutounattendedXml} "$workdir/Autounattend.xml"'' else ""} + ${if postInstallScript != null then ''cp ${postInstallScript} "$workdir/post-install.cmd"'' else ""} + + genisoimage \ + -allow-limited-size -iso-level 3 -o "$out" \ + -joliet -joliet-long -rational-rock -udf \ + -b boot/etfsboot.com -no-emul-boot -boot-load-size 8 -boot-info-table \ + -eltorito-alt-boot -e efi/microsoft/boot/efisys.bin -no-emul-boot \ + "$workdir" + rm -rf "$workdir" + '' diff --git a/lib/images/windows/templates/apps/7zip-file-associations.reg b/lib/images/windows/templates/apps/7zip-file-associations.reg new file mode 100644 index 0000000000000000000000000000000000000000..1b413ec45740361d19194b4ffe0286f8440d1941 GIT binary patch literal 36166 zcmezWFPtHhA&()2A)ld~p_oB|A&4QBA)O(Up_rkBp@^Z9L4m=QA%!86p@boyp@>0& zA&eoFp@^ZFA(J7WA&)_U!IVLd!GOVlftP^`jH4Mm7`z!=86p|t8GIQ08Jrm$8GIPx z8GIQW8JrnB7(5yL7+e`*7=ju68Qd5`7{VDG8G^tf&I~yWi44UI#SEzo#SF1v(?F(D z%fuL{OAVoBJ22QXC^48b=rTkxWHJ=MWtC`RvpGW*PJ2Oew6Qgjp@1O`9-5dwhRM^) z_9BKXoW4$E0LjwE<|KwH1|ys{Lu6@ZGblCTw;iH_HZDkJNMuOD>34`Mt!yp;mkW@* zi<#Pz86fhsu|0($6_39mvb3@}mmwWzh^H`sWNBkFsH9B6X){EYHa4d*Br=rXv>75x z8=KSdl%yaz+Sv-qwYbtY$Yzi{ZEVlLTZ%(uX=8H^Lk2@4&Ul8%(#qy4h73G5gJfxE zb1q(AS22L(X=8gHLkZs4hRM^$_96yQy^JfBLu6@Xa{)sx&iF230LjwE=3<6IhEj0b z6Oxl~`5s*ztz1)%Cmf0yK(e&4xddys=#9xxY89wmNp>_NwK(X28|%m!e-178)P?t z#-a=v6c`*CiWrg^G8i%$${12BZk3+A(a8t_bFw_VJKnnWJm^&k|Dd#nZb%7 zh9QO_fT4&XpCO$A)S}8|P+)Ll$YjU?k9>h-5oSYpRSbFzDGWIbISe`smUQz?F+&DJ zD%e(JAJ8XGV;B&&$1vnG6fmSR*w42EEu);J+4bfNA1CJIOF$`30f`uibb{v@8gwGwrH#dQ1|1fhDM06l>6~yM@l(S$y zLZokc)>WWXjp>4c%VGH3GCXq`WbF!aoI^zDox32m(kEw`)4T-;^Uc8IDooo4CP(3O z$8gO}kkweI`3WLM{~QIemp*yQkb(9sLD*Wifocn4vwxs+7A$TNB@DtP!!vI|RvRMc zEQkob^A*HadgUrZn%7k@{~%Hlav0Nkv;xz1L^>WU?ijB53ABb5IgBBzYU!V!K<=Pd zexiNb4-$$4RYzgkK2Z4y603+(1>uh2nV%pl!;$k7M13eJBDX| z0h-A9m6v} zLG}?N*HjP@dgmvIt@O!HW;D-DFy9PJZo;&EU~&^acMR9u1lk*qnwvl(^v_KoTZd0> zg7{{jaud4k1C^U_yJPs~CeUdQ==ljGHV`=qWG{X4lpzB>>L|!r8Ur;4mBWB(|G?!e zNZcYy7=%lPXWoLHWrCcyU}E&nTQGa+lediMum%a?BSeZq4ryA?M`5!ck)j8WONM9O z0_`j>LfJ755uta!g4jx*TxCr2J{8P21CygLZ6BEYgwGwrH8+9IpF%FPASY7MKR1Eg zL7&`Y$Uyr&@{mv*sMa5*?E{scAhC)lRS@nNp7{y=EH&hug)T?$Jce#ReR7)-9cF`~3j^UY~K<8DW_Pih>^v+KZTj`abXy5mO`3I4bkjpGu*IAghBhv9;amVn@ zPpBs=g5siz?kB%MY^6_rqWxNRm~RFyH&vmWlreC*370#DYiG4dYHBkRBnRADWXh4xMMiyCe(9ZAhBUI0QD0n^g-tj(c>IidgUpQ ze-P;jx!j_Ciw_o_2>TJ~dGNSoxaKU-ihblzE@1$P(7&Dn*-D>!iXJ;bAfY&LH5DW@ z2R=vPcE@ndPoSF>Q1cT=g#P&nWb1IsPZ0kgQZ#ZH)4q;Ew|$^$DBSKCuK5Xcj|6gD zmoR`t=%1fJw$dj*88Ogfln>&cfyz(lwhvT(!tIXXnx8Ol6e(eVh|xb!LF}bhuA==q zTUbaAT)PhzqKJGwP&o^qONM9Og52?hTvtIv=$)@1w$dk8(PM@e<{w0wMh;_IxBW0} zADA44&mF@vKS6FTLyl{R2)*+Y#8!IcC)(dU1@q6q|ptdjl z^AcztkUk^lwC_cMLU5qkdgyi!R4xL=C!!=lxMFzbA=Dg1|J4zo(56T3q5aATkWUcl z2Dz4}b&C$wW<-h|3~m^%xdwE04RZKG?o6e3?E|ruUM)G=r(;m44pjXE3rj>E9;lpz z9tsF|4A0& zA&eoFp@^ZFA(J7WA&)_U!IVLd!GOVlftP^`jH4Mm7`z!=86p|t8GIQ08Jrm$8GIPx z8GIQW8JrnB7(5yL7+e`*7=ju68Qd5`7{VDG8G^tf&I~yWi44UI#SEzo#SAeFnGCrM zi45sr_rPRg!FD(>*fJ@H^B9nQ7{j2%;LKpf5W^4y^-VfM z5kn$FE`tJt8$%{T4m6}XpIjL#7)ls);k6&A-j9KX zfF468xSSl=Gz$xf!BRS4x(AVmAoa*lEgkUrY_xQMl@5*!0StZ&?gLytz+8b`mP1k> zb=y?f>M%V9Y%K|FE*dU51Ha!!^9CYs1Pyp=64aaZWEiNL6Jq<|s5v2S8B8SvD0Lva zXZVy4xP3NSIv|%0;R9Z81~G&WXz38nFnCG_P)H1xb~UK9LUqq@Djjh7Y_xPhln!AG zo(ygSUOqrvF>vh%i2Z}5jDWZXxy~NwUIwO%hEF+x&u^n81+1iSVlZPc8Q^jP;)a1L zB_OsBjxqw`mcdg(V7h0xln?lPHd;C$N(V=V$N?`MKyDbg(g9@qU@09yZW&Ca1G;;L zPw9Z$XQQP9qIB?O2pI6v0px~(D;+?#50=sa>N~eKuM;z)AYVlW!uwu3VR$PEKmI)H2+ETsd; zErY3aKzGmZDIIY8Y_xPhE**>qLFr&TV6&T`vdMUm%x*&5GPp_yP|P4^TZdEWfXip2 zr2}&5FwkosKzRZ*4^4RA3oX_ZLu?;BZ3j?DAadit&T2x!2D!8vPNf5CNF#hUS~?&~ z2hd)+flh-UHw;|Q0%ZGODIGv=LC%eXBW4h#6(SEoTsBm995{pbkRpdP!e^tU19IsQ zGT=RnP==5J>{)*Jy(yW)P*-a48)yLmJ_;(b56AbO;{cwgV`pf(NYa z0I_}Wln$VfK;*`OFC9?bgUCY=mkrgn10>dw%VC7iMoR}o>EOaJ@TCLD4FlJ90NFlR zN(Yczkkc$A4-Q0Zl8^o4v5lW;7>tu0iVP&aHRt%1O`j#0CLMGki)1+&&vE9T25M z(17pdc3}XyVc<#!knMw|bO5EJuyZ3kBdkQ)ZBbO6~tSV{+wTLx3=m8EXertK#h#M4rtE;V*6kz9YC=$m`Vq9_Y9xX0k_XaO9w>h5HSc&k#l8$ zxMJYi4-oqYOBn%i&0s1eFkLi!$_ada8!ai2ONxQM52zuH@Y!hT04p8b7(j4fdl?`XxD8n80I_{=lnxNLAm_%x5i^L=3Xz8( zE*q*VqcB4n;j_`w0Z}@5GK38H%1Ji{h${wDzQA-5BK-{YoPp18qj>|7H+%*`58RCb z;)=nPH!xi^eDVf9zm4V%Sl)0S1UUoZhJhNHg4jMdT9pvD44x7K(>=qbe8A_k(b566 zbO;;Z759+13S$^-`2y8N!z*Xt^4n1T?4+fV3Ur!2g!{EsinC=-ac>S@b?mWPKO%Dc$D+a1f4YPl+lo7DdK;%lu zoXJ3~bA^Qua=C@bO%S&Y)tOHGej6<*P)mw|zLysgTLZTm0%reUDk(srF}O+!R2L1e zk^-0CMoS7rNip#EcYA9 z1J%Zc*giPwO^92NbK}644w&v4E~NuLpN*Cdh|Eu545H@;Zr)`_StCZ04p857(gq02fF7CbHzZF5HR}(M;QTg&EP2|u(@csloR;2pfP@ZrdupN95+XqX%8NdJviNP{n z4GA0M(rP%B4yYlG@Y!hTfLc1Z4~p`^eSk{{_d#-JGbB%fLSt~15~wah625ez;9 z-iiRZVc^E+K(-H-UIxf5$gO%v9vrCIe=i1f_Y9xX0k_XaO9$lAVW8LGgVG@CO7DTs z6R7SPPI&^C&qnhEEKhhd7%`X*aE%FZ!$8%T5ZebwjR|qf;Hfb&-7{QD2YfyoEgcZ0 z1L)?~fvusu89;6rxY7Y+`(P;@KyDdKr31QqhEM5$+h?Pt1FUrL1?S9xEgc|k7^u<# zV*B7I9UyKQJf#DsdxlHtfX`>6r30dLa2@b_gM1l4ZWy@I0c87NDIGv=8BC=Ex_gFC z>44j3qoo6)bO6m`xiSp&T%Ioj%pHR%Z(wuN@W~(eeK(p%5P8IZz)!XIWdONh;A&8i z?SrKT1-WG~l@jRg89pThZl8^o4v5krh#`D{NA7(YKyDbg(g9@qU@09yZW&Ca1G;;L zPw9Z$XQQP9taR`L_k0I-Y!2dvfhrv!whxZd0pgayQ#xR}XSkFO_44j3 zqoo6^bO;y(Z3l=O2C8&`*giN)2Z&n+Pw9Z^p5anD;PcsN>3}F5oEahpxMvZ-0CK~? zl@1`=2TSPya?4;U9njq~d`bu0J{v6^5TyfX<W33}67cVc^;hAlnB^=>T%eU@9HZ-7|bj2i!gzEgcZ0 zgWrI!B@SQ!xnbZ+2axT9rE~zfWiXWv=iZh7`V~_Wcy$# z9YAgwOr-<5dxlTxfZJ!Ir30dL2xf2^@KH&S8wOLJKzGmZ$rHGJHkv1p^MuPF$P+FD zb{cO0LokEOAUTT{;+DZxI)GvZaqj1EDjjh7Y_xPhlnx<-U>*U~2E6S6a>Kxt4j|hHOX&b|%U~)U(A_h9N(bCN8!a7R zr9&WtKZEB0mktm&3{>d=v3+oq4iL8tp3(u+J;SARz~{5k(g9XF1Th#h4D`4I#0>*g zIzVh69Hj%qErX|Yz;w@WDIM_nY_xPhln#yqex_g$1IP^nS2}=fA1tK<$Ss4ZbU=5{ z@F^W|`)ssyKrS5y`a~p9=@33(a~2TW2T$n$3JJsy-Egg_c zhk?F15EN5B16n%x43hN@u#iBcSx6onsG9>Z-Gj(O5SI<*(gBp_5Fw55*=Xs2TslMz z@SFuG4Mq)U=@2!DN(WF#Akr)(4-RDMfa)GZ9)h@RD3=bnd^TD-z)FW;hM)oOSwP${ zP@|F%+XqL_0^*jz)3d;I&u}Rn@cC@CbU-d0+y=Pq0E(%BJJkSU``{@ZKp`<$+76Jg zK`yO^Q|W*j(g>f8mJZ0J!$8kjfYKmn{CHr_Jb~CgcuEIQNDQXZ0o6T-JT$a>7LZuS zUha*S4v5krY`{-72xb7eVc>ceAlnB^=>T#Ia+-zY!GRigfW-`=v>HC81A0g!d^TD- zz)FV@26qO>0q$8q+%Qn31H|^hQ93}}GI&Y{O!px25Tx81s-u$ld^TD-AW8?%0beT_ z!T@r^z?BXl+XqYO0CLMGki)1+&&vE9Z*Y$fgYEHlnT%eU@9HZ-Gj(OkTf$? z``+OUxP3NSI>1VY2nOQ;Z#zKTFi@of#P-2aIzZeqcuEIM_Y9ZP0iVxCO9w>h;56X( zeMEq7LLRu%0Tcp*rE~zfWiXWv=45H@;Zr)`_StCZfG8cp26#pFr+f%F%&aoGUPMlF(@!3G88iuGvqTQGh{L(GL%5Yi{bjY z7`Pas89W%g8C)468R8jy82lNW85|jW7~&ax85|j$89W#~8T=Sr8DbcM8T=XC7(y7r z85|jcz#`5JIZzu?8HyQV81xv78PXVv7-AW?7#tXE8I%}28FCpC8PXY28QkG&^%xxC zu7bH9q|=Qdh(U>g+Ad0DC}7BANGHopjtr1nJZa&oB8E&d-4w*&Nt19YVkjruMd37Y zQ5i!fLmHWGf~;Dhe#|8?m@$};0SwNwUjW6hOju~tEL%hr`40=x#F7U5JuX59vI6D|5~X$&ApPEF#* z0D{yHqcnz0h7vMMZZ`%`h7g*R-DwOtWTz)L1|J%wr*yL2tnLu1ci&jbo*(CvJ^pO@W%4A4p$R{iAJQg=D(OlOd1>F3Ms6 z)pq2iB`*e0ZAXik%VH=XbKJy>A%MY%CgD`TkV;m}fn4NDGZ&FJ<_&R^JB?gLzME*{ zq6&r_GGi`+!G|VcL~a}0iy?w0ZGmhCBL-8l!pIw}llpCeYzEMH4mrI7ZwAnK4)tA> zOSX%AY38C-GJD~^46Za8smNsjmE5Uh&cyjL(5_s{WymM9N9W7nPm}&!E(5va-o6Z= zn*pew!t)r&O;4b?V_LKW^2iP&KN^KmJ_Bgpi=1-FAG|u0`mvTz_Q;n%jYhr-$PS|b z8ii2-Lo!1JS@l~0gEK<}4cY*p)+BkY?f@E<-31Kk47p^6kvoGgO~Q!WxdB?O=qO+) zAUljeEAgnG`wJL~$(+#%U7&IZDXMUDS`ga)wlfBr@ksVP`VY!d2w1$AO%=LJJpx`jh00 ylR!>GpuUSL!21@+aS>z(HZ5F4?pP9Jr!g&DL~j2Uvb&KME+TIw2WUqX$Ta}zQl=~b literal 0 HcmV?d00001 diff --git a/lib/images/windows/templates/apps/imageglass.nix b/lib/images/windows/templates/apps/imageglass.nix new file mode 100644 index 0000000..36b595a --- /dev/null +++ b/lib/images/windows/templates/apps/imageglass.nix @@ -0,0 +1,26 @@ +# Install ImageGlass and set file associations +{ pkgs, makeFilesISO, ... }: +let + installer = pkgs.fetchurl { + url = "https://github.com/d2phap/ImageGlass/releases/download/9.4.1.15/ImageGlass_9.4.1.15_x64.msi"; + hash = "sha256-o7q+5XpGLK+P/GQxvN/Ud5w1NxPpJ24AL54HNwda32Q="; + }; + assocReg = ./imageglass-file-associations.reg; +in { + name = "imageglass"; + cdroms = [ (makeFilesISO { name = "imageglass"; files = [ installer assocReg ]; }) ]; + auditScript = '' + @echo off + echo Installing ImageGlass... + msiexec /i "D:\ImageGlass_9.4.1.15_x64.msi" /qn /norestart ALLUSERS=1 + + echo Importing ImageGlass file associations... + regedit /s "D:\imageglass-file-associations.reg" + + :: delete shortcut on desktop + del /q "C:\Users\Public\Desktop\ImageGlass.lnk" + + :: Remove Paint Brush class registration + reg delete "HKLM\SOFTWARE\Classes\PBrush" /f 2>nul + ''; +} diff --git a/lib/images/windows/templates/apps/office.nix b/lib/images/windows/templates/apps/office.nix new file mode 100644 index 0000000..12910be --- /dev/null +++ b/lib/images/windows/templates/apps/office.nix @@ -0,0 +1,29 @@ +# Install Microsoft Office 2024 (Word, Excel, PowerPoint) via offline deployment +{ pkgs, makeFilesISO, ... }: +let + officeSrc = pkgs.fetchurl { + url = "https://git.sagar.ch/dotfiles/office-2024-word-excel-powerpoint/archive/4351e8d93ceb28f65de9ec4dc9c27dccbe91ff34.zip"; + hash = "sha256-ulNecpkUH6O9cqYhgtmG++a1gi+c4HIR1Vvo22VygJs="; + }; +in { + name = "office"; + cdroms = [ (makeFilesISO { name = "office"; files = [ officeSrc ]; }) ]; + auditScript = '' + @echo off + echo Installing Microsoft Office 2024... + call D:\office-2024-word-excel-powerpoint\install.bat + + :: Office privacy policies (HKCU, preserved to new users via CopyProfile) + reg add "HKCU\Software\Policies\Microsoft\office\16.0\common\privacy" /v "disconnectedstate" /t REG_DWORD /d 2 /f >nul + reg add "HKCU\Software\Policies\Microsoft\office\16.0\common\privacy" /v "usercontentdisabled" /t REG_DWORD /d 2 /f >nul + reg add "HKCU\Software\Policies\Microsoft\office\16.0\common\privacy" /v "downloadcontentdisabled" /t REG_DWORD /d 2 /f >nul + reg add "HKCU\Software\Policies\Microsoft\office\16.0\common\privacy" /v "controllerconnectedservicesenabled" /t REG_DWORD /d 2 /f >nul + reg add "HKCU\Software\Policies\Microsoft\office\common\clienttelemetry" /v "sendtelemetry" /t REG_DWORD /d 3 /f >nul + + :: incase of enabling connected experiences, this disables the dialog box that shows at first run + :: reg add "HKCU\SOFTWARE\Microsoft\Office\16.0\Common\Privacy\SettingsStore\Anonymous" /v "OptionalConnectedExperiencesNoticeVersion" /t REG_DWORD /d 2 /f >nul + + :: supress dialogbox - Choose your theme for 365 Apps + reg add "HKCU\SOFTWARE\Microsoft\Office\16.0\Common\PromoDialog" /v "FluentWelcomeDialogShown" /t REG_DWORD /d 1 /f >nul + ''; +} diff --git a/lib/images/windows/templates/apps/sandboxie.nix b/lib/images/windows/templates/apps/sandboxie.nix new file mode 100644 index 0000000..69e82eb --- /dev/null +++ b/lib/images/windows/templates/apps/sandboxie.nix @@ -0,0 +1,16 @@ +# Install Sandboxie-Plus +{ pkgs, makeFilesISO, ... }: +let + installer = pkgs.fetchurl { + url = "https://github.com/sandboxie-plus/Sandboxie/releases/download/v1.15.6/Sandboxie-Plus-x64-v1.15.6.exe"; + hash = "sha256-0VQXy9rFCJCfVyHLJYXe/s6imcwEZugsNOKMhUoLUVM="; + }; +in { + name = "sandboxie"; + cdroms = [ (makeFilesISO { name = "sandboxie"; files = [ installer ]; }) ]; + auditScript = '' + @echo off + echo Installing Sandboxie-Plus... + start /wait D:\Sandboxie-Plus-x64-v1.15.6.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /MERGETASKS="!desktopicon" + ''; +} diff --git a/lib/images/windows/templates/apps/thorium.nix b/lib/images/windows/templates/apps/thorium.nix new file mode 100644 index 0000000..f2f2a09 --- /dev/null +++ b/lib/images/windows/templates/apps/thorium.nix @@ -0,0 +1,40 @@ +# Install Thorium browser and set as default via Active Setup (all users) +# PS-SFTA is stored in Program Files and runs at each user's first logon. +# post-oobe.cmd also runs SFTA for the initial user. +{ pkgs, makeFilesISO, ... }: +let + installer = pkgs.fetchurl { + url = "https://github.com/Alex313031/Thorium-Win/releases/download/M138.0.7204.303/thorium_AVX2_mini_installer.exe"; + hash = "sha256-43daDPX4N7NbVx1UfmO6KDMKKufhmpeQMO7qTeRggqo="; + }; + sfta = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/DanysysTeam/PS-SFTA/master/SFTA.ps1"; + hash = "sha256-Prb23uP9jJFgQEIGC51ljwjshdP9ChR2kRnf14vDCFE="; + }; +in { + name = "thorium"; + cdroms = [ (makeFilesISO { name = "thorium"; files = [ installer sfta ]; }) ]; + auditScript = '' + @echo off + echo Installing Thorium browser... + copy D:\thorium_AVX2_mini_installer.exe C:\thorium_installer.exe + start /wait C:\thorium_installer.exe --silent --system-level --do-not-launch-chrome + del /q C:\thorium_installer.exe + echo Waiting for setup.exe to finish... + :wait_loop + tasklist /fi "imagename eq setup.exe" 2>nul | find /i "setup.exe" >nul + if not errorlevel 1 ( + timeout /t 5 /nobreak >nul + goto wait_loop + ) + + echo Setting up Thorium as default browser for all users... + :: Store SFTA in Program Files (survives sysprep as installed program) + mkdir "C:\Program Files\vmix" 2>nul + copy D:\SFTA.ps1 "C:\Program Files\vmix\SFTA.ps1" >nul + :: Register Active Setup: runs at first logon for every new user + reg add "HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\vmix-default-browser" /v "(Default)" /t REG_SZ /d "Set default browser" /f >nul + reg add "HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\vmix-default-browser" /v "StubPath" /t REG_SZ /d "powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command \". 'C:\\Program Files\\vmix\\SFTA.ps1'; Set-FTA 'ThoriumHTM' '.htm'; Set-FTA 'ThoriumHTM' '.html'; Set-PTA 'ThoriumHTM' 'http'; Set-PTA 'ThoriumHTM' 'https'\"" /f >nul + reg add "HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\vmix-default-browser" /v "Version" /t REG_SZ /d "1,0,0,0" /f >nul + ''; +} diff --git a/lib/images/windows/templates/apps/vlc.nix b/lib/images/windows/templates/apps/vlc.nix new file mode 100644 index 0000000..e5be15c --- /dev/null +++ b/lib/images/windows/templates/apps/vlc.nix @@ -0,0 +1,19 @@ +# Install VLC and register shell handlers for video/audio formats +{ pkgs, makeFilesISO, ... }: +let + installer = pkgs.fetchurl { + url = "https://get.videolan.org/vlc/3.0.21/win64/vlc-3.0.21-win64.exe"; + hash = "sha256-l0JomlDpbdwE2Azv8EayjaK+79YXvhgWb4xecV7GDFk="; + }; +in { + name = "vlc"; + cdroms = [ (makeFilesISO { name = "vlc"; files = [ installer ]; }) ]; + auditScript = '' + @echo off + echo Installing VLC... + start /wait D:\vlc-3.0.21-win64.exe /S /L=1033 + + :: delete shortcut on desktop + del /q "C:\Users\Public\Desktop\VLC media player.lnk" + ''; +} diff --git a/lib/images/windows/templates/default-apps.nix b/lib/images/windows/templates/default-apps.nix new file mode 100644 index 0000000..75e9baf --- /dev/null +++ b/lib/images/windows/templates/default-apps.nix @@ -0,0 +1,49 @@ +# Set default file associations via DefaultAssociations.xml policy. +# Writes XML to C:\Program Files\vmix\ (survives sysprep as installed program). +# Sets Group Policy registry to point at the XML. +{ pkgs, lib, ... }: +{ + name = "default-apps"; + auditScript = '' + @echo off + set "DA=C:\Program Files\vmix\DefaultAssociations.xml" + mkdir "C:\Program Files\vmix" 2>nul + echo ^ > "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + echo ^ >> "%DA%" + reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v DefaultAssociationsConfiguration /t REG_SZ /d "C:\Program Files\vmix\DefaultAssociations.xml" /f + ''; +} diff --git a/lib/images/windows/templates/default.nix b/lib/images/windows/templates/default.nix new file mode 100644 index 0000000..a23c7ed --- /dev/null +++ b/lib/images/windows/templates/default.nix @@ -0,0 +1,43 @@ +# Windows customization templates. +# Templates can provide: +# windowsRegistry — offline registry merge (fast, no boot) +# auditScript — runs in Audit Mode via QEMU boot +# cdroms — ISOs to attach when booting for auditScript +# uploads — files to inject into the image before auditScript +{ pkgs, lib, system, drivers, makeFilesISO, ... }: +let + args = { inherit pkgs lib system drivers makeFilesISO; }; +in { + # Essentials (drivers, runtimes, removals, performance) + essentials = { + virtioTools = import ./essentials/virtio-tools.nix args; + removeEdge = import ./essentials/remove-edge.nix args; + removeIE = import ./essentials/remove-ie.nix args; + removeWMP = import ./essentials/remove-wmp.nix args; + removePaint = import ./essentials/remove-paint.nix args; + amdGpuDrivers = import ./essentials/amd-gpu-drivers.nix args; + vcppRuntimes = import ./essentials/vcpp-runtimes.nix args; + bestPerformance = import ./essentials/best-performance.nix args; + clearFileAssociations = import ./essentials/clear-file-associations.nix args; + }; + + # Applications + apps = { + thorium = import ./apps/thorium.nix args; + edgeWebview = import ./apps/edge-webview.nix args; + sevenZip = import ./apps/7zip.nix args; + vlc = import ./apps/vlc.nix args; + imageGlass = import ./apps/imageglass.nix args; + sandboxie = import ./apps/sandboxie.nix args; + office = import ./apps/office.nix args; + }; + + # Default file associations policy + defaultApps = import ./default-apps.nix args; + + # Generalize (sysprep + OOBE) + generalize = import ./generalize.nix args; + + # Offline registry templates + reg = import ./registry args; +} diff --git a/lib/images/windows/templates/essentials/amd-gpu-drivers.nix b/lib/images/windows/templates/essentials/amd-gpu-drivers.nix new file mode 100644 index 0000000..b92446f --- /dev/null +++ b/lib/images/windows/templates/essentials/amd-gpu-drivers.nix @@ -0,0 +1,11 @@ +# Install AMD GPU drivers (silent install via INF from ISO) +{ drivers, makeFilesISO, ... }: +{ + name = "amd-gpu"; + cdroms = [ (makeFilesISO { name = "amd-gpu-drivers"; files = [ drivers.amd-gpu-zip ]; }) ]; + auditScript = '' + @echo off + echo Installing AMD GPU drivers (INF)... + pnputil /add-driver "D:\WT6A_INF\u0413647.inf" /install /subdirs + ''; +} diff --git a/lib/images/windows/templates/essentials/best-performance.nix b/lib/images/windows/templates/essentials/best-performance.nix new file mode 100644 index 0000000..09a3d18 --- /dev/null +++ b/lib/images/windows/templates/essentials/best-performance.nix @@ -0,0 +1,46 @@ +# Best Performance visual effects — set on Administrator profile in Audit Mode. +# Used with generalize's CopyProfile=true: the Administrator's profile +# (with these settings applied) becomes the default profile for new users. +# CopyProfile bypasses SystemParametersInfo defaults during profile creation. +# CopyProfile is the only approach that reliably actually works. Editing NTUSER.dat still reset some parameters +{ ... }: +{ + name = "best-perf"; + auditScript = '' + @echo off + echo Applying best performance to current profile... + :: Set appearance options to "custom" + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects" /v VisualFXSetting /t REG_DWORD /d 3 /f + :: Animate controls, fade/slide menus, fade/slide tooltips, + :: fade out menu items, shadows under mouse, shadows under windows, + :: slide open combo boxes, smooth-scroll list boxes (disabled) + reg add "HKCU\Control Panel\Desktop" /v UserPreferencesMask /t REG_BINARY /d 9012038010000000 /f + :: Animate windows when minimizing and maximizing (disabled) + reg add "HKCU\Control Panel\Desktop\WindowMetrics" /v MinAnimate /t REG_SZ /d "0" /f + :: Animations in the taskbar (disabled) + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v TaskbarAnimations /t REG_DWORD /d 0 /f + :: Enable Peek (disabled) + reg add "HKCU\Software\Microsoft\Windows\DWM" /v EnableAeroPeek /t REG_DWORD /d 0 /f + :: Save taskbar thumbnail previews (disabled) + reg add "HKCU\Software\Microsoft\Windows\DWM" /v AlwaysHibernateThumbnails /t REG_DWORD /d 0 /f + :: Show thumbnails instead of icons (disabled) + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v IconsOnly /t REG_DWORD /d 1 /f + :: Show translucent selection rectangle (disabled) + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v ListviewAlphaSelect /t REG_DWORD /d 0 /f + :: Show window contents while dragging (disabled) + reg add "HKCU\Control Panel\Desktop" /v DragFullWindows /t REG_SZ /d "0" /f + :: Smooth edges of screen fonts (enabled) + reg add "HKCU\Control Panel\Desktop" /v FontSmoothing /t REG_SZ /d "2" /f + reg add "HKCU\Control Panel\Desktop" /v FontSmoothingGamma /t REG_DWORD /d 0 /f + reg add "HKCU\Control Panel\Desktop" /v FontSmoothingOrientation /t REG_DWORD /d 1 /f + reg add "HKCU\Control Panel\Desktop" /v FontSmoothingType /t REG_DWORD /d 2 /f + :: Use drop shadows for icon labels on the desktop (disabled) + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v ListviewShadow /t REG_DWORD /d 0 /f + :: Disable transparency effects + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v EnableTransparency /t REG_DWORD /d 0 /f + :: Disable accent color on taskbar and window borders + reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" /v ColorPrevalence /t REG_DWORD /d 0 /f + reg add "HKCU\Software\Microsoft\Windows\DWM" /v ColorPrevalence /t REG_DWORD /d 0 /f + + ''; +} diff --git a/lib/images/windows/templates/essentials/clear-file-associations.nix b/lib/images/windows/templates/essentials/clear-file-associations.nix new file mode 100644 index 0000000..5f4bd32 --- /dev/null +++ b/lib/images/windows/templates/essentials/clear-file-associations.nix @@ -0,0 +1,13 @@ +# Clear all FileExts UserChoice entries on the Administrator profile. +# In Audit Mode these keys aren't hash-protected yet. +# With CopyProfile=true in generalize, the clean profile (without UserChoice) +# is copied to new users, so HKLM Classes become the effective defaults. +{ ... }: +{ + name = "clear-assoc"; + auditScript = '' + @echo off + echo Clearing FileExts UserChoice entries... + reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FileExts" /f 2>nul + ''; +} diff --git a/lib/images/windows/templates/essentials/remove-edge.nix b/lib/images/windows/templates/essentials/remove-edge.nix new file mode 100644 index 0000000..4e1e9ac --- /dev/null +++ b/lib/images/windows/templates/essentials/remove-edge.nix @@ -0,0 +1,50 @@ +# Remove Microsoft Edge (both Chromium and built-in LTSC versions) +{ ... }: +{ + name = "no-edge"; + auditScript = '' + @echo off + :: Remove Chromium Edge using its installer + for /f "delims=" %%i in ('dir /b /ad "C:\Program Files (x86)\Microsoft\Edge\Application" 2^>nul') do ( + if exist "C:\Program Files (x86)\Microsoft\Edge\Application\%%i\Installer\setup.exe" ( + "C:\Program Files (x86)\Microsoft\Edge\Application\%%i\Installer\setup.exe" --uninstall --system-level --force-uninstall + ) + ) + + :: Remove Edge Update + if exist "C:\Program Files (x86)\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe" ( + "C:\Program Files (x86)\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe" /uninstall + ) + + :: Remove Edge directories + rmdir /s /q "C:\Program Files (x86)\Microsoft\Edge" 2>nul + rmdir /s /q "C:\Program Files (x86)\Microsoft\EdgeUpdate" 2>nul + rmdir /s /q "C:\Program Files (x86)\Microsoft\EdgeCore" 2>nul + + :: Remove Edge and DevToolsClient SystemApps + for /d %%d in ("C:\Windows\SystemApps\Microsoft.MicrosoftEdge_*") do ( + takeown /f "%%d" /r /d y >nul 2>nul + icacls "%%d" /grant Administrators:F /t >nul 2>nul + rmdir /s /q "%%d" 2>nul + ) + for /d %%d in ("C:\Windows\SystemApps\Microsoft.MicrosoftEdgeDevToolsClient_*") do ( + takeown /f "%%d" /r /d y >nul 2>nul + icacls "%%d" /grant Administrators:F /t >nul 2>nul + rmdir /s /q "%%d" 2>nul + ) + + :: Remove Edge AppxPackage for all users (audit mode) + powershell -Command "Get-AppxPackage -AllUsers *MicrosoftEdge* | Remove-AppxPackage -AllUsers -EA SilentlyContinue" + + :: Remove Edge shortcuts + del /q "%ProgramData%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk" 2>nul + del /q "%PUBLIC%\Desktop\Microsoft Edge.lnk" 2>nul + del /q "%APPDATA%\Microsoft\Internet Explorer\Quick Launch\Microsoft Edge.lnk" 2>nul + del /q "%APPDATA%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Microsoft Edge.lnk" 2>nul + + :: Remove Edge Update scheduled tasks + schtasks /delete /tn "\MicrosoftEdgeUpdateTaskMachineCore" /f 2>nul + schtasks /delete /tn "\MicrosoftEdgeUpdateTaskMachineUA" /f 2>nul + reg add "HKLM\SOFTWARE\Microsoft\EdgeUpdate" /v DoNotUpdateToEdgeWithChromium /t REG_DWORD /d 1 /f + ''; +} diff --git a/lib/images/windows/templates/essentials/remove-ie.nix b/lib/images/windows/templates/essentials/remove-ie.nix new file mode 100644 index 0000000..cb52014 --- /dev/null +++ b/lib/images/windows/templates/essentials/remove-ie.nix @@ -0,0 +1,12 @@ +# Remove Internet Explorer +{ ... }: +{ + name = "no-ie"; + auditScript = '' + @echo off + echo Removing Internet Explorer... + dism /online /Remove-Capability /CapabilityName:Browser.InternetExplorer~~~~0.0.11.0 /NoRestart 2>nul + :: Disable IE feature if still present + dism /online /Disable-Feature /FeatureName:Internet-Explorer-Optional-amd64 /NoRestart 2>nul + ''; +} diff --git a/lib/images/windows/templates/essentials/remove-paint.nix b/lib/images/windows/templates/essentials/remove-paint.nix new file mode 100644 index 0000000..8dc23c8 --- /dev/null +++ b/lib/images/windows/templates/essentials/remove-paint.nix @@ -0,0 +1,20 @@ +# Remove Microsoft Paint +{ ... }: +{ + name = "no-paint"; + auditScript = '' + @echo off + echo Removing Microsoft Paint... + :: Take ownership and delete mspaint.exe + takeown /f "C:\Windows\System32\mspaint.exe" >nul 2>nul + icacls "C:\Windows\System32\mspaint.exe" /grant Administrators:F >nul 2>nul + del /f "C:\Windows\System32\mspaint.exe" 2>nul + :: Remove Paint optional feature + dism /online /Remove-Capability /CapabilityName:Microsoft.Windows.MSPaint~~~~0.0.1.0 /NoRestart 2>nul + :: Remove Paint shortcuts + del /q "%ProgramData%\Microsoft\Windows\Start Menu\Programs\Accessories\Paint.lnk" 2>nul + :: Remove PBrush class registration + reg delete "HKLM\SOFTWARE\Classes\PBrush" /f 2>nul + reg delete "HKLM\SOFTWARE\Classes\pbrush" /f 2>nul + ''; +} diff --git a/lib/images/windows/templates/essentials/remove-wmp.nix b/lib/images/windows/templates/essentials/remove-wmp.nix new file mode 100644 index 0000000..26c92f7 --- /dev/null +++ b/lib/images/windows/templates/essentials/remove-wmp.nix @@ -0,0 +1,11 @@ +# Remove Windows Media Player +{ ... }: +{ + name = "no-wmp"; + auditScript = '' + @echo off + echo Removing Windows Media Player... + dism /online /Disable-Feature /FeatureName:WindowsMediaPlayer /NoRestart 2>nul + dism /online /Remove-Capability /CapabilityName:Microsoft.Windows.MediaPlayer~~~~0.0.12.0 /NoRestart 2>nul + ''; +} diff --git a/lib/images/windows/templates/essentials/vcpp-runtimes.nix b/lib/images/windows/templates/essentials/vcpp-runtimes.nix new file mode 100644 index 0000000..e6e49f9 --- /dev/null +++ b/lib/images/windows/templates/essentials/vcpp-runtimes.nix @@ -0,0 +1,18 @@ +# Install all Visual C++ Redistributable runtimes (2005-2022, x86+x64) +{ pkgs, makeFilesISO, ... }: +let + installer = pkgs.fetchurl { + url = "https://github.com/abbodi1406/vcredist/releases/download/v0.103.0/VisualCppRedist_AIO_x86_x64.exe"; + hash = "sha256-PBiORlG8wH3yvbBtaVhRWHl7G6+5FVewkhdiFijNWyM="; + }; +in { + name = "vcpp"; + cdroms = [ (makeFilesISO { name = "vcpp-runtimes"; files = [ installer ]; }) ]; + auditScript = '' + @echo off + echo Installing Visual C++ Redistributable runtimes... + copy D:\VisualCppRedist_AIO_x86_x64.exe C:\vcpp-setup.exe + start /wait C:\vcpp-setup.exe /ai /gm2 + del /q C:\vcpp-setup.exe + ''; +} diff --git a/lib/images/windows/templates/essentials/virtio-tools.nix b/lib/images/windows/templates/essentials/virtio-tools.nix new file mode 100644 index 0000000..03382d8 --- /dev/null +++ b/lib/images/windows/templates/essentials/virtio-tools.nix @@ -0,0 +1,18 @@ +# Install VirtIO guest tools (QEMU agent + SPICE vdagent) +{ drivers, ... }: +{ + name = "virtio"; + cdroms = [ drivers.virtio-iso ]; + auditScript = '' + @echo off + :: VirtIO ISO is the first (and only) CD — drive letter D: + if exist D:\cert\virtio_win_cert.cer ( + certutil -addstore TrustedPublisher D:\cert\virtio_win_cert.cer + ) + if exist D:\virtio-win-guest-tools.exe ( + D:\virtio-win-guest-tools.exe /install /passive /norestart + ) else if exist D:\guest-agent\qemu-ga-x86_64.msi ( + msiexec /i D:\guest-agent\qemu-ga-x86_64.msi /qn /norestart + ) + ''; +} diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix new file mode 100644 index 0000000..efe1cc8 --- /dev/null +++ b/lib/images/windows/templates/generalize.nix @@ -0,0 +1,168 @@ +# Generalize image via sysprep + OOBE in two phases. +# Phase 1 (sysprep): runs sysprep /generalize /oobe /shutdown in Audit Mode +# Phase 2 (oobe): boots through OOBE, creates user, activates Windows, shuts down +# Between phases, NTUSER.DAT can be modified offline. +# Usage: (templates.generalize { username = "User"; password = ""; }) +{ pkgs, lib, makeFilesISO, ... }: +let + masScript = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/massgravel/Microsoft-Activation-Scripts/97602941e5724316aa31b6ca1da5c70245d234d5/MAS/All-In-One-Version-KL/MAS_AIO.cmd"; + hash = "sha256-1hl89jQf2p+RtE3ue/+cZevSoz7Ra3p3u350aE/Xy74="; + }; +in +{ + username ? "User", + password ? "", + autoLogon ? true, + hostname ? "WIN-VM", + locale ? "en-US", + timezone ? "UTC", + # Desktop background solid color as hex string (e.g. "8e8cd8") + bgColor ? null, +}: let + # Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry + hexToRgbStr = hex: let + hexChars = lib.stringToCharacters hex; + hexToDec = h: let + c = lib.toLower h; + m = { "0"=0; "1"=1; "2"=2; "3"=3; "4"=4; "5"=5; "6"=6; "7"=7; "8"=8; "9"=9; "a"=10; "b"=11; "c"=12; "d"=13; "e"=14; "f"=15; }; + in m.${c}; + r = hexToDec (builtins.elemAt hexChars 0) * 16 + hexToDec (builtins.elemAt hexChars 1); + g = hexToDec (builtins.elemAt hexChars 2) * 16 + hexToDec (builtins.elemAt hexChars 3); + b = hexToDec (builtins.elemAt hexChars 4) * 16 + hexToDec (builtins.elemAt hexChars 5); + in "${toString r} ${toString g} ${toString b}"; + + stripHash = s: lib.removePrefix "#" s; + bgRgb = if bgColor != null then hexToRgbStr (stripHash bgColor) else null; + + # Post-OOBE script: runs as the created user via FirstLogonCommands. + postOobeScript = pkgs.writeText "post-oobe.cmd" '' + @echo off + ${lib.optionalString (!autoLogon) '' + reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon /f 2>nul + reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v DefaultUserName /f 2>nul + reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v DefaultPassword /f 2>nul + ''} + ${lib.optionalString (bgColor != null) '' + :: Set solid background color + reg add "HKCU\Control Panel\Desktop" /v WallPaper /t REG_SZ /d "" /f + reg add "HKCU\Control Panel\Colors" /v Background /t REG_SZ /d "${bgRgb}" /f + reg add "HKCU\Control Panel\Desktop" /v WallpaperStyle /t REG_SZ /d "0" /f + ''} + + :: Remove Edge AppxPackage for current user (runs in user context during OOBE) + :: The app is already removed on one of the templates but a ghost appx entry remains that can only be deleted at the user level + powershell -Command "Get-AppxPackage *MicrosoftEdge* | Remove-AppxPackage -ErrorAction SilentlyContinue" + powershell -Command "Get-AppxPackage *MicrosoftEdgeDevToolsClient* | Remove-AppxPackage -ErrorAction SilentlyContinue" + + + :: Activate Windows using HWID method + if exist C:\MAS_AIO.cmd ( + echo. | call C:\MAS_AIO.cmd /HWID + ) + + :: Activate Office using Ohook method (if Office is installed) + if exist "C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE" ( + if exist C:\MAS_AIO.cmd ( + echo. | call C:\MAS_AIO.cmd /Ohook + ) + ) + del /q C:\MAS_AIO.cmd 2>nul + + :: Clean up + del /q C:\oobe-unattend.xml 2>nul + del /q C:\vmix-audit-script.cmd 2>nul + del /q C:\vmix-audit-wrapper.cmd 2>nul + + shutdown /s /t 5 /c "vmix generalize complete" + del /q C:\post-oobe.cmd 2>nul + ''; + + oobeXml = pkgs.writeText "oobe-unattend.xml" '' + + + + + + true + + Automatic + + + + + + + ${locale} + ${locale} + ${locale} + ${locale} + + + + true + true + true + true + Work + true + true + 3 + + + + + + ${password} + + Administrators + ${username} + + + + + + ${password} + + true + ${username} + + ${hostname} + ${timezone} + + + 1 + C:\post-oobe.cmd + false + + + + + + ''; +in { + name = "generalize"; + uploads = [ + { source = oobeXml; dest = "/oobe-unattend.xml"; } + { source = postOobeScript; dest = "/post-oobe.cmd"; } + { source = masScript; dest = "/MAS_AIO.cmd"; } + ]; + # Sysprep reboots into OOBE within the same QEMU session + auditScript = '' + @echo off + C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /reboot /quiet /unattend:C:\oobe-unattend.xml + ''; +} + + # :: Enable RDP (sysprep resets offline registry changes) + # reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f + # reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v UserAuthentication /t REG_DWORD /d 0 /f + # netsh advfirewall firewall add rule name="RDP" dir=in protocol=tcp localport=3389 action=allow + # :: Start and enable the RDP service + # sc config TermService start= auto + # net start TermService + diff --git a/lib/images/windows/templates/registry/ai.nix b/lib/images/windows/templates/registry/ai.nix new file mode 100644 index 0000000..3688222 --- /dev/null +++ b/lib/images/windows/templates/registry/ai.nix @@ -0,0 +1,10 @@ +# Disable Copilot, Recall, and AI data analysis +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot] + "TurnOffWindowsCopilot"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsAI] + "AllowRecallEnablement"=dword:00000000 + "DisableAIDataAnalysis"=dword:00000001 +'' diff --git a/lib/images/windows/templates/registry/consumer.nix b/lib/images/windows/templates/registry/consumer.nix new file mode 100644 index 0000000..12ecaa2 --- /dev/null +++ b/lib/images/windows/templates/registry/consumer.nix @@ -0,0 +1,50 @@ +# Disable consumer features, suggested apps, push-to-install, widgets, Cortana +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\CloudContent] + "DisableWindowsConsumerFeatures"=dword:00000001 + "DisableTailoredExperiencesWithDiagnosticData"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\PushToInstall] + "DisablePushToInstall"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Windows Feeds] + "EnableFeeds"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Dsh] + "AllowNewsAndInterests"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Windows Search] + "AllowCortana"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Explorer] + "DisableSearchBoxSuggestions"=dword:00000001 + "HideSCAMeetNow"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\GameDVR] + "AllowGameDVR"=dword:00000000 + + [HKEY_LOCAL_MACHINE\System\GameConfigStore] + "GameDVR_Enabled"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Maps] + "AutoDownloadAndUpdateMapData"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MapsBroker] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\XblAuthManager] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\XblGameSave] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\XboxNetApiSvc] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\WMPNetworkSvc] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\RetailDemo] + "Start"=dword:00000004 +'' diff --git a/lib/images/windows/templates/registry/default.nix b/lib/images/windows/templates/registry/default.nix new file mode 100644 index 0000000..2750934 --- /dev/null +++ b/lib/images/windows/templates/registry/default.nix @@ -0,0 +1,63 @@ +# Offline registry customization templates. +# Each file returns raw registry entries (no header). +# Templates are composed into bundles via mkReg which adds the .reg header. +{ ... }: +let + regHeader = "Windows Registry Editor Version 5.00"; + mkReg = entries: '' + ${regHeader} + ${entries} + ''; + + rdpEntries = import ./rdp.nix; + telemetryEntries = import ./telemetry.nix; + errorReportingEntries = import ./error-reporting.nix; + defenderEntries = import ./defender.nix; + updatesEntries = import ./updates.nix; + smartScreenEntries = import ./smart-screen.nix; + hibernationEntries = import ./hibernation.nix; + systemRestoreEntries = import ./system-restore.nix; + networkEntries = import ./insecure-samba.nix; + privacyEntries = import ./privacy.nix; + aiEntries = import ./ai.nix; + consumerEntries = import ./consumer.nix; + performanceEntries = import ./performance.nix; + disableUcpdEntries = import ./disable-ucpd.nix; + +in rec { + # === Individual templates === + disableTelemetry = { name = "no-telemetry"; windowsRegistry = mkReg telemetryEntries; }; + disableErrorReporting = { name = "no-wer"; windowsRegistry = mkReg errorReportingEntries; }; + disableDefender = { name = "no-defender"; windowsRegistry = mkReg defenderEntries; }; + disableUpdates = { name = "no-updates"; windowsRegistry = mkReg updatesEntries; }; + disableSmartScreen = { name = "no-smartscreen"; windowsRegistry = mkReg smartScreenEntries; }; + disableHibernation = { name = "no-hibernate"; windowsRegistry = mkReg hibernationEntries; }; + disableSystemRestore = { name = "no-restore"; windowsRegistry = mkReg systemRestoreEntries; }; + networkTweaks = { name = "network"; windowsRegistry = mkReg networkEntries; }; + disablePrivacyTracking = { name = "no-tracking"; windowsRegistry = mkReg privacyEntries; }; + disableAI = { name = "no-ai"; windowsRegistry = mkReg aiEntries; }; + disableConsumerFeatures = { name = "no-consumer"; windowsRegistry = mkReg consumerEntries; }; + performanceTweaks = { name = "performance"; windowsRegistry = mkReg performanceEntries; }; + disableUCPD = { name = "no-ucpd"; windowsRegistry = mkReg disableUcpdEntries; }; + + # === Convenience bundles == + + # Hardened: comprehensive debloat for lab VMs + hardened = { + name = "hardened"; + windowsRegistry = mkReg ( + telemetryEntries + + errorReportingEntries + + defenderEntries + + updatesEntries + + smartScreenEntries + + hibernationEntries + + systemRestoreEntries + + networkEntries + + privacyEntries + + aiEntries + + consumerEntries + + performanceEntries + ); + }; +} diff --git a/lib/images/windows/templates/registry/defender.nix b/lib/images/windows/templates/registry/defender.nix new file mode 100644 index 0000000..9d268df --- /dev/null +++ b/lib/images/windows/templates/registry/defender.nix @@ -0,0 +1,11 @@ +# Disable Windows Defender +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender] + "DisableAntiSpyware"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection] + "DisableRealtimeMonitoring"=dword:00000001 +'' diff --git a/lib/images/windows/templates/registry/disable-ucpd.nix b/lib/images/windows/templates/registry/disable-ucpd.nix new file mode 100644 index 0000000..a7ad95e --- /dev/null +++ b/lib/images/windows/templates/registry/disable-ucpd.nix @@ -0,0 +1,10 @@ +# Disable User Choice Protection Driver (UCPD) on Win11 +# This allows programmatic changes to file/URL associations via UserChoice +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System] + "EnableUCPD"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\UCPD] + "Start"=dword:00000004 +'' diff --git a/lib/images/windows/templates/registry/error-reporting.nix b/lib/images/windows/templates/registry/error-reporting.nix new file mode 100644 index 0000000..6424e77 --- /dev/null +++ b/lib/images/windows/templates/registry/error-reporting.nix @@ -0,0 +1,9 @@ +# Disable Windows Error Reporting +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting] + "Disabled"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\WerSvc] + "Start"=dword:00000004 +'' diff --git a/lib/images/windows/templates/registry/hibernation.nix b/lib/images/windows/templates/registry/hibernation.nix new file mode 100644 index 0000000..89074df --- /dev/null +++ b/lib/images/windows/templates/registry/hibernation.nix @@ -0,0 +1,9 @@ +# Disable hibernation and fast startup (avoids disk corruption with snapshots) +'' + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\Power] + "HiberbootEnabled"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Power] + "HibernateEnabled"=dword:00000000 +'' diff --git a/lib/images/windows/templates/registry/insecure-samba.nix b/lib/images/windows/templates/registry/insecure-samba.nix new file mode 100644 index 0000000..d307f8d --- /dev/null +++ b/lib/images/windows/templates/registry/insecure-samba.nix @@ -0,0 +1,12 @@ +# Suppress network discovery popup and allow insecure guest logons for SMB +'' + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Network\NewNetworkWindowOff] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\LanmanWorkstation] + "AllowInsecureGuestAuth"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\LanmanWorkstation\Parameters] + "AllowInsecureGuestAuth"=dword:00000001 + "RequireSecuritySignature"=dword:00000000 +'' diff --git a/lib/images/windows/templates/registry/performance.nix b/lib/images/windows/templates/registry/performance.nix new file mode 100644 index 0000000..0dcb4c8 --- /dev/null +++ b/lib/images/windows/templates/registry/performance.nix @@ -0,0 +1,34 @@ +# VM performance optimizations: disable power throttling, SysMain, lock screen, autorun +'' + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Power] + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Power\PowerThrottling] + "PowerThrottlingOff"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control] + "SvcHostSplitThresholdInKB"=dword:00400000 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\FileSystem] + "LongPathsEnabled"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Remote Assistance] + "fAllowToGetHelp"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Personalization] + "NoLockScreen"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer] + "NoDriveTypeAutoRun"=dword:000000ff + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\FileHistory] + "Disabled"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\SysMain] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\RemoteRegistry] + "Start"=dword:00000004 +'' diff --git a/lib/images/windows/templates/registry/privacy.nix b/lib/images/windows/templates/registry/privacy.nix new file mode 100644 index 0000000..7e14b9e --- /dev/null +++ b/lib/images/windows/templates/registry/privacy.nix @@ -0,0 +1,44 @@ +# Disable activity tracking, advertising ID, location, and input data collection +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System] + "EnableActivityFeed"=dword:00000000 + "PublishUserActivities"=dword:00000000 + "UploadUserActivities"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\AdvertisingInfo] + "DisabledByGroupPolicy"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Privacy] + "TailoredExperiencesWithDiagnosticDataEnabled"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\LocationAndSensors] + "DisableLocation"=dword:00000001 + "DisableLocationScripting"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\lfsvc] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\HandwritingErrorReports] + "PreventHandwritingErrorReports"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\TabletPC] + "PreventHandwritingDataSharing"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\InputPersonalization] + "RestrictImplicitTextCollection"=dword:00000001 + "RestrictImplicitInkCollection"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsInkWorkspace] + "AllowWindowsInkWorkspace"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Settings] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy] + "HasAccepted"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy] + "LetAppsRunInBackground"=dword:00000002 +'' diff --git a/lib/images/windows/templates/registry/smart-screen.nix b/lib/images/windows/templates/registry/smart-screen.nix new file mode 100644 index 0000000..cc6fdf1 --- /dev/null +++ b/lib/images/windows/templates/registry/smart-screen.nix @@ -0,0 +1,6 @@ +# Disable SmartScreen +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System] + "EnableSmartScreen"=dword:00000000 +'' diff --git a/lib/images/windows/templates/registry/system-restore.nix b/lib/images/windows/templates/registry/system-restore.nix new file mode 100644 index 0000000..4bca710 --- /dev/null +++ b/lib/images/windows/templates/registry/system-restore.nix @@ -0,0 +1,8 @@ +# Disable system restore +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\SystemRestore] + "DisableSR"=dword:00000001 +'' diff --git a/lib/images/windows/templates/registry/telemetry.nix b/lib/images/windows/templates/registry/telemetry.nix new file mode 100644 index 0000000..a3d1c50 --- /dev/null +++ b/lib/images/windows/templates/registry/telemetry.nix @@ -0,0 +1,15 @@ +# Disable telemetry, diagnostics tracking, and feedback +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\DataCollection] + "AllowTelemetry"=dword:00000000 + "DoNotShowFeedbackNotifications"=dword:00000001 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\DiagTrack] + "Start"=dword:00000004 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\dmwappushservice] + "Start"=dword:00000004 +'' diff --git a/lib/images/windows/templates/registry/updates.nix b/lib/images/windows/templates/registry/updates.nix new file mode 100644 index 0000000..4978080 --- /dev/null +++ b/lib/images/windows/templates/registry/updates.nix @@ -0,0 +1,21 @@ +# Disable automatic Windows updates, driver searching, and update-related annoyances +'' + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU] + "NoAutoUpdate"=dword:00000001 + "NoAutoRebootWithLoggedOnUsers"=dword:00000001 + "AUPowerManagement"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization] + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config] + "DODownloadMode"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\DriverSearching] + "SearchOrderConfig"=dword:00000000 + + [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\DoSvc] + "Start"=dword:00000004 +'' diff --git a/lib/images/windows/win10/default.nix b/lib/images/windows/win10/default.nix new file mode 100644 index 0000000..19b3e50 --- /dev/null +++ b/lib/images/windows/win10/default.nix @@ -0,0 +1,12 @@ +{ pkgs, lib, system, windows, ... }: +let + upstreamISOsJSON = lib.importJSON ./upstream.json; + + fetchUpstream = name: src: + if (src.type or "") == "fetchgit" + then pkgs.fetchgit { inherit (src) url rev hash fetchLFS; } + else pkgs.fetchurl { inherit (src) url sha256; }; + + upstreamISOs = lib.mapAttrs fetchUpstream upstreamISOsJSON.${system}; + images = (import ./images.nix) { inherit pkgs lib system windows upstreamISOs; }; +in images diff --git a/lib/images/windows/win10/images.nix b/lib/images/windows/win10/images.nix new file mode 100644 index 0000000..830d96c --- /dev/null +++ b/lib/images/windows/win10/images.nix @@ -0,0 +1,34 @@ +# Pre-built Win10 LTSC 2021 images from upstream ISO +# Pipeline: makeImage (Audit Mode) → essentials → apps → registry → generalize +{ pkgs, lib, system, windows, upstreamISOs, ... }: +with windows; +{ + ltsc = rec { + upstream = makeImage { + name = "win10-ltsc-2021"; + upstreamISO = upstreamISOs.win10-ltsc-2021; + productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D"; + }; + basic = customizeImageFold upstream (with templates; [ + essentials.virtioTools + essentials.removeIE + essentials.removeWMP + essentials.removeEdge + essentials.vcppRuntimes + essentials.bestPerformance + reg.hardened + apps.edgeWebview + apps.thorium + ]); + + withApps = customizeImageFold basic (with templates; [ + apps.sandboxie + apps.sevenZip + apps.vlc + apps.imageGlass + apps.office + ]); + + withAMDGPU = customizeImage basic templates.essentials.amdGpuDrivers; + }; +} diff --git a/lib/images/windows/win10/upstream.json b/lib/images/windows/win10/upstream.json new file mode 100644 index 0000000..c9f8ead --- /dev/null +++ b/lib/images/windows/win10/upstream.json @@ -0,0 +1,11 @@ +{ + "x86_64-linux": { + "win10-ltsc-2021": { + "type": "fetchgit", + "url": "https://git.sagar.ch/dotfiles/win10ltsc2021-official.iso.git", + "rev": "9bb55c21ceaf224504c4240677e808b3ec91a610", + "hash": "sha256-AeGnOnkToOSiD5iRblLKaRmR8qBUkCaGp8F8KkGlVIA=", + "fetchLFS": true + } + } +} diff --git a/lib/images/windows/win11/default.nix b/lib/images/windows/win11/default.nix new file mode 100644 index 0000000..19b3e50 --- /dev/null +++ b/lib/images/windows/win11/default.nix @@ -0,0 +1,12 @@ +{ pkgs, lib, system, windows, ... }: +let + upstreamISOsJSON = lib.importJSON ./upstream.json; + + fetchUpstream = name: src: + if (src.type or "") == "fetchgit" + then pkgs.fetchgit { inherit (src) url rev hash fetchLFS; } + else pkgs.fetchurl { inherit (src) url sha256; }; + + upstreamISOs = lib.mapAttrs fetchUpstream upstreamISOsJSON.${system}; + images = (import ./images.nix) { inherit pkgs lib system windows upstreamISOs; }; +in images diff --git a/lib/images/windows/win11/images.nix b/lib/images/windows/win11/images.nix new file mode 100644 index 0000000..ef2b6cb --- /dev/null +++ b/lib/images/windows/win11/images.nix @@ -0,0 +1,38 @@ +# Pre-built Win11 LTSC 2024 images from upstream ISO +# Pipeline: makeImage (Audit Mode) → essentials → apps → registry +{ pkgs, lib, system, windows, upstreamISOs, ... }: +with windows; +{ + ltsc = rec { + upstream = makeImage { + name = "win11-ltsc-2024"; + upstreamISO = upstreamISOs.win11-ltsc-2024; + productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D"; + bypassRequirements = true; + windowsVersionForVirtioDrivers = "w11"; + }; + + basic = customizeImageFold upstream (with templates; [ + essentials.virtioTools + essentials.removeIE + essentials.removeWMP + essentials.removeEdge + essentials.vcppRuntimes + essentials.bestPerformance + reg.hardened + reg.disableUCPD + apps.edgeWebview + apps.thorium + ]); + + withApps = customizeImageFold basic (with templates; [ + apps.sandboxie + apps.sevenZip + apps.vlc + apps.imageGlass + apps.office + ]); + + withAMDGPU = customizeImage basic templates.essentials.amdGpuDrivers; + }; +} diff --git a/lib/images/windows/win11/upstream.json b/lib/images/windows/win11/upstream.json new file mode 100644 index 0000000..0fed555 --- /dev/null +++ b/lib/images/windows/win11/upstream.json @@ -0,0 +1,11 @@ +{ + "x86_64-linux": { + "win11-ltsc-2024": { + "type": "fetchgit", + "url": "https://git.sagar.ch/dotfiles/win11ltsc2024-official.iso.git", + "rev": "d4a749719eb593884d1ba9835b96104e1f06290b", + "hash": "sha256-AWp4SPVfpHT7jAczaq24bgrYUZy4sLByRHMhGbPFBwg=", + "fetchLFS": true + } + } +} diff --git a/lib/network.nix b/lib/network.nix index 04c1dfe..b91ba75 100644 --- a/lib/network.nix +++ b/lib/network.nix @@ -1,5 +1,5 @@ { pkgs, lib, ... }: rec { - calc = (import ((builtins.fetchTarball "https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba/archive/3885f7cd9ed0a746a9d675da6f265d41e9fd6704.tar.gz") + "/net.nix" ) { inherit lib; }).lib.net; + calc = (import ((builtins.fetchTarball { url = "https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba/archive/3885f7cd9ed0a746a9d675da6f265d41e9fd6704.tar.gz"; sha256 = "sha256:0s17g6jdkfnsqxwh1k9arhn4r2aa3rknpnhpkppw1sbzjix36c4b"; }) + "/net.nix" ) { inherit lib; }).lib.net; regex.ipv4 = let compRegex = "(25[0-5]|(2[0-4]|10|1?[1-9])?[0-9])"; @@ -8,6 +8,33 @@ regex.cidr4 = "${regex.ipv4}/(3[0-2]|[1-2]?[0-9])"; regex.ipOrCidr4 = "(${regex.ipv4}|${regex.cidr4})"; - ipv4ToInt = ipv4: calc.ip.diff ipv4 "0.0.0.0"; - intToipv4 = inte: calc.ip.add "0.0.0.0" inte; + + ipv4ToInt = ip: + with builtins; + with lib; + let + octets = match ''^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$'' ip; + in + if octets == null then + throw "Invalid IPv4 address: ${ip}" + else + let + a = toInt (elemAt octets 0); + b = toInt (elemAt octets 1); + c = toInt (elemAt octets 2); + d = toInt (elemAt octets 3); + in + if a > 255 || b > 255 || c > 255 || d > 255 then + throw "Invalid IPv4 octet > 255: ${ip}" + else + (a * 256 * 256 * 256) + (b * 256 * 256) + (c * 256) + d; + + intToIpv4 = n: + let + a = n / (256 * 256 * 256); + b = builtins.mod (n / (256 * 256)) 256; + c = builtins.mod (n / 256) 256; + d = builtins.mod n 256; + in + "${toString a}.${toString b}.${toString c}.${toString d}"; } \ No newline at end of file diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..1f26736 --- /dev/null +++ b/module.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./nixos/default.nix + ]; +} \ No newline at end of file diff --git a/nixos/default.nix b/nixos/default.nix index 1ff2390..3c0f982 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -5,5 +5,15 @@ let args = { inherit config pkgs lib vmixLib; }; in { - imports = [ (import ./network args) (import ./vm args) ]; + imports = [ (import ./networks args) (import ./vms args) ]; + + config.nix.settings.sandbox = "relaxed"; # for vm customize to work properly + + options.vmix.namespaces = mkOption { + type = types.attrsOf + (types.submodule (import ./namespaceSubmoduleOptions.nix args)); + default = {}; + }; + + config.nixpkgs.overlays = [ (import ../overlay.nix) ]; } \ No newline at end of file diff --git a/nixos/namespaceSubmoduleOptions.nix b/nixos/namespaceSubmoduleOptions.nix new file mode 100644 index 0000000..1c848e3 --- /dev/null +++ b/nixos/namespaceSubmoduleOptions.nix @@ -0,0 +1,13 @@ +{ lib, ... }@args : +with lib; +{ + options = { + networks = (import ./networks/options.nix args); + + vms = mkOption { + type = types.attrsOf + (types.submodule (import ./vms/submoduleOptions.nix args)); + default = { }; + }; + }; +} \ No newline at end of file diff --git a/nixos/network/config.nix b/nixos/network/config.nix deleted file mode 100644 index edd9986..0000000 --- a/nixos/network/config.nix +++ /dev/null @@ -1,215 +0,0 @@ -{ config, pkgs, lib, vmixLib, ... }: -with vmixLib.network; -let - vmixCfg = config.vmix; - # creates a /30 network from available range for veth-pair wan interfaces - mkVethIPv4Range = index: availableIPv4Range: - let - vethIPv4RangeLength = 30; - in - (calc.cidr.subnet (vethIPv4RangeLength - (calc.cidr.length availableIPv4Range)) index availableIPv4Range); - - namespaceGlobalService = { - "ns.net.vmix@" = { - description = "network namespace %I for vmix"; - before = [ "network.target" ]; - path = with pkgs; [ iproute2 utillinux ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - PrivateMounts = false; - PrivateNetwork = true; - ExecStart = (pkgs.writeShellScript "ns.net.vmix-start" '' - NAMESPACE="$1.vmix" - ip netns add $NAMESPACE - umount /var/run/netns/$NAMESPACE - mount --bind /proc/self/ns/net /var/run/netns/$NAMESPACE - '') + " %I"; - ExecStop = "${pkgs.iproute2}/bin/ip netns del %I.vmix"; - }; - }; - }; - - mkLanDomainName = networkName: lanName: lanCfg: - if (lanCfg.domain != null) then lanCfg.domain else "${lanName}.${networkName}.vmix"; - - mkLan = networkName: staticRoutes: lanName: cfg: - let - lanCfg = cfg // { name = lanName; namespace = "${networkName}"; }; - lanInterfaceName = "brx-${lanCfg.name}"; - lanInterfaceIPAddress = calc.cidr.host 1 lanCfg.ipv4.range; - netmask = calc.cidr.netmask lanCfg.ipv4.range; - networkPrefix = builtins.elemAt (lib.splitString "/" lanCfg.ipv4.range) 1; - - dhcpStartAddress = - if (lanCfg.ipv4.dhcp.startAddress != null) - then lanCfg.ipv4.dhcp.startAddress - else (calc.cidr.host 2 lanCfg.ipv4.range); - - dhcpEndAddress = - if (lanCfg.ipv4.dhcp.endAddress != null) - then lanCfg.ipv4.dhcp.endAddress - else (calc.cidr.host ((calc.cidr.capacity lanCfg.ipv4.range) - 2) lanCfg.ipv4.range); - - createLanInterface = '' - ip link add ${lanInterfaceName} type bridge - ip address add ${lanInterfaceIPAddress}/${networkPrefix} dev ${lanInterfaceName} - ip link set ${lanInterfaceName} up - ''; - - deleteLanInterface = '' - ip link del ${lanInterfaceName} - ''; - - lanDomainName = mkLanDomainName networkName lanName lanCfg; - - lanDnsmasqConf = '' - # lan ${lanName} - dhcp-range=${lanInterfaceName},${dhcpStartAddress},${dhcpEndAddress},${netmask},12h - domain=${lanDomainName},${lanInterfaceName} - dhcp-option=${lanInterfaceName},option:classless-static-route,${lib.concatStringsSep "," (builtins.map (route: "${route},${lanInterfaceIPAddress}") ([ "0.0.0.0/0" ] ++ (builtins.filter (route: route != lanCfg.ipv4.range) staticRoutes)))} - '' + (lib.optionalString (lanCfg.ipv4.dhcp.dns.nameservers != []) ("dhcp-option=${lanInterfaceName},option:dns-server,${(lib.concatStringsSep "," lanCfg.ipv4.dhcp.dns.nameservers)}\n")); - in - lanCfg // { - createIface = createLanInterface; - deleteIface = deleteLanInterface; - dnsmasqConf = lanDnsmasqConf; - domain = lanDomainName; - }; - - mkLansService = networkName: wanCfg: lansCfg: - let - dhcpLeaseFile="/tmp/vmix/lans.${networkName}.dhcp.leases"; - staticRoutes = [ wanCfg.ipv4.range ] ++ (builtins.map (lanCfg: lanCfg.ipv4.range) (lib.attrValues lansCfg)); - lansList = lib.attrValues(lib.mapAttrs (mkLan networkName staticRoutes) lansCfg); - dnsmasqConf = pkgs.writeText "dnsmasq-${networkName}.conf" ('' - dhcp-host=*:*:*:*:*:*,id:* - except-interface=lo - dhcp-authoritative - localise-queries - no-hosts - expand-hosts - dhcp-leasefile=${dhcpLeaseFile} - filter-AAAA - address=/host/${calc.cidr.host 1 wanCfg.ipv4.range} - no-resolv - ${lib.concatStringsSep "\n" (builtins.map (nameserver: "server=${nameserver}") wanCfg.dns.nameservers)} - '' + (lib.concatMapStrings (lan: lan.dnsmasqConf) lansList) - ); - - createLansInterfaces = pkgs.writeShellScript "create-lans-${networkName}-vmix" ('' - # for dnsmasq temp files - mkdir -p /tmp/vmix - rm -f ${dhcpLeaseFile} - '' + (lib.concatMapStrings (lan: lan.createIface) lansList) - ); - - deleteLansInterfaces = pkgs.writeShellScript "delete-lans-${networkName}-vmix" (lib.concatMapStrings (lan: lan.deleteIface) lansList); - in - { - "lans.net.vmix@${networkName}" = rec { - bindsTo = [ "ns.net.vmix@${networkName}.service" ]; - after = bindsTo; - wantedBy = [ "net.vmix@${networkName}.target" ]; - unitConfig.JoinsNamespaceOf = "ns.net.vmix@${networkName}.service"; - path = with pkgs; [ iproute2 ]; - serviceConfig = { - ExecStartPre = createLansInterfaces; - ExecStart = "${pkgs.dnsmasq}/bin/dnsmasq -d -C ${dnsmasqConf}"; - ExecReload = pkgs.writeShellScript "reload-dnsmasq" "kill -HUP $MAINPID"; - ExecStopPost = deleteLansInterfaces; - Restart = "on-failure"; - RestartSec = "5"; - PrivateTmp = true; - ProtectSystem = true; - ProtectHome = true; - PrivateNetwork = true; - }; - }; - }; - - mkWanService = networkName: cfg: - let - wanCfg = cfg // { namespace = networkName; }; - vethInNSToHost.iface = "vhost"; - vethOnHostToNS.iface = "vn-${wanCfg.namespace}"; - vethOnHostToNS.ipv4.address = calc.cidr.host 1 wanCfg.ipv4.range; - vethInNSToHost.ipv4.address = calc.cidr.host 2 wanCfg.ipv4.range; - networkPrefix = builtins.elemAt (lib.splitString "/" wanCfg.ipv4.range) 1; - iptablesMark = builtins.toString (ipv4ToInt vethOnHostToNS.ipv4.address); - - createWanCommands = '' - ip link add ${vethOnHostToNS.iface} type veth peer name ${vethInNSToHost.iface} - ip link set ${vethInNSToHost.iface} netns ${wanCfg.namespace}.vmix - - ip address add ${vethOnHostToNS.ipv4.address}/${networkPrefix} dev ${vethOnHostToNS.iface} - ip netns exec ${wanCfg.namespace}.vmix ip address add ${vethInNSToHost.ipv4.address}/${networkPrefix} dev ${vethInNSToHost.iface} - - iptables -A FORWARD -i ${vethOnHostToNS.iface} -j ACCEPT - iptables -A FORWARD -o ${vethOnHostToNS.iface} -j ACCEPT - #iptables -A INPUT -i ${vethOnHostToNS.iface} -j DROP - - iptables -t mangle -A PREROUTING -i ${vethOnHostToNS.iface} -j MARK --set-mark ${iptablesMark} - iptables -t nat -A POSTROUTING -m mark --mark ${iptablesMark} -j MASQUERADE - - ip link set ${vethOnHostToNS.iface} up - ip netns exec ${wanCfg.namespace}.vmix ip link set ${vethInNSToHost.iface} up - ip netns exec ${wanCfg.namespace}.vmix ip r add default via ${vethOnHostToNS.ipv4.address} - - ${lib.concatMapStrings (lanRange: "ip r add ${lanRange} via ${vethInNSToHost.ipv4.address} \n") wanCfg.lanRanges} - ''; - - createWan = pkgs.writeShellScript "create-wan-${wanCfg.namespace}-vmix" createWanCommands; - - deleteWan = - let - createdIptablesRules = lib.filter (line: (lib.hasPrefix "iptables" line)) (lib.splitString "\n" createWanCommands); - delIptablesRules = builtins.map (rule: lib.replaceStrings [ "-A" ] [ "-D"] rule) createdIptablesRules; - in - pkgs.writeShellScript "delete-wan-${wanCfg.namespace}-vmix" ('' - ip link del ${vethOnHostToNS.iface} - '' + (lib.concatStringsSep "\n" delIptablesRules)); - in - { - "wan.net.vmix@${wanCfg.namespace}" = rec { - bindsTo = [ "ns.net.vmix@${wanCfg.namespace}.service" ]; - after = bindsTo; - wantedBy = [ "net.vmix@${wanCfg.namespace}.target" ]; - path = with pkgs; [ iproute2 iptables ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = createWan; - ExecStop = deleteWan; - }; - }; - }; - - mkMacvlanService = networkName: macvlanName: cfg: - {}; - - mkNetworkServices = networkName: cfg: - let - netCfg = cfg // { name = networkName; }; - vethIPv4RangeForWan = mkVethIPv4Range netCfg.index vmixCfg.global.net.wan.ipv4.range; - wanCfg = netCfg.wan // { ipv4.range = vethIPv4RangeForWan; lanRanges = builtins.map (lan: lan.ipv4.range) (lib.attrValues netCfg.lans); }; - in - (mkLansService netCfg.name wanCfg netCfg.lans) - // (mkWanService netCfg.name wanCfg) - // (lib.concatMapAttrs (mkMacvlanService netCfg.name) netCfg.bridges.macvlans); - - networkNames = builtins.attrNames vmixCfg.networks; - - networkServices = pkgs.unstable.lib.mergeAttrsList (lib.imap0 (index: networkName: (mkNetworkServices networkName (vmixCfg.networks.${networkName} // { inherit index;}))) networkNames); - networkTargets = lib.concatMapAttrs (networkName: netCfg: { - "net.vmix@${networkName}" = { - description = "Network ${networkName} for vmix"; - bindsTo = [ "ns.net.vmix@${networkName}.service" "lans.net.vmix@${networkName}.service" "wan.net.vmix@${networkName}.service" ]; - }; - }) vmixCfg.networks; -in -{ - config.systemd.services = namespaceGlobalService // networkServices; - config.systemd.targets = networkTargets; - config.boot.kernel.sysctl."net.ipv4.ip_forward" = 1; -} diff --git a/nixos/network/options.nix b/nixos/network/options.nix deleted file mode 100644 index 5e98b57..0000000 --- a/nixos/network/options.nix +++ /dev/null @@ -1,186 +0,0 @@ -{ config, pkgs, lib, vmixLib, ... }: -with lib; -with vmixLib.network; -{ - options = { - bridges.macvlans = mkOption { - type = types.attrsOf (types.submodule { - options = { - uplink.iface = mkOption { - type = types.str; - }; - - uplink.namespace = mkOption { - type = types.nullOr types.str; - default = null; - }; - - namespace = mkOption { - type = types.nullOr types.str; - default = null; - }; - - ipv4.static.address = mkOption { - type = types.nullOr (types.strMatching regex.ipOrCidr4); - default = null; - }; - - ipv4.static.gateway = mkOption { - type = types.nullOr (types.strMatching regex.ipv4); - default = null; - }; - - ipv4.dhcp.client = mkOption { - type = types.bool; - default = false; - }; - - ipv4.dhcp.gateway = mkOption { - type = types.bool; - default = false; - }; - }; - }); - }; - - bridges.macvtaps = mkOption { - type = types.attrsOf (types.submodule { - options = { - uplink.iface = mkOption { - type = types.str; - }; - - uplink.namespace = mkOption { - type = types.nullOr types.str; - default = null; - }; - - many = mkOption { - type = types.bool; - default = true; - }; - }; - }); - }; - - wan = { - enable = mkOption { - type = types.bool; - default = true; - }; - - dns.nameservers = mkOption { - type = types.listOf (types.strMatching regex.ipv4); - default = []; - description = "List of IP Addresses of DNS servers to use as upstream DNS servers in the DHCP/DNS server. If left empty, it will use host's DNS servers"; - }; - - dns.useHostResolvConf = mkOption { - type = types.bool; - default = true; - description = "Whether to use host's /etc/resolv.conf for upstream DNS queries."; - }; - - host.wan.enable = mkOption { - type = types.bool; - default = true; - }; - - host.wan.masquerade = mkOption { - type = types.bool; - default = true; - }; - - host.lan.enable = mkOption { - type = types.bool; - default = true; - }; - - host.lan.masquerade = mkOption { - type = types.bool; - default = true; - }; - - host.self.enable = mkOption { - type = types.bool; - default = true; - }; - - host.self.dns.addNSLansResolver = mkOption { - type = types.bool; - default = true; - }; - - host.self.addNSLansRoutes = mkOption { - type = types.bool; - default = true; - }; - }; - - lans = mkOption { - type = types.attrsOf (types.submodule { - options.domain = mkOption { - type = types.nullOr types.str; - default = null; - description = "Domain name for the hosts of this lan."; - }; - - options.ipv4 = { - range = mkOption { - type = types.strMatching regex.cidr4; - description = "IPv4 Range in x.x.x.x/y format to be assigned to the network."; - }; - - address = mkOption { - type = types.nullOr (types.strMatching regex.ipv4); - default = null; - description = "IPv4 address to attach to the bridge interface of this Lan."; - }; - - dhcp.enable = mkOption { - type = types.bool; - default = true; - description = "Whether to start a DHCP server within this network."; - }; - - dhcp.startAddress = mkOption { - type = types.nullOr (types.strMatching regex.ipv4); - description = "Starting IP Address for DHCP clients."; - default = null; - }; - - dhcp.endAddress = mkOption { - type = types.nullOr (types.strMatching regex.ipv4); - description = "Ending IP Address for DHCP clients."; - default = null; - }; - - dhcp.dns.resolver.enable = mkOption { - type = types.bool; - default = true; - description = "Add dnsmasq's built in resolver to lan clients DHCP responses"; - }; - - dhcp.dns.nameservers = mkOption { - type = types.listOf (types.strMatching regex.ipv4); - default = []; - description = "List of IP Addresses of DNS servers to use as upstream DNS servers in the DHCP/DNS server. If left empty, it will use host's DNS servers"; - }; - - dhcp.dns.zonefiles = mkOption { - default = null; - description = "Additional zonefiles to add for the DNS server"; - }; - }; - }); - }; - -# routes.internal.add = mkOption { -# description = "Additional routes to add on the internal network"; -# }; - -# routes.host.add = mkOption { -# description = "Addtional routes to add on the host's network namespace"; -# }; - }; -} \ No newline at end of file diff --git a/nixos/networks/config.nix b/nixos/networks/config.nix new file mode 100644 index 0000000..faaafba --- /dev/null +++ b/nixos/networks/config.nix @@ -0,0 +1,290 @@ +{ config, pkgs, lib, vmixLib, ... }: +with vmixLib.network; +let + vmixCfg = config.vmix; + # creates a /30 network from available range for veth-pair wan interfaces + mkVethIPv4Range = index: availableIPv4Range: + let + vethIPv4RangeLength = 30; + in + (calc.cidr.subnet (vethIPv4RangeLength - (calc.cidr.length availableIPv4Range)) index availableIPv4Range); + + namespaceGlobalService = { + "ns.net.vmix@" = { + description = "network namespace %I for vmix"; + before = [ "network.target" ]; + path = with pkgs; [ iproute2 utillinux ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + PrivateMounts = false; + PrivateNetwork = true; + ExecStart = (pkgs.writeShellScript "ns.net.vmix-start" '' + spaceName="$1.vmix" + ip netns add $spaceName + umount /var/run/netns/$spaceName + mount --bind /proc/self/ns/net /var/run/netns/$spaceName + '') + " %I"; + ExecStop = "${pkgs.iproute2}/bin/ip netns del %I.vmix"; + }; + }; + }; + + mkLanDomainName = spaceName: lanName: lanCfg: + if (lanCfg.domain != null) then lanCfg.domain else "${lanName}.${spaceName}.vmix"; + + mkLan = spaceName: staticRoutes: wanCfg: lanName: cfg: + let + lanCfg = cfg // { name = lanName; spaceName = "${spaceName}"; }; + lanInterfaceName = "brx-${lanCfg.name}"; + lanInterfaceIPAddress = + if (lanCfg.ipv4.address != null) + then lanCfg.ipv4.address + else (calc.cidr.host 1 lanCfg.ipv4.range); + netmask = calc.cidr.netmask lanCfg.ipv4.range; + networkPrefix = builtins.elemAt (lib.splitString "/" lanCfg.ipv4.range) 1; + + dhcpStartAddress = + if (lanCfg.ipv4.dhcp.startAddress != null) + then lanCfg.ipv4.dhcp.startAddress + else (calc.cidr.host 2 lanCfg.ipv4.range); + + dhcpEndAddress = + if (lanCfg.ipv4.dhcp.endAddress != null) + then lanCfg.ipv4.dhcp.endAddress + else (calc.cidr.host ((calc.cidr.capacity lanCfg.ipv4.range) - 2) lanCfg.ipv4.range); + + createLanInterface = '' + ip link add ${lanInterfaceName} type bridge + ip address add ${lanInterfaceIPAddress}/${networkPrefix} dev ${lanInterfaceName} + ip link set ${lanInterfaceName} up + ''; + + deleteLanInterface = '' + ip link del ${lanInterfaceName} + ''; + + lanDomainName = mkLanDomainName spaceName lanName lanCfg; + + lanDnsmasqConf = '' + # lan ${lanName} + domain=${lanDomainName},${lanInterfaceName} + '' + + (lib.optionalString lanCfg.ipv4.dhcp.enable '' + dhcp-range=${lanInterfaceName},${dhcpStartAddress},${dhcpEndAddress},${netmask},12h + dhcp-option=${lanInterfaceName},option:classless-static-route,${lib.concatMapStringsSep "," (route: "${route},${lanInterfaceIPAddress}") ([ "0.0.0.0/0" ] ++ (builtins.filter (route: route != lanCfg.ipv4.range) staticRoutes))} + '') + + (lib.optionalString (lanCfg.ipv4.dhcp.enable && (!wanCfg.dns.resolver.enable) && (lanCfg.ipv4.dhcp.dns.addresses != null) && (lanCfg.ipv4.dhcp.dns.addresses != [])) + ("dhcp-option=${lanInterfaceName},option:dns-server,${(lib.concatStringsSep "," lanCfg.ipv4.dhcp.dns.addresses)}\n")); + + staticIPsListedOnVMs = with lib; concatMapAttrs (vmName: vmCfg: optionalAttrs ((vmCfg ? networks.lans.${lanName}.ip) && (vmCfg.networks.lans.${lanName}.ip != null)) { ${vmCfg.networks.lans.${lanName}.mac} = vmCfg.networks.lans.${lanName}.ip; }) config.vmix.namespaces.${spaceName}.vms; + in + lanCfg // { + createIface = createLanInterface; + deleteIface = deleteLanInterface; + dnsmasqConf = lanDnsmasqConf; + staticHostsFileContents = (with lib; concatStringsSep "\n" (mapAttrsToList (mac: ipv4: "${mac},${ipv4},infinite") (lanCfg.ipv4.dhcp.statics // staticIPsListedOnVMs))); + domain = lanDomainName; + }; + + mkLansService = spaceName: wanCfg: lansCfg: + let + lansTmpDir = "/tmp/vmix/${spaceName}"; + dhcpDynamicLeaseFileForDnsmasq="${lansTmpDir}/lans.dhcp.dynamic.leases"; + dhcpStaticHostsFileForDnsmasq="${lansTmpDir}/lans.dhcp.static.hosts"; + staticRoutes = [ wanCfg.ipv4.range ] ++ (builtins.map (lanCfg: lanCfg.ipv4.range) (lib.attrValues lansCfg)); + lansList = lib.attrValues(lib.mapAttrs (mkLan spaceName staticRoutes wanCfg) lansCfg); + dnsmasqConf = pkgs.writeText "dnsmasq-${spaceName}.conf" ('' + dhcp-host=*:*:*:*:*:*,id:* + except-interface=lo + dhcp-authoritative + ${lib.optionalString (!wanCfg.dns.resolver.enable) "port=0"} + localise-queries + no-hosts + expand-hosts + dhcp-leasefile=${dhcpDynamicLeaseFileForDnsmasq} + dhcp-hostsfile=${dhcpStaticHostsFileForDnsmasq} + filter-AAAA + address=/host/${calc.cidr.host 1 wanCfg.ipv4.range} + ${lib.optionalString (wanCfg.dns.resolver.enable && wanCfg.dns.resolver.useHostResolvConf) "no-resolv"} + ${lib.concatMapStringsSep "\n" (nameserver: "server=${nameserver}") (lib.optionals wanCfg.dns.resolver.enable wanCfg.dns.resolver.upstream)} + '' + (lib.concatMapStrings (lan: lan.dnsmasqConf) lansList) + ); + + createLansInterfaces = pkgs.writeShellScript "create-lans-${spaceName}-vmix" ('' + # for dnsmasq temp files + mkdir -m 700 -p /tmp/vmix/${spaceName} + '' + (lib.concatMapStrings (lan: lan.createIface) lansList) + ); + + deleteLansInterfaces = pkgs.writeShellScript "delete-lans-${spaceName}-vmix" ('' + # for dnsmasq temp files + rm -rf /tmp/vmix/${spaceName} + '' + lib.concatMapStrings (lan: lan.deleteIface) lansList + ); + + staticHostsFile = pkgs.writeText "dnsmasq-${spaceName}-static-hostsfile" (lib.concatMapStringsSep "\n" (lan: lan.staticHostsFileContents) lansList); + in + { + "lans.net.vmix@${spaceName}" = rec { + bindsTo = [ "ns.net.vmix@${spaceName}.service" ]; + after = bindsTo ++ [ "lans.static-leases-hostsfile.net.vmix@${spaceName}.service" ]; + wantedBy = [ "net.vmix@${spaceName}.target" ]; + unitConfig.JoinsNamespaceOf = "ns.net.vmix@${spaceName}.service"; + path = with pkgs; [ iproute2 ]; + reloadTriggers = [ staticHostsFile ]; + serviceConfig = { + ExecStartPre = createLansInterfaces; + ExecStart = "${pkgs.dnsmasq}/bin/dnsmasq -d -C ${dnsmasqConf}"; + ExecReload = pkgs.writeShellScript "reload-dnsmasq" "kill -HUP $MAINPID"; + ExecStopPost = deleteLansInterfaces; + Restart = "on-failure"; + RestartSec = "5"; + PrivateTmp = true; + ProtectSystem = true; + ProtectHome = true; + PrivateNetwork = true; + }; + }; + + "lans.static-leases-hostsfile.net.vmix@${spaceName}" = rec { + bindsTo = [ "ns.net.vmix@${spaceName}.service" ]; + after = bindsTo; + requiredBy = [ "sysinit-reactivation.target" ]; + before = requiredBy; + wantedBy = [ "net.vmix@${spaceName}.target" ]; + unitConfig.JoinsNamespaceOf = "ns.net.vmix@${spaceName}.service"; + serviceConfig = { + Type = "oneshot"; + ExecCondition = pkgs.writeShellScript "check-if-symlink-already-present.sh" "! [ ${staticHostsFile} -ef ${dhcpStaticHostsFileForDnsmasq} ]"; + ExecStart = pkgs.writeShellScript "create-hostsfile-symlink.sh" "mkdir -m 700 -p ${lansTmpDir}; ln -sf ${staticHostsFile} ${dhcpStaticHostsFileForDnsmasq};"; + Restart = "on-failure"; + RestartSec = "5"; + PrivateTmp = true; + ProtectSystem = true; + ProtectHome = true; + PrivateNetwork = true; + }; + }; + }; + + mkWanService = spaceName: cfg: + let + wanCfg = cfg // { spaceName = spaceName; }; + vethInNSToHost.iface = "vhost"; + vethOnHostToNS.iface = "vn-${wanCfg.spaceName}"; + vethOnHostToNS.ipv4.address = calc.cidr.host 1 wanCfg.ipv4.range; + vethInNSToHost.ipv4.address = calc.cidr.host 2 wanCfg.ipv4.range; + networkPrefix = builtins.elemAt (lib.splitString "/" wanCfg.ipv4.range) 1; + iptablesMark = builtins.toString (ipv4ToInt vethOnHostToNS.ipv4.address); + portForwardRules = lib.concatStringsSep "\n" (lib.mapAttrsToList (hostIPnPort: nsPort: "iptables -t nat -A PREROUTING -p tcp --dport ${hostIPnPort} -j DNAT --to-destination ${vethInNSToHost.ipv4.address}:${toString nsPort}") wanCfg.forwardPorts); + + createWanCommands = '' + ip link add ${vethOnHostToNS.iface} type veth peer name ${vethInNSToHost.iface} + ip link set ${vethInNSToHost.iface} netns ${wanCfg.spaceName}.vmix + + ip address add ${vethOnHostToNS.ipv4.address}/${networkPrefix} dev ${vethOnHostToNS.iface} + ip netns exec ${wanCfg.spaceName}.vmix ip address add ${vethInNSToHost.ipv4.address}/${networkPrefix} dev ${vethInNSToHost.iface} + + iptables -A FORWARD -i ${vethOnHostToNS.iface} -j ACCEPT + iptables -A FORWARD -o ${vethOnHostToNS.iface} -j ACCEPT + ${lib.optionalString (!wanCfg.host.reachable) "iptables -I INPUT 1 -i ${vethOnHostToNS.iface} -j DROP"} + + ${lib.optionalString wanCfg.masquerade "iptables -t mangle -A PREROUTING -i ${vethOnHostToNS.iface} -j MARK --set-mark ${iptablesMark}"} + ${lib.optionalString wanCfg.masquerade "iptables -t nat -A POSTROUTING -m mark --mark ${iptablesMark} -j MASQUERADE"} + + ${portForwardRules} + + ip link set ${vethOnHostToNS.iface} up + ip netns exec ${wanCfg.spaceName}.vmix ip link set ${vethInNSToHost.iface} up + ip netns exec ${wanCfg.spaceName}.vmix ip r add default via ${vethOnHostToNS.ipv4.address} + + ${lib.optionalString wanCfg.host.addNSLansRoutes (lib.concatMapStrings (lanRange: "ip r add ${lanRange} via ${vethInNSToHost.ipv4.address} \n") wanCfg.lanRanges)} + ''; + + createWan = pkgs.writeShellScript "create-wan-${wanCfg.spaceName}-vmix" createWanCommands; + + deleteWan = + let + createdIptablesRules = lib.filter (line: (lib.hasPrefix "iptables" line)) (lib.splitString "\n" createWanCommands); + delIptablesRules = builtins.map ( + rule: lib.replaceStrings [ "-I INPUT 1" "-A" ] [ "-D INPUT" "-D" ] rule + ) createdIptablesRules; + in + pkgs.writeShellScript "delete-wan-${wanCfg.spaceName}-vmix" ('' + ip link del ${vethOnHostToNS.iface} + '' + (lib.concatStringsSep "\n" delIptablesRules)); + in + { + "wan.net.vmix@${wanCfg.spaceName}" = rec { + bindsTo = [ "ns.net.vmix@${wanCfg.spaceName}.service" ]; + after = bindsTo; + wantedBy = [ "net.vmix@${wanCfg.spaceName}.target" ]; + path = with pkgs; [ iproute2 iptables ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = createWan; + ExecStop = deleteWan; + }; + }; + }; + + mkVmGraphicsForwardPorts = spaceName: + let + vms = vmixCfg.namespaces.${spaceName}.vms; + + vmGraphicsPortMappings = lib.concatLists (lib.mapAttrsToList (vmName: vmCfg: + (lib.optional (vmCfg.vnc.enable && vmCfg.vnc.forwardHostPort != null) { + hostPort = toString vmCfg.vnc.forwardHostPort; + nsPort = vmCfg.vnc.port; + reason = "vnc"; + inherit vmName; + }) + ++ (lib.optional (vmCfg.spice.enable && vmCfg.spice.forwardHostPort != null) { + hostPort = toString vmCfg.spice.forwardHostPort; + nsPort = vmCfg.spice.port; + reason = "spice"; + inherit vmName; + }) + ) vms); + + addForwardPortMapping = acc: mapping: + if builtins.hasAttr mapping.hostPort acc then + throw "Duplicate vmix auto-forward host port ${mapping.hostPort} in namespace '${spaceName}' (at least '${mapping.vmName}' ${mapping.reason} conflicts with another VM mapping)." + else + acc // { ${mapping.hostPort} = mapping.nsPort; }; + in + builtins.foldl' addForwardPortMapping {} vmGraphicsPortMappings; + + mkNetworkServices = spaceName: cfg: + let + netCfg = cfg // { name = spaceName; }; + vethIPv4RangeForWan = mkVethIPv4Range netCfg.index vmixCfg.global.net.wan.ipv4.range; + vmGraphicsForwardPorts = mkVmGraphicsForwardPorts spaceName; + manualForwardPorts = netCfg.wan.forwardPorts; + wanCfg = netCfg.wan // { + ipv4.range = vethIPv4RangeForWan; + lanRanges = builtins.map (lan: lan.ipv4.range) (lib.attrValues netCfg.lans); + forwardPorts = vmGraphicsForwardPorts // manualForwardPorts; + }; + in + (mkLansService netCfg.name wanCfg netCfg.lans) + // (lib.optionalAttrs wanCfg.enable (mkWanService netCfg.name wanCfg)); + + spaceNames = builtins.attrNames vmixCfg.namespaces; + + networkServices = lib.mergeAttrsList (lib.imap0 (index: spaceName: (mkNetworkServices spaceName (vmixCfg.namespaces.${spaceName}.networks // { inherit index;}))) spaceNames); + networkTargets = lib.mergeAttrsList (builtins.map (spaceName: { + "net.vmix@${spaceName}" = { + description = "Network ${spaceName} for vmix"; + bindsTo = [ "ns.net.vmix@${spaceName}.service" "lans.net.vmix@${spaceName}.service" ] + ++ lib.optionals vmixCfg.namespaces.${spaceName}.networks.wan.enable [ "wan.net.vmix@${spaceName}.service" ]; + }; + }) spaceNames); +in +{ + config.systemd.services = namespaceGlobalService // networkServices; + config.systemd.targets = networkTargets; + config.boot.kernel.sysctl."net.ipv4.ip_forward" = lib.mkDefault 1; +} diff --git a/nixos/network/default.nix b/nixos/networks/default.nix similarity index 69% rename from nixos/network/default.nix rename to nixos/networks/default.nix index 7a98a6b..267f5d7 100644 --- a/nixos/network/default.nix +++ b/nixos/networks/default.nix @@ -6,11 +6,5 @@ with lib; default = "172.27.72.0/24"; # enough to create 64x /30 networks for veth pairs used for wan interfaces }; - options.vmix.networks = mkOption { - type = types.attrsOf - (types.submodule (import ./options.nix args)); - default = { }; - }; - imports = [ (import ./config.nix args) ]; } \ No newline at end of file diff --git a/nixos/networks/options.nix b/nixos/networks/options.nix new file mode 100644 index 0000000..a3cb8e5 --- /dev/null +++ b/nixos/networks/options.nix @@ -0,0 +1,137 @@ +{ lib, vmixLib, ... }: +with lib; +with vmixLib.network; +{ + macvtaps = mkOption { + description = "Macvtap network definitions available to VMs in this namespace."; + type = types.attrsOf (types.submodule { + options = { + uplink.iface = mkOption { + type = types.str; + description = "Host interface name to attach the macvtap device to."; + }; + + uplink.namespace = mkOption { + type = types.nullOr types.str; + default = null; + description = "Optional network namespace where the uplink interface exists. Null means the host namespace."; + }; + }; + }); + }; + + wan = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable forwarding traffic to and from the namespace to the rest of the networks on the host including the internet. (iptables FORWARD chain on the host)"; + }; + + masquerade = mkOption { + type = types.bool; + default = true; + description = "Masquerade outgoing traffic using host's IP"; + }; + + host.reachable = mkOption { + type = types.bool; + default = true; + description = "Allow talking to the host itself from the namespace and VMs on the lan (iptables INPUT chain on the host)"; + }; + + host.addNSLansRoutes = mkOption { + type = types.bool; + default = true; + description = "add routes to the LAN on host so the vms are reachable from the host"; + }; + + # host.dns.addNSLansResolver = mkOption { + # type = types.bool; + # default = true; + # }; + + dns.resolver.enable = mkOption { + type = types.bool; + default = true; + description = "Add dnsmasq's built in resolver to lan clients DHCP responses"; + }; + dns.resolver.useHostResolvConf = mkOption { + type = types.bool; + default = false; + description = "Use host's resolvconf for upstreaming dns queries"; + }; + + dns.resolver.upstream = mkOption { + type = types.listOf (types.strMatching regex.ipv4); + default = []; + description = "Upstream DNS servers for dnsmasq's built in resolver"; + }; + + forwardPorts = mkOption { + type = types.attrsOf types.int; + default = {}; + description = "Map host TCP port to namespace destination TCP port."; + }; + }; + + lans = mkOption { + description = "Layer-2 LAN bridge networks and DHCP settings for the namespace."; + type = types.attrsOf (types.submodule { + options.domain = mkOption { + type = types.nullOr types.str; + default = null; + description = "Domain name for the hosts of this lan."; + }; + + options.ipv4 = { + range = mkOption { + type = types.strMatching regex.cidr4; + description = "IPv4 Range in x.x.x.x/y format to be assigned to the network."; + }; + + address = mkOption { + type = types.nullOr (types.strMatching regex.ipv4); + default = null; + description = "IPv4 address to attach to the bridge interface of this Lan."; + }; + + dhcp.enable = mkOption { + type = types.bool; + default = true; + description = "Whether to start a DHCP server within this network."; + }; + + dhcp.startAddress = mkOption { + type = types.nullOr (types.strMatching regex.ipv4); + description = "Starting IP Address for DHCP clients."; + default = null; + }; + + dhcp.endAddress = mkOption { + type = types.nullOr (types.strMatching regex.ipv4); + description = "Ending IP Address for DHCP clients."; + default = null; + }; + + dhcp.dns.addresses = mkOption { + type = types.nullOr (types.listOf (types.strMatching regex.ipv4)); + description = "List of IP Addresses to pass as DNS servers in the DHCP response. These servers are only passed if dnsmasq's built in resolver is not enabled via wan.dns.resolver.enable"; + }; + + dhcp.statics = mkOption { + description = "Static IP leases for mac addresses"; + type = types.attrsOf (types.strMatching regex.ipv4); + default = {}; + }; + }; + }); + }; + + # routes.internal.add = mkOption { + # description = "Additional routes to add on the internal network"; + # }; + + # routes.host.add = mkOption { + # description = "Addtional routes to add on the host's network namespace"; + # }; +} diff --git a/nixos/vm/config.nix b/nixos/vm/config.nix deleted file mode 100644 index d5ea82f..0000000 --- a/nixos/vm/config.nix +++ /dev/null @@ -1,150 +0,0 @@ -{ config, pkgs, lib, vmixLib, ... }: -with lib; -with vmixLib.network; -let - vmixCfg = config.vmix; - - mkServices4aVM = name: cfg: - let - vmCfg = cfg // { inherit name; }; - netName = head (attrNames vmCfg.network.vmix); - netCfg = vmCfg.network.vmix.${netName} // { name = netName; }; - - mkTap4aLan = lanName: tapCfg: - let - tapInterfaceName = "vt-${vmCfg.name}-${lanName}"; - lanInterfaceName = "brx-${lanName}"; - in - { - name = lanName; - iface = tapInterfaceName; - mac = tapCfg.mac; - create = '' - ip tuntap add dev ${tapInterfaceName} mode tap - ip link set dev ${tapInterfaceName} up - ip link set dev ${tapInterfaceName} master ${lanInterfaceName} - ''; - delete = '' - ip link del ${tapInterfaceName} - ''; - }; - - mkMacvtap = macvtapName: macvtapVmCfg: - let - macvtapNetworkCfg = config.vmix.networks.${netCfg.name}.bridges.macvtaps.${macvtapName}; - macvtapInterfaceName = "mt-${vmCfg.name}-${macvtapNetworkCfg.uplink.iface}"; - in - { - name = macvtapName; - iface = macvtapInterfaceName; - mac = macvtapVmCfg.mac; - create = '' - ip link add link ${macvtapNetworkCfg.uplink.iface} name ${macvtapInterfaceName} type macvtap mode bridge - ${lib.optionalString (macvtapVmCfg.mac != null) "ip link set dev ${macvtapInterfaceName} address ${macvtapVmCfg.mac}"} - ip link set ${macvtapInterfaceName} netns ${netName}.vmix - ip netns exec ${netName}.vmix ip link set dev ${macvtapInterfaceName} up - ''; - delete = '' - ip netns exec ${netName}.vmix ip link del ${macvtapInterfaceName} - ''; - }; - - allTaps = (mapAttrsToList mkTap4aLan netCfg.lans); - allMacvtaps = (mapAttrsToList mkMacvtap netCfg.macvtaps); - - createTapsforLansScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( - concatStringsSep "\n" (builtins.map (tap: tap.create) allTaps) - ); - - deleteTapsforLansScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( - concatStringsSep "\n" (builtins.map (tap: tap.delete) allTaps) - ); - - createMacvapsScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( - concatStringsSep "\n" (builtins.map (macvtap: macvtap.create) allMacvtaps) - ); - - deleteMacvapsScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( - concatStringsSep "\n" (builtins.map (macvtap: macvtap.delete) allMacvtaps) - ); - - osImage = vmixLib.customizeImage vmCfg.disks.os.file { - name = vmCfg.name; - commands = '' - truncate /etc/machine-id - run-command systemd-machine-id-setup - run-command ssh-keygen -A - ''; - }; - - qemuStartVMScript = pkgs.writeShellScript "${vmCfg.name}-qemu-vmix" '' - exec qemu-system-${vmCfg.arch} \ - -nographic \ - ${optionalString vmCfg.kvm "-accel kvm"} \ - -name ${vmCfg.name} \ - -m ${toString vmCfg.mem.size} \ - -smp cores=${toString vmCfg.cpu.cores} \ - -cpu ${vmCfg.cpu.model} \ - -machine type=${vmCfg.pc.type} \ - ${optionalString vmCfg.bios.efi "-bios ${pkgs.OVMF.fd}/FV/OVMF.fd"} \ - ${optionalString vmCfg.bios.tpm "-chardev socket,id=chrtpm,path=/tmp/mytpm-sock -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"} \ - -drive file=${toString osImage},format=qcow2,if=virtio${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"} \ - ${optionalString (vmCfg.disks.iso.file != null) "-drive file=${toString vmCfg.disks.iso.file},media=cdrom,readonly=on"} \ - ${concatMapStrings (diskCfg: '' - -drive file=${diskCfg.file},format=qcow2,if=${toString vmCfg.disks.bus} \ - '') (attrValues vmCfg.disks.add)} \ - ${concatMapStrings (shareCfg: '' - -virtfs local,path=${toString shareCfg.source},security_model=passthrough,mount_tag=${shareCfg.target} \ - '') (attrValues vmCfg.shares)} \ - ${concatMapStrings (tapCfg: '' - -device virtio-net-pci,netdev=lan-${tapCfg.name},mac=${tapCfg.mac} \ - -netdev tap,id=lan-${tapCfg.name},ifname=${tapCfg.iface},script=no,downscript=no \ - '') allTaps} \ - ${optionalString cfg.network.user.enable " - -netdev user,id=user \ - -device virtio-net-pci,netdev=user \ - "} \ - ${optionalString (vmCfg.boot.menu == true) "-boot menu=on"} \ - ${concatStrings (imap1 (i: macvtap: '' - -device virtio-net-pci,netdev=macvtap-${macvtap.name},mac=$(ip l show ${macvtap.iface} | awk '/link\/ether/{print $2}') \ - -netdev tap,id=macvtap-${macvtap.name},fd=${toString (i+2)} ${toString (i+2)}<>/dev/tap$(ip l show ${macvtap.iface} | awk -F':' '/${macvtap.iface}/{print $1}') \ - '') allMacvtaps)} \ - #${optionalString (length vmCfg.boot.order > 0) "-boot order=${concatStringsSep "," vmCfg.boot.order}"} \ - ''; - in - { - "vm.vmix@${vmCfg.name}" = rec { - bindsTo = [ "net.vmix@${netCfg.name}.target" "macvtaps.vm.vmix@${vmCfg.name}.service" ]; - unitConfig.JoinsNamespaceOf = "ns.net.vmix@${netCfg.name}.service"; - after = bindsTo; - path = with pkgs; [ iproute2 qemu gawk ]; - serviceConfig = { - ExecStartPre = createTapsforLansScript; - ExecStart = qemuStartVMScript; - ExecStopPost = deleteTapsforLansScript; - PrivateTmp = true; - ProtectSystem = true; - ProtectHome = true; - PrivateNetwork = true; - }; - }; - - "macvtaps.vm.vmix@${vmCfg.name}" = rec { - bindsTo = [ "net.vmix@${netCfg.name}.target" ]; - after = bindsTo; - partOf = [ "vm.vmix@${vmCfg.name}.service" ]; - path = with pkgs; [ iproute2 ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = createMacvapsScript; - ExecStop = deleteMacvapsScript; - }; - }; - }; - - vmServices = concatMapAttrs mkServices4aVM vmixCfg.vms; -in -{ - config.systemd.services = vmServices; -} \ No newline at end of file diff --git a/nixos/vm/default.nix b/nixos/vm/default.nix deleted file mode 100644 index 8ed8f96..0000000 --- a/nixos/vm/default.nix +++ /dev/null @@ -1,11 +0,0 @@ -args@{ config, pkgs, lib, vmixLib, ... }: -with lib; -{ - options.vmix.vms = mkOption { - type = types.attrsOf - (types.submodule (import ./options.nix args)); - default = { }; - }; - - imports = [ (import ./config.nix args) ]; -} \ No newline at end of file diff --git a/nixos/vm/options.nix b/nixos/vm/options.nix deleted file mode 100644 index 4055bde..0000000 --- a/nixos/vm/options.nix +++ /dev/null @@ -1,167 +0,0 @@ -{ config, pkgs, lib, ... }: -with lib; -{ - options = { - cpu.cores = mkOption { - type = types.int; - default = 2; - description = "Number of CPU cores."; - }; - cpu.model = mkOption { - type = types.str; - default = "host"; - description = "CPU model."; - }; - kvm = mkOption { - type = types.bool; - default = true; - description = "Enable KVM."; - }; - arch = mkOption { - type = types.str; - default = "x86_64"; - description = "Architecture of the VM."; - }; - pc.type = mkOption { - type = types.str; - default = "q35"; - description = "PC type."; - }; - bios.efi = mkOption { - type = types.bool; - default = true; - description = "Enable EFI BIOS."; - }; - bios.tpm = mkOption { - type = types.bool; - default = false; - description = "Enable TPM BIOS."; - }; - mem.size = mkOption { - type = types.int; - default = 1024; - description = "Memory size in MB."; - }; - mem.balloon = mkOption { - type = types.bool; - default = false; - description = "Enable memory ballooning."; - }; - disks.os.file = mkOption { - type = types.path; - description = "Path to the OS disk image."; - }; - disks.os.persist = mkOption { - type = types.bool; - default = false; - description = "Persist OS disk changes."; - }; - disks.iso.file = mkOption { - type = types.nullOr types.path; - description = "Path to the ISO file."; - default = null; - }; - disks.add = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - file = mkOption { - type = types.path; - description = "Path to the additional disk."; - }; - mounts = mkOption { - type = types.attrsOf types.str; - description = "Mount points for the additional disk."; - }; - opts = mkOption { - type = types.str; - description = "additional options in QEMU args for this disk"; - }; - }; - }); - description = "Additional disks."; - }; - shares = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - source = mkOption { - type = types.path; - description = "Source path for the shared directory."; - }; - target = mkOption { - type = types.str; - description = "Target path inside the VM for the shared directory."; - }; - }; - }); - description = "Shared directories."; - }; - - disks.bus = mkOption { - type = types.str; - default = "virtio"; - description = "Bus type for the disks."; - }; - boot.order = mkOption { - type = types.listOf types.str; - description = "Boot order."; - default = [ "os" "iso" ]; - }; - boot.menu = mkOption { - type = types.bool; - default = false; - description = "Enable boot menu."; - }; - - network.user.enable = mkOption { - type = types.bool; - default = false; - description = "enable qemu user networking"; - }; - - network.vmix = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - lans = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - enable = mkOption { - type = types.bool; - default = false; - description = "Enable the LAN interface."; - }; - mac = mkOption { - type = types.str; - description = "MAC address for the LAN interface."; - }; - }; - }); - description = "LAN interfaces."; - }; - macvtaps = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - enable = mkOption { - type = types.bool; - default = false; - description = "Enable the MACVTap interface."; - }; - mac = mkOption { - type = types.nullOr types.str; - default = null; - description = "MAC address for the MACVTap interface."; - }; - }; - }); - description = "MACVTap interfaces."; - }; - }; - }); - description = "Network interfaces."; - }; - }; -} \ No newline at end of file diff --git a/nixos/vms/config.nix b/nixos/vms/config.nix new file mode 100644 index 0000000..cae926d --- /dev/null +++ b/nixos/vms/config.nix @@ -0,0 +1,272 @@ +{ config, pkgs, lib, vmixLib, ... }: +with lib; +with vmixLib.network; +let + vmixCfg = config.vmix; + mkShortIfaceName = prefix: seed: "${prefix}-${builtins.substring 0 8 (builtins.hashString "sha256" seed)}"; + + mkServices4aVMInNamespace = spaceName: vmName: cfg: + let + vmCfg = cfg // { name = vmName; }; + netCfg = vmCfg.networks; + + mkTap4aLan = lanName: tapCfg: + let + tapInterfaceName = mkShortIfaceName "vt" "${spaceName}-${vmCfg.name}-${lanName}"; + lanInterfaceName = "brx-${lanName}"; + in + { + name = lanName; + iface = tapInterfaceName; + mac = tapCfg.mac; + create = '' + ip tuntap add dev ${tapInterfaceName} mode tap + ip link set dev ${tapInterfaceName} up + ip link set dev ${tapInterfaceName} master ${lanInterfaceName} + '' + lib.optionalString (tapCfg.ip != null) '' + # Make the static ip somehow part of the script so nixOs thinks the service has changed when the IP changes, which will trigger a VM restart. + # So whenever IP changes, VM will restart automatically + # The IP is actually bein assigned to VM by dnsmasq, when VM requests it via DHCP + # Static IP - ${tapCfg.ip} + ''; + delete = '' + ip link del ${tapInterfaceName} + ''; + }; + + mkMacvtap = macvtapName: macvtapVmCfg: + let + macvtapNetworkCfg = config.vmix.namespaces.${spaceName}.networks.macvtaps.${macvtapName}; + macvtapInterfaceName = mkShortIfaceName "mt" "${spaceName}-${vmCfg.name}-${macvtapNetworkCfg.uplink.iface}-${macvtapName}"; + uplinkNamespaceArg = lib.optionalString (macvtapNetworkCfg.uplink.namespace != null) "-n ${macvtapNetworkCfg.uplink.namespace}"; + in + { + name = macvtapName; + iface = macvtapInterfaceName; + mac = macvtapVmCfg.mac; + + create = '' + ip ${uplinkNamespaceArg} link add link ${macvtapNetworkCfg.uplink.iface} name ${macvtapInterfaceName} type macvtap mode bridge + ${lib.optionalString (macvtapVmCfg.mac != null) "ip ${uplinkNamespaceArg} link set dev ${macvtapInterfaceName} address ${macvtapVmCfg.mac}"} + ip ${uplinkNamespaceArg} link set ${macvtapInterfaceName} netns ${spaceName}.vmix + ip -n ${spaceName}.vmix link set dev ${macvtapInterfaceName} up + ''; + delete = '' + ip -n ${spaceName}.vmix link del ${macvtapInterfaceName} + ''; + }; + + allTaps = (mapAttrsToList mkTap4aLan netCfg.lans); + allMacvtaps = (mapAttrsToList mkMacvtap netCfg.macvtaps); + + createTapsforLansScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( + concatStringsSep "\n" (builtins.map (tap: tap.create) allTaps) + ); + + deleteTapsforLansScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( + concatStringsSep "\n" (builtins.map (tap: tap.delete) allTaps) + ); + + createMacvTapsScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( + concatStringsSep "\n" (builtins.map (macvtap: macvtap.create) allMacvtaps) + ); + + deleteMacvTapsScript = pkgs.writeShellScript "${vmCfg.name}-taps-vmix" ( + concatStringsSep "\n" (builtins.map (macvtap: macvtap.delete) allMacvtaps) + ); + + hasOsDisk = vmCfg.disks.os.file != null; + + # Auto-detect Windows from _vmixOsType marker on the disk image + isWindows = vmCfg.windows.enable || (hasOsDisk && (vmCfg.disks.os.file._vmixOsType or "linux") == "windows"); + + # Linux VMs: apply customizeImage with 9p fstab and machine-id setup + linuxOsImage = vmixLib.linux.customizeImage vmCfg.disks.os.file { + name = vmCfg.name; + commands = '' + truncate /etc/machine-id + run-command systemd-machine-id-setup + run-command ssh-keygen -A + run ${vmixLib.linux.scriptsNFiles.add-9p-mounts-to-fstab vmCfg.shares} + ''; + }; + + # Windows VMs: use disk image as-is (customization done at image build time) + storeImage = if !hasOsDisk then null + else if isWindows then vmCfg.disks.os.file + else linuxOsImage; + + # When persist = true, QEMU needs a mutable disk outside /nix/store. + # The store image is copied to persistPath on first boot. + osDiskPath = if !hasOsDisk then null + else if vmCfg.disks.os.persist then vmCfg.disks.os.persistPath + else toString storeImage; + + # Script to seed the persistent disk from the store image on first boot + seedPersistentDiskScript = pkgs.writeShellScript "${vmCfg.name}-seed-disk-vmix" '' + PERSIST_PATH="${vmCfg.disks.os.persistPath}" + if [ ! -f "$PERSIST_PATH" ]; then + echo "Seeding persistent disk from store image..." + mkdir -p "$(dirname "$PERSIST_PATH")" + cp --no-preserve=mode "${toString storeImage}" "$PERSIST_PATH" + chmod 600 "$PERSIST_PATH" + fi + ''; + + persistExecStartPre = lib.optional (hasOsDisk && vmCfg.disks.os.persist) seedPersistentDiskScript; + + # QEMU expects single-letter boot codes (e.g. c,d,n), while vmix uses readable names. + bootOrderQemu = + let + bootDeviceAliases = { + os = "c"; + iso = "d"; + net = "n"; + floppy = "a"; + }; + in + concatStrings (builtins.map (device: bootDeviceAliases.${device}) vmCfg.boot.order); + + spiceUsbRedirArgs = + if vmCfg.spice.enable && vmCfg.spice.usbRedir.enable then + concatStringsSep " \\\n " ([ + "-device qemu-xhci,id=spice-usb-xhci" + ] ++ (concatMap (i: [ + "-chardev spicevmc,name=usbredir,id=spice-usbredirchardev${toString i}" + "-device usb-redir,chardev=spice-usbredirchardev${toString i},id=spice-usbredirdev${toString i}" + ]) (range 1 vmCfg.spice.usbRedir.channels))) + else + ""; + + vncArgs = concatStringsSep "," ( + [ + "${vmCfg.vnc.addr}:${toString (vmCfg.vnc.port - 5900)}" + "share=${vmCfg.vnc.sharePolicy}" + ] + ++ optional (vmCfg.vnc.websocketPort != null) "websocket=${toString vmCfg.vnc.websocketPort}" + ++ optional (vmCfg.vnc.passwordFile != null) "password-secret=vnc-pass-${vmCfg.name}" + ); + + qemuStartVMScript = pkgs.writeShellScript "${vmCfg.name}-qemu-vmix" '' + ${optionalString vmCfg.vnc.enable '' + ${optionalString (vmCfg.vnc.passwordFile != null) '' + if [ ! -r ${escapeShellArg vmCfg.vnc.passwordFile} ]; then + echo "VNC password file is not readable: ${vmCfg.vnc.passwordFile}" >&2 + exit 1 + fi + if [ ! -s ${escapeShellArg vmCfg.vnc.passwordFile} ]; then + echo "VNC password file is empty: ${vmCfg.vnc.passwordFile}" >&2 + exit 1 + fi + ''} + ''} + ${optionalString vmCfg.spice.enable '' + ${optionalString (vmCfg.spice.passwordFile != null) '' + if [ ! -r ${escapeShellArg vmCfg.spice.passwordFile} ]; then + echo "SPICE password file is not readable: ${vmCfg.spice.passwordFile}" >&2 + exit 1 + fi + if [ ! -s ${escapeShellArg vmCfg.spice.passwordFile} ]; then + echo "SPICE password file is empty: ${vmCfg.spice.passwordFile}" >&2 + exit 1 + fi + ''} + ''} + exec qemu-system-${vmCfg.arch} \ + ${if vmCfg.nographic && vmCfg.pci.passthrough != [] then "-display none -vga none" else optionalString vmCfg.nographic "-nographic"} \ + ${optionalString (vmCfg.vnc.enable && vmCfg.vnc.passwordFile != null) "-object secret,id=vnc-pass-${vmCfg.name},file=${escapeShellArg vmCfg.vnc.passwordFile}"} \ + ${optionalString vmCfg.vnc.enable "-vnc ${vncArgs}"} \ + ${optionalString (vmCfg.spice.enable && vmCfg.spice.passwordFile != null) "-object secret,id=spice-pass-${vmCfg.name},file=${escapeShellArg vmCfg.spice.passwordFile}"} \ + ${optionalString vmCfg.spice.enable "-spice addr=${vmCfg.spice.addr},port=${toString vmCfg.spice.port}${optionalString (vmCfg.spice.passwordFile == null) ",disable-ticketing=on"}${optionalString (vmCfg.spice.passwordFile != null) ",password-secret=spice-pass-${vmCfg.name}"}"} \ + ${optionalString vmCfg.spice.enable "-vga ${vmCfg.spice.displayDevice}"} \ + ${optionalString (vmCfg.spice.enable && vmCfg.spice.agent.enable) "-device virtio-serial-pci -chardev spicevmc,id=vdagent,debug=0,name=vdagent -device virtserialport,chardev=vdagent,name=com.redhat.spice.0"} \ + ${# Guest agent channel — prevents qemu-ga from spinning when virtio-win guest tools are installed + optionalString isWindows "${optionalString (!vmCfg.spice.enable || !vmCfg.spice.agent.enable) "-device virtio-serial-pci"} -chardev socket,path=/tmp/qga-${vmCfg.name}.sock,server=on,wait=off,id=qga0 -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0"} \ + ${spiceUsbRedirArgs} \ + ${optionalString vmCfg.kvm "-accel kvm"} \ + -name ${vmCfg.name} \ + -m ${toString vmCfg.mem.size} \ + ${optionalString vmCfg.mem.balloon "-device virtio-balloon-pci"} \ + -smp cores=${toString vmCfg.cpu.cores} \ + -cpu ${vmCfg.cpu.model}${optionalString vmCfg.cpu.hideVirtualized ",kvm=off,hv_vendor_id=1234567890ab,-hypervisor"} \ + -machine type=${vmCfg.pc.type}${optionalString vmCfg.cpu.hideVirtualized ",kernel_irqchip=on"} \ + ${optionalString vmCfg.bios.efi "-bios ${pkgs.OVMF.fd}/FV/OVMF.fd"} \ + ${optionalString vmCfg.bios.tpm "-chardev socket,id=chrtpm,path=/tmp/mytpm-sock -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"} \ + ${# Windows: localtime RTC, USB tablet for mouse, disable S3/S4 sleep + optionalString isWindows '' + -rtc base=localtime,clock=host \ + -device qemu-xhci -device usb-tablet \ + -global ICH9-LMB.disable_s3=1 -global ICH9-LMB.disable_s4=1 \ + ''} \ + ${optionalString hasOsDisk "-drive file=${osDiskPath},format=qcow2,if=virtio${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"}"} \ + ${optionalString (vmCfg.disks.iso.file != null) "-drive file=${toString vmCfg.disks.iso.file},media=cdrom,readonly=on"} \ + ${concatMapStrings (diskCfg: '' + -drive file=${toString diskCfg.file},format=${diskCfg.format},if=${vmCfg.disks.bus} \ + '') (attrValues vmCfg.disks.add)} \ + ${concatStrings (mapAttrsToList (shareName: shareCfg: '' + -virtfs local,path=${toString shareCfg.source},security_model=passthrough,mount_tag=${shareName} \ + '') vmCfg.shares)} \ + ${optionalString cfg.networks.user.enable " + -netdev user,id=user \ + -device virtio-net-pci,netdev=user \ + "} \ + ${concatMapStrings (tapCfg: '' + -device virtio-net-pci,netdev=lan-${tapCfg.name},mac=${tapCfg.mac} \ + -netdev tap,id=lan-${tapCfg.name},ifname=${tapCfg.iface},script=no,downscript=no \ + '') allTaps} \ + ${concatStrings (imap1 (i: macvtap: '' + -device virtio-net-pci,netdev=macvtap-${macvtap.name},mac=$(ip l show ${macvtap.iface} | awk '/link\/ether/{print $2}') \ + -netdev tap,id=macvtap-${macvtap.name},fd=${toString (i+2)} ${toString (i+2)}<>/dev/tap$(ip l show ${macvtap.iface} | awk -F':' '/${macvtap.iface}/{print $1}') \ + '') allMacvtaps)} \ + ${concatStrings (imap1 (i: pciAddr: '' + -device pcie-root-port,id=pci-passthrough${toString i},chassis=${toString i},slot=${toString i} \ + -device vfio-pci,host=${pciAddr},bus=pci-passthrough${toString i}${optionalString (i == 1) ",x-vga=on${optionalString (vmCfg.pci.romFile != null) ",romfile=${vmCfg.pci.romFile}"}"} \ + '') vmCfg.pci.passthrough)} \ + ${concatMapStrings (usbDev: '' + -device usb-host,vendorid=0x${usbDev.vendorId},productid=0x${usbDev.productId} \ + '') vmCfg.usb.hostDevices} \ + ${optionalString (vmCfg.boot.menu == true) "-boot menu=on"} \ + ${optionalString (length vmCfg.boot.order > 0) "-boot order=${bootOrderQemu}"} \ + ''; + in + lib.optionalAttrs (cfg.enable) { + "vm.vmix@${vmCfg.name}" = rec { + bindsTo = [ "net.vmix@${spaceName}.target" "macvtaps.vm.vmix@${vmCfg.name}.service" ]; + unitConfig.JoinsNamespaceOf = "ns.net.vmix@${spaceName}.service"; + after = bindsTo; + path = with pkgs; [ iproute2 qemu gawk coreutils ]; + serviceConfig = { + ExecStartPre = persistExecStartPre ++ [ createTapsforLansScript ]; + ExecStart = qemuStartVMScript; + ExecStopPost = deleteTapsforLansScript; + PrivateTmp = true; + ProtectSystem = true; + ProtectHome = true; + PrivateNetwork = true; + } // lib.optionalAttrs (vmCfg.pci.passthrough != []) { + # VFIO passthrough needs raw device access — relax sandboxing + ProtectSystem = lib.mkForce false; + SupplementaryGroups = [ "kvm" ]; + }; + wantedBy = lib.mkIf vmCfg.autostart [ "multi-user.target" ]; + }; + + "macvtaps.vm.vmix@${vmCfg.name}" = rec { + bindsTo = [ "net.vmix@${spaceName}.target" ]; + after = bindsTo; + partOf = [ "vm.vmix@${vmCfg.name}.service" ]; + path = with pkgs; [ iproute2 ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = createMacvTapsScript; + ExecStop = deleteMacvTapsScript; + }; + }; + }; + + vmServices = concatMapAttrs (spaceName: namespaceCfg: (concatMapAttrs (mkServices4aVMInNamespace spaceName) namespaceCfg.vms)) vmixCfg.namespaces; +in +{ + config.systemd.services = vmServices; +} diff --git a/nixos/vms/default.nix b/nixos/vms/default.nix new file mode 100644 index 0000000..354b7b7 --- /dev/null +++ b/nixos/vms/default.nix @@ -0,0 +1,5 @@ +args@{ config, pkgs, lib, vmixLib, ... }: +with lib; +{ + imports = [ (import ./config.nix args) ]; +} \ No newline at end of file diff --git a/nixos/vms/submoduleOptions.nix b/nixos/vms/submoduleOptions.nix new file mode 100644 index 0000000..5dcca25 --- /dev/null +++ b/nixos/vms/submoduleOptions.nix @@ -0,0 +1,309 @@ +{ config, pkgs, lib, vmixLib, ... }: +with lib; +{ + options = { + autostart = mkOption { + type = types.bool; + default = false; + description = "Start VM on host boot."; + }; + enable = mkOption { + type = types.bool; + default = true; + description = "Enable/disable VM service creation."; + }; + vnc = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable VNC."; + }; + addr = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "VNC bind address inside the VM namespace."; + }; + port = mkOption { + type = types.ints.between 5900 5999; + default = 5900; + description = "VNC TCP port inside the VM namespace."; + }; + forwardHostPort = mkOption { + type = types.nullOr types.port; + default = null; + description = "Optional host TCP port to auto-forward to this VM VNC port."; + }; + websocketPort = mkOption { + type = types.nullOr types.port; + default = null; + description = "Optional websocket port for VNC (for noVNC-style clients)."; + }; + passwordFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to a runtime file containing the VNC password. When set, VNC auth is enabled via password-secret."; + }; + sharePolicy = mkOption { + type = types.enum [ "allow-exclusive" "force-shared" "ignore" ]; + default = "allow-exclusive"; + description = "VNC client sharing policy."; + }; + }; + spice = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable SPICE display server."; + }; + port = mkOption { + type = types.int; + default = 5930; + description = "SPICE TCP port inside the VM namespace."; + }; + forwardHostPort = mkOption { + type = types.nullOr types.port; + default = null; + description = "Optional host TCP port to auto-forward to this VM SPICE port."; + }; + addr = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "SPICE bind address inside the VM namespace."; + }; + passwordFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to a runtime file containing SPICE password. When set, ticketing is enabled automatically, otherwise ticketing is disabled."; + }; + agent.enable = mkOption { + type = types.bool; + default = true; + description = "Enable SPICE guest agent channel (clipboard/resolution helpers)."; + }; + usbRedir = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable SPICE USB redirection channels."; + }; + channels = mkOption { + type = types.ints.between 1 16; + default = 4; + description = "Number of SPICE USB redirection channels to expose."; + }; + }; + displayDevice = mkOption { + type = types.enum [ "virtio" "qxl" "std" "none" ]; + default = "qxl"; + description = "QEMU -vga type to use with SPICE (qxl, virtio, std, none)."; + }; + }; + nographic = mkOption { + type = types.bool; + default = true; + description = "Run QEMU without a graphical window (-nographic)."; + }; + cpu.cores = mkOption { + type = types.int; + default = 2; + description = "Number of CPU cores."; + }; + cpu.model = mkOption { + type = types.str; + default = "host"; + description = "CPU model."; + }; + cpu.hideVirtualized = mkOption { + type = types.bool; + default = true; + description = "Hide hypervisor from guest. Prevents GPU driver Code 43 errors by stripping hypervisor CPUID leaf."; + }; + kvm = mkOption { + type = types.bool; + default = true; + description = "Enable KVM."; + }; + arch = mkOption { + type = types.str; + default = "x86_64"; + description = "Architecture of the VM."; + }; + pc.type = mkOption { + type = types.str; + default = "q35"; + description = "PC type."; + }; + bios.efi = mkOption { + type = types.bool; + default = true; + description = "Enable EFI BIOS."; + }; + bios.tpm = mkOption { + type = types.bool; + default = false; + description = "Enable TPM BIOS."; + }; + mem.size = mkOption { + type = types.int; + default = 1024; + description = "Memory size in MB."; + }; + mem.balloon = mkOption { + type = types.bool; + default = false; + description = "Enable memory ballooning."; + }; + disks.os.file = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to the OS disk image. Null for ISO-only VMs. For Windows, use a vmixLib.windows.* image — config auto-detects via _vmixOsType metadata."; + }; + disks.os.persist = mkOption { + type = types.bool; + default = false; + description = "Persist OS disk changes. The store image is copied to persistPath on first boot and QEMU writes to that mutable copy."; + }; + disks.os.persistPath = mkOption { + type = types.str; + default = ""; + description = "Mutable path for the persistent OS disk (e.g. /storage/vms/myvm/os.qcow2). Required when persist = true."; + }; + disks.iso.file = mkOption { + type = types.nullOr (types.either types.path types.str); + description = "Path to the ISO file. Can be a Nix store path or a string path to a local file."; + default = null; + }; + disks.add = mkOption { + default = {}; + type = types.attrsOf (types.submodule { + options = { + file = mkOption { + type = types.str; + description = "String literal path to the additional disk."; + }; + format = mkOption { + type = types.str; + description = "raw/qcow2 etc"; + }; + mounts = mkOption { + type = types.attrsOf types.str; + description = "Mount points for the additional disk."; + }; + opts = mkOption { + type = types.str; + description = "additional options in QEMU args for this disk"; + }; + }; + }); + description = "Additional disks."; + }; + shares = mkOption { + default = {}; + type = types.attrsOf (types.submodule { + options = { + source = mkOption { + type = types.path; + description = "Source path for the shared directory."; + }; + target = mkOption { + type = types.str; + description = "Target path inside the VM for the shared directory."; + }; + }; + }); + description = "Shared directories."; + }; + + disks.bus = mkOption { + type = types.str; + default = "virtio"; + description = "Bus type for the disks."; + }; + boot.order = mkOption { + type = types.listOf (types.enum [ "os" "iso" "net" "floppy" ]); + description = "Boot order."; + default = [ "os" "iso" ]; + }; + boot.menu = mkOption { + type = types.bool; + default = false; + description = "Enable boot menu."; + }; + + windows = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable Windows-optimized QEMU flags. Auto-enabled when disks.os.file carries _vmixOsType = \"windows\" metadata."; + }; + }; + + tpm = { + stateDir = mkOption { + type = types.str; + default = "/tmp"; + description = "Directory for TPM state persistence. Set to a /storage path for persistence across reboots."; + }; + }; + + pci.passthrough = mkOption { + type = types.listOf types.str; + default = []; + description = "PCI device addresses to passthrough via VFIO (e.g. [\"0000:03:00.0\" \"0000:03:00.1\"])."; + }; + pci.romFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "GPU VBIOS ROM file for the first passthrough device. Required when GPU PCI ROM BAR doesn't expose the full VBIOS (common with AMD Navi+)."; + }; + + usb.hostDevices = mkOption { + default = []; + type = types.listOf (types.submodule { + options = { + vendorId = mkOption { type = types.str; description = "USB vendor ID (e.g. \"1d6b\")."; }; + productId = mkOption { type = types.str; description = "USB product ID (e.g. \"0104\")."; }; + }; + }); + description = "USB host devices to passthrough to the VM."; + }; + + networks.user.enable = mkOption { + type = types.bool; + default = false; + description = "enable qemu user networking"; + }; + + networks.lans = mkOption { + default = {}; + type = types.attrsOf (types.submodule { + options = { + mac = mkOption { + type = types.str; + description = "MAC address for the LAN interface."; + }; + ip = mkOption { + type = types.nullOr (types.strMatching vmixLib.network.regex.ipv4); + default = null; + description = "assign static IP from the lan pool."; + }; + }; + }); + description = "LAN interfaces."; + }; + + networks.macvtaps = mkOption { + default = {}; + type = types.attrsOf (types.submodule { + options = { + mac = mkOption { + type = types.nullOr types.str; + default = null; + description = "MAC address for the MACVTap interface."; + }; + }; + }); + description = "MACVTap interfaces."; + }; + }; +} diff --git a/overlay.nix b/overlay.nix index 65654ba..0f26929 100644 --- a/overlay.nix +++ b/overlay.nix @@ -1,3 +1,7 @@ -final: prev: { - vmixLib = prev.callPackage ./lib {}; +final: prev: +let + # Pin vmixLib to nixpkgs 25-11 so all VM images are built with a consistent toolchain + vmixPkgs = prev.v25-11 or prev; +in { + vmixLib = vmixPkgs.callPackage ./lib {}; } \ No newline at end of file