sync with labv2.nix + standalone flake with toDisk app
Previous history: -c359054daku working! -8de5cfffix integer overflow in vmix network lib -9c25a66daku on 25.05. with ollama -385a3bfvmix enables relaxed sandbox -c363da1restructure vmixLib into linux/windows subattrs with OS-specific customizeImage -edd4dc2vmix: port namespace model and module improvements from conf.nix -6666ecfvmix: add SPICE support, install virtio guest tools with SPICE agent -46f5671vmix: add QEMU guest agent channel for Windows VMs -e1fea34vmix: add Win11 LTSC 2024 image, refactor VirtIO driver selection -c27ae68vmix: make customizeImage chroot-sandboxed by default, opt-in impure -305fbacvirt customize needs chroot for now due to usr bin env things. could be fixed later -264d30fvmix: add win10 VM on desk, disable SMB signing for guest Samba access -9b64f51vmix: split Windows templates into per-category files, add comprehensive debloat -ef91bf8vmix: fix missing parent registry keys in Windows templates -f87f340win10 VM on panda with AMD GPU + USB passthrough -38e474fvmix: split Windows build into Audit Mode install + composable templates -a6a8db3vmix: win11 support, remove build VNC, switch VMs to SPICE -6cf5a21generalize stage sets bg color, accent color and sets visual effects to performance -a84849fremove rdp template since it doesn't even work -5245263vmix: best performance template + generalize cleanup -ab12dd3vmix: use CopyProfile for best performance visual effects -bce3326vmix: CopyProfile for best performance visual effects -2496107vmix: add app templates (7zip, VLC, ImageGlass, Edge WebView, VC++ runtimes) -29a6123wip: debug default associations xml -2a2e5f5vmix: fix DefaultAssociations.xml cmd.exe escaping -cc6ff9dvmix: move DefaultAssociations.xml to template only -a4a78ecvmix: add removeWMP template to remove Windows Media Player -3fe56devmix: improved Edge removal (files, shortcuts, scheduled tasks) -a491767vmix: fully remove Edge via post-oobe AppxPackage removal -6ca1619vmix: remove Edge DevToolsClient SystemApps + AppxPackage -0c1ec35vmix: sandboxie windows app template -628bbd2vmix: add Sandboxie-Plus template -f055a41vmix: reorganize templates, add file associations, remove Paint -34326f4vmix: set Thorium as default browser via PS-SFTA in post-oobe -86af258vmix: Active Setup for default browser (all users, no post-oobe needed) -35b8cb0remove vnc display from thorium template -c7e0af6vmix: fix Win11 generalize timeout + UCPD disable for URL associations -43a1345vmix: add Office 2024 template + Ohook activation in generalize -03bbce0vmix: updated office installation xml. more privacy options enabled -790a0eevmix: thorium installation - hide SFTA window -a0e5c18vmix: fix office install.bat call + add privacy registry policies -3df38cavmix: fix Ohook activation + suppress Office theme dialog -df39ba3vmix: remove sandboxie shortcut from desktop -50d5972vmix: skip Sandboxie desktop shortcut via installer flag -ee2fa0fvmix: fix win10 default browser -938315bvmix: windows: set accent color to automatic. remove accent color from unnecessary elements -beceda8vmix: 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) <noreply@anthropic.com>
This commit is contained in:
parent
dd1fb16e1b
commit
94f299bb81
77 changed files with 2785 additions and 796 deletions
93
flake.nix
93
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;
|
||||
|
||||
nixosModules.default = import ./module.nix;
|
||||
|
||||
lib.${system} = vmixLib;
|
||||
|
||||
packages.${system} = {
|
||||
toDisk = pkgs.writeShellScriptBin "vmix-to-disk" ''
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: vmix-to-disk <qcow2-image> <target-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."
|
||||
'';
|
||||
};
|
||||
|
||||
nixmox = customizeImage images.debian.v12.proxmox (images.debian.templates.rooted // {
|
||||
name = "nixmox";
|
||||
});
|
||||
|
||||
default = playfuldeb;
|
||||
apps.${system}.toDisk = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.toDisk}/bin/vmix-to-disk";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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; };
|
||||
}
|
||||
|
|
@ -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"; }
|
||||
|
|
@ -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
|
||||
|
|
@ -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,7 +20,7 @@ in
|
|||
});
|
||||
|
||||
# proxmox
|
||||
proxmox = customizeImage default (templates.proxmoxOnDebian12 // {
|
||||
proxmox = customizeImage upstreamImages.${upstreamImageName} (templates.proxmoxOnDebian12 // {
|
||||
name = "proxmox";
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
lib/images/linux/default.nix
Normal file
14
lib/images/linux/default.nix
Normal file
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
32
lib/images/windows/default.nix
Normal file
32
lib/images/windows/default.nix
Normal file
|
|
@ -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; };
|
||||
}
|
||||
5
lib/images/windows/drivers/amd-gpu-drivers.nix
Normal file
5
lib/images/windows/drivers/amd-gpu-drivers.nix
Normal file
|
|
@ -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";
|
||||
}
|
||||
4
lib/images/windows/drivers/default.nix
Normal file
4
lib/images/windows/drivers/default.nix
Normal file
|
|
@ -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; };
|
||||
}
|
||||
6
lib/images/windows/drivers/virtio-iso-w10-11.nix
Normal file
6
lib/images/windows/drivers/virtio-iso-w10-11.nix
Normal file
|
|
@ -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";
|
||||
}
|
||||
113
lib/images/windows/helpers/customizeImage.nix
Normal file
113
lib/images/windows/helpers/customizeImage.nix
Normal file
|
|
@ -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 = <drv or path>; 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"; }
|
||||
174
lib/images/windows/helpers/makeAuditModeAutounattend.nix
Normal file
174
lib/images/windows/helpers/makeAuditModeAutounattend.nix
Normal file
|
|
@ -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" ''
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
|
||||
<!-- windowsPE — locale, product key, VirtIO drivers, disk partitioning -->
|
||||
<settings pass="windowsPE">
|
||||
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<SetupUILanguage>
|
||||
<UILanguage>${locale}</UILanguage>
|
||||
</SetupUILanguage>
|
||||
<InputLocale>${locale}</InputLocale>
|
||||
<SystemLocale>${locale}</SystemLocale>
|
||||
<UILanguage>${locale}</UILanguage>
|
||||
<UserLocale>${locale}</UserLocale>
|
||||
</component>
|
||||
|
||||
<component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<DriverPaths>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
|
||||
<Path>${virtioDriverLetter}:\amd64\${windowsVersionForVirtioDrivers}</Path>
|
||||
</PathAndCredentials>
|
||||
</DriverPaths>
|
||||
</component>
|
||||
|
||||
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<UserData>
|
||||
${lib.optionalString (productKey != "") ''
|
||||
<ProductKey>
|
||||
<Key>${productKey}</Key>
|
||||
</ProductKey>
|
||||
''}
|
||||
<AcceptEula>true</AcceptEula>
|
||||
</UserData>
|
||||
|
||||
<ImageInstall>
|
||||
<OSImage>
|
||||
<InstallFrom>
|
||||
<MetaData wcm:action="add">
|
||||
<Key>/IMAGE/INDEX</Key>
|
||||
<Value>${toString imageIndex}</Value>
|
||||
</MetaData>
|
||||
</InstallFrom>
|
||||
<InstallTo>
|
||||
<DiskID>${toString diskIndex}</DiskID>
|
||||
<PartitionID>${if efi then "3" else "1"}</PartitionID>
|
||||
</InstallTo>
|
||||
</OSImage>
|
||||
</ImageInstall>
|
||||
|
||||
<DiskConfiguration>
|
||||
<Disk wcm:action="add">
|
||||
<DiskID>${toString diskIndex}</DiskID>
|
||||
<WillWipeDisk>true</WillWipeDisk>
|
||||
${if efi then ''
|
||||
<CreatePartitions>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Size>100</Size>
|
||||
<Type>EFI</Type>
|
||||
</CreatePartition>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Size>16</Size>
|
||||
<Type>MSR</Type>
|
||||
</CreatePartition>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Extend>true</Extend>
|
||||
<Type>Primary</Type>
|
||||
</CreatePartition>
|
||||
</CreatePartitions>
|
||||
<ModifyPartitions>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<PartitionID>1</PartitionID>
|
||||
<Format>FAT32</Format>
|
||||
<Label>EFI</Label>
|
||||
</ModifyPartition>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<PartitionID>2</PartitionID>
|
||||
</ModifyPartition>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<PartitionID>3</PartitionID>
|
||||
<Format>NTFS</Format>
|
||||
<Label>Windows</Label>
|
||||
</ModifyPartition>
|
||||
</ModifyPartitions>
|
||||
'' else ''
|
||||
<CreatePartitions>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Extend>true</Extend>
|
||||
<Type>Primary</Type>
|
||||
</CreatePartition>
|
||||
</CreatePartitions>
|
||||
<ModifyPartitions>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<PartitionID>1</PartitionID>
|
||||
<Format>NTFS</Format>
|
||||
<Label>Windows</Label>
|
||||
<Active>true</Active>
|
||||
</ModifyPartition>
|
||||
</ModifyPartitions>
|
||||
''}
|
||||
</Disk>
|
||||
</DiskConfiguration>
|
||||
|
||||
${lib.optionalString bypassRequirements ''
|
||||
<RunSynchronous>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
''}
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- specialize — inject RunOnce to clean up cached Autounattend and shutdown -->
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<RunSynchronous>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v vmixCleanup /t REG_SZ /d "cmd /c del /q C:\Windows\Panther\unattend.xml" /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v vmixShutdown /t REG_SZ /d "shutdown /s /t 10 /c vmix-audit-ready" /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- oobeSystem — enter Audit Mode -->
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<Reseal>
|
||||
<Mode>Audit</Mode>
|
||||
</Reseal>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
''
|
||||
23
lib/images/windows/helpers/makeFilesISO.nix
Normal file
23
lib/images/windows/helpers/makeFilesISO.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
69
lib/images/windows/helpers/makeImage.nix
Normal file
69
lib/images/windows/helpers/makeImage.nix
Normal file
|
|
@ -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"; }
|
||||
40
lib/images/windows/helpers/makeWinISO.nix
Normal file
40
lib/images/windows/helpers/makeWinISO.nix
Normal file
|
|
@ -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"
|
||||
''
|
||||
BIN
lib/images/windows/templates/apps/7zip-file-associations.reg
Normal file
BIN
lib/images/windows/templates/apps/7zip-file-associations.reg
Normal file
Binary file not shown.
20
lib/images/windows/templates/apps/7zip.nix
Normal file
20
lib/images/windows/templates/apps/7zip.nix
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Install 7-Zip and set file associations
|
||||
{ pkgs, makeFilesISO, ... }:
|
||||
let
|
||||
installer = pkgs.fetchurl {
|
||||
url = "https://github.com/ip7z/7zip/releases/download/26.00/7z2600-x64.msi";
|
||||
hash = "sha256-w4jQREhxyhGyEjcAGvFYz92tfhN4UXleW2XO5ptRhJU=";
|
||||
};
|
||||
assocReg = ./7zip-file-associations.reg;
|
||||
in {
|
||||
name = "7zip";
|
||||
cdroms = [ (makeFilesISO { name = "7zip"; files = [ installer assocReg ]; }) ];
|
||||
auditScript = ''
|
||||
@echo off
|
||||
echo Installing 7-Zip...
|
||||
msiexec /i "D:\7z2600-x64.msi" /qn /norestart
|
||||
|
||||
echo Importing 7-Zip file associations...
|
||||
regedit /s "D:\7zip-file-associations.reg"
|
||||
'';
|
||||
}
|
||||
18
lib/images/windows/templates/apps/edge-webview.nix
Normal file
18
lib/images/windows/templates/apps/edge-webview.nix
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Install Microsoft Edge WebView2 Runtime
|
||||
{ pkgs, makeFilesISO, ... }:
|
||||
let
|
||||
installer = pkgs.fetchurl {
|
||||
url = "https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/7ff41be4-dbce-4efe-823f-c7d33a4e3c5e/MicrosoftEdgeWebview2Setup.exe";
|
||||
hash = "sha256-Asl+XjKpfYlhY+xoo+o6sDOeV/J0NIuGiTmWOLZJsrk=";
|
||||
};
|
||||
in {
|
||||
name = "edge-webview";
|
||||
cdroms = [ (makeFilesISO { name = "edge-webview"; files = [ installer ]; }) ];
|
||||
auditScript = ''
|
||||
@echo off
|
||||
echo Installing Edge WebView2 Runtime...
|
||||
copy D:\MicrosoftEdgeWebview2Setup.exe C:\webview-setup.exe
|
||||
start /wait C:\webview-setup.exe /silent /install
|
||||
del /q C:\webview-setup.exe
|
||||
'';
|
||||
}
|
||||
Binary file not shown.
26
lib/images/windows/templates/apps/imageglass.nix
Normal file
26
lib/images/windows/templates/apps/imageglass.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
29
lib/images/windows/templates/apps/office.nix
Normal file
29
lib/images/windows/templates/apps/office.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
16
lib/images/windows/templates/apps/sandboxie.nix
Normal file
16
lib/images/windows/templates/apps/sandboxie.nix
Normal file
|
|
@ -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"
|
||||
'';
|
||||
}
|
||||
40
lib/images/windows/templates/apps/thorium.nix
Normal file
40
lib/images/windows/templates/apps/thorium.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
19
lib/images/windows/templates/apps/vlc.nix
Normal file
19
lib/images/windows/templates/apps/vlc.nix
Normal file
|
|
@ -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"
|
||||
'';
|
||||
}
|
||||
49
lib/images/windows/templates/default-apps.nix
Normal file
49
lib/images/windows/templates/default-apps.nix
Normal file
|
|
@ -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 ^<?xml version="1.0" encoding="UTF-8"?^> > "%DA%"
|
||||
echo ^<DefaultAssociations^> >> "%DA%"
|
||||
echo ^<Association Identifier=".7z" ProgId="7-Zip.7z" ApplicationName="7-Zip"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".zip" ProgId="7-Zip.zip" ApplicationName="7-Zip"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".rar" ProgId="7-Zip.rar" ApplicationName="7-Zip"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".tar" ProgId="7-Zip.tar" ApplicationName="7-Zip"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".gz" ProgId="7-Zip.gz" ApplicationName="7-Zip"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".xz" ProgId="7-Zip.xz" ApplicationName="7-Zip"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".cab" ProgId="7-Zip.cab" ApplicationName="7-Zip"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".mp4" ProgId="VLC.mp4" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".mkv" ProgId="VLC.mkv" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".avi" ProgId="VLC.avi" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".mov" ProgId="VLC.mov" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".wmv" ProgId="VLC.wmv" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".webm" ProgId="VLC.webm" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".mpg" ProgId="VLC.mpg" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".ts" ProgId="VLC.ts" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".mp3" ProgId="VLC.mp3" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".flac" ProgId="VLC.flac" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".wav" ProgId="VLC.wav" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".aac" ProgId="VLC.aac" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".ogg" ProgId="VLC.ogg" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".wma" ProgId="VLC.wma" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".m4a" ProgId="VLC.m4a" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".opus" ProgId="VLC.opus" ApplicationName="VLC"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".jpg" ProgId="ImageGlass.jpg" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".jpeg" ProgId="ImageGlass.jpeg" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".png" ProgId="ImageGlass.png" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".gif" ProgId="ImageGlass.gif" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".bmp" ProgId="ImageGlass.bmp" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".webp" ProgId="ImageGlass.webp" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".tif" ProgId="ImageGlass.tif" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".ico" ProgId="ImageGlass.ico" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".svg" ProgId="ImageGlass.svg" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^<Association Identifier=".heic" ProgId="ImageGlass.heic" ApplicationName="ImageGlass"/^> >> "%DA%"
|
||||
echo ^</DefaultAssociations^> >> "%DA%"
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v DefaultAssociationsConfiguration /t REG_SZ /d "C:\Program Files\vmix\DefaultAssociations.xml" /f
|
||||
'';
|
||||
}
|
||||
43
lib/images/windows/templates/default.nix
Normal file
43
lib/images/windows/templates/default.nix
Normal file
|
|
@ -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;
|
||||
}
|
||||
11
lib/images/windows/templates/essentials/amd-gpu-drivers.nix
Normal file
11
lib/images/windows/templates/essentials/amd-gpu-drivers.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
46
lib/images/windows/templates/essentials/best-performance.nix
Normal file
46
lib/images/windows/templates/essentials/best-performance.nix
Normal file
|
|
@ -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
|
||||
|
||||
'';
|
||||
}
|
||||
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
50
lib/images/windows/templates/essentials/remove-edge.nix
Normal file
50
lib/images/windows/templates/essentials/remove-edge.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
12
lib/images/windows/templates/essentials/remove-ie.nix
Normal file
12
lib/images/windows/templates/essentials/remove-ie.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
20
lib/images/windows/templates/essentials/remove-paint.nix
Normal file
20
lib/images/windows/templates/essentials/remove-paint.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
11
lib/images/windows/templates/essentials/remove-wmp.nix
Normal file
11
lib/images/windows/templates/essentials/remove-wmp.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
18
lib/images/windows/templates/essentials/vcpp-runtimes.nix
Normal file
18
lib/images/windows/templates/essentials/vcpp-runtimes.nix
Normal file
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
18
lib/images/windows/templates/essentials/virtio-tools.nix
Normal file
18
lib/images/windows/templates/essentials/virtio-tools.nix
Normal file
|
|
@ -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
|
||||
)
|
||||
'';
|
||||
}
|
||||
168
lib/images/windows/templates/generalize.nix
Normal file
168
lib/images/windows/templates/generalize.nix
Normal file
|
|
@ -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" ''
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<!-- Copy Administrator profile to default (preserves Audit Mode customizations) -->
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<CopyProfile>true</CopyProfile>
|
||||
<Themes>
|
||||
<WindowColor>Automatic</WindowColor>
|
||||
</Themes>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<InputLocale>${locale}</InputLocale>
|
||||
<SystemLocale>${locale}</SystemLocale>
|
||||
<UILanguage>${locale}</UILanguage>
|
||||
<UserLocale>${locale}</UserLocale>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<NetworkLocation>Work</NetworkLocation>
|
||||
<SkipMachineOOBE>true</SkipMachineOOBE>
|
||||
<SkipUserOOBE>true</SkipUserOOBE>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
</OOBE>
|
||||
<UserAccounts>
|
||||
<LocalAccounts>
|
||||
<LocalAccount wcm:action="add">
|
||||
<Password>
|
||||
<Value>${password}</Value>
|
||||
</Password>
|
||||
<Group>Administrators</Group>
|
||||
<Name>${username}</Name>
|
||||
</LocalAccount>
|
||||
</LocalAccounts>
|
||||
</UserAccounts>
|
||||
<AutoLogon>
|
||||
<Password>
|
||||
<Value>${password}</Value>
|
||||
</Password>
|
||||
<Enabled>true</Enabled>
|
||||
<Username>${username}</Username>
|
||||
</AutoLogon>
|
||||
<ComputerName>${hostname}</ComputerName>
|
||||
<TimeZone>${timezone}</TimeZone>
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<CommandLine>C:\post-oobe.cmd</CommandLine>
|
||||
<RequiresUserInput>false</RequiresUserInput>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
'';
|
||||
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
|
||||
|
||||
10
lib/images/windows/templates/registry/ai.nix
Normal file
10
lib/images/windows/templates/registry/ai.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
50
lib/images/windows/templates/registry/consumer.nix
Normal file
50
lib/images/windows/templates/registry/consumer.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
63
lib/images/windows/templates/registry/default.nix
Normal file
63
lib/images/windows/templates/registry/default.nix
Normal file
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
11
lib/images/windows/templates/registry/defender.nix
Normal file
11
lib/images/windows/templates/registry/defender.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
10
lib/images/windows/templates/registry/disable-ucpd.nix
Normal file
10
lib/images/windows/templates/registry/disable-ucpd.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
|
|
@ -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
|
||||
''
|
||||
9
lib/images/windows/templates/registry/hibernation.nix
Normal file
9
lib/images/windows/templates/registry/hibernation.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
12
lib/images/windows/templates/registry/insecure-samba.nix
Normal file
12
lib/images/windows/templates/registry/insecure-samba.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
34
lib/images/windows/templates/registry/performance.nix
Normal file
34
lib/images/windows/templates/registry/performance.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
44
lib/images/windows/templates/registry/privacy.nix
Normal file
44
lib/images/windows/templates/registry/privacy.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
6
lib/images/windows/templates/registry/smart-screen.nix
Normal file
6
lib/images/windows/templates/registry/smart-screen.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Disable SmartScreen
|
||||
''
|
||||
|
||||
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\System]
|
||||
"EnableSmartScreen"=dword:00000000
|
||||
''
|
||||
8
lib/images/windows/templates/registry/system-restore.nix
Normal file
8
lib/images/windows/templates/registry/system-restore.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
15
lib/images/windows/templates/registry/telemetry.nix
Normal file
15
lib/images/windows/templates/registry/telemetry.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
21
lib/images/windows/templates/registry/updates.nix
Normal file
21
lib/images/windows/templates/registry/updates.nix
Normal file
|
|
@ -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
|
||||
''
|
||||
12
lib/images/windows/win10/default.nix
Normal file
12
lib/images/windows/win10/default.nix
Normal file
|
|
@ -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
|
||||
34
lib/images/windows/win10/images.nix
Normal file
34
lib/images/windows/win10/images.nix
Normal file
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
11
lib/images/windows/win10/upstream.json
Normal file
11
lib/images/windows/win10/upstream.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
12
lib/images/windows/win11/default.nix
Normal file
12
lib/images/windows/win11/default.nix
Normal file
|
|
@ -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
|
||||
38
lib/images/windows/win11/images.nix
Normal file
38
lib/images/windows/win11/images.nix
Normal file
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
11
lib/images/windows/win11/upstream.json
Normal file
11
lib/images/windows/win11/upstream.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}";
|
||||
}
|
||||
6
module.nix
Normal file
6
module.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
./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) ];
|
||||
}
|
||||
13
nixos/namespaceSubmoduleOptions.nix
Normal file
13
nixos/namespaceSubmoduleOptions.nix
Normal file
|
|
@ -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 = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
# };
|
||||
};
|
||||
}
|
||||
290
nixos/networks/config.nix
Normal file
290
nixos/networks/config.nix
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) ];
|
||||
}
|
||||
137
nixos/networks/options.nix
Normal file
137
nixos/networks/options.nix
Normal file
|
|
@ -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";
|
||||
# };
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) ];
|
||||
}
|
||||
|
|
@ -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.";
|
||||
};
|
||||
};
|
||||
}
|
||||
272
nixos/vms/config.nix
Normal file
272
nixos/vms/config.nix
Normal file
|
|
@ -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;
|
||||
}
|
||||
5
nixos/vms/default.nix
Normal file
5
nixos/vms/default.nix
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
args@{ config, pkgs, lib, vmixLib, ... }:
|
||||
with lib;
|
||||
{
|
||||
imports = [ (import ./config.nix args) ];
|
||||
}
|
||||
309
nixos/vms/submoduleOptions.nix
Normal file
309
nixos/vms/submoduleOptions.nix
Normal file
|
|
@ -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.";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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 {};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue