diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix index 65194b2..c369eaa 100644 --- a/lib/images/windows/templates/generalize.nix +++ b/lib/images/windows/templates/generalize.nix @@ -35,6 +35,10 @@ in # during the build (see extraDisk in the returned set), so this is done and # verified before the image ever reaches a host. dataDisk ? null, + # Unified Write Filter: protect a volume by redirecting its writes to a + # disk-backed overlay held on another one, e.g. + # { protectedVolume = "C:"; swapfileVolume = "D:"; overlaySizeMB = 8192; } + writeFilter ? null, # delayOobeRun = true: sysprep only, OOBE + activation on real hardware # delayOobeRun = false: sysprep + OOBE + activation in build VM delayOobeRun ? false, @@ -54,6 +58,30 @@ in stripHash = s: lib.removePrefix "#" s; bgRgb = if bgColor != null then hexToRgbStr (stripHash bgColor) else null; + uwfProtected = if writeFilter != null then (writeFilter.protectedVolume or "C:") else "C:"; + uwfSwapVolume = if writeFilter != null then (writeFilter.swapfileVolume or "D:") else "D:"; + uwfOverlaySizeMB = if writeFilter != null then (writeFilter.overlaySizeMB or 8192) else 8192; + + # Runs from RunOnce on the target's first boot rather than during the build, + # for two reasons: enabling the DISM feature needs a reboot before uwfmgr + # exists at all, and the overlay swapfile has to be created on the real data + # volume rather than on the build's throwaway copy of it. + # + # Order is forced by uwfmgr: create-swapfile is only accepted while the + # filter is off and the overlay is already in disk mode. The default disk + # overlay would otherwise sit at C:\uwfswap.sys, on the volume being + # protected. Enabling the filter itself only takes effect after a restart. + uwfConfigScript = pkgs.writeText "vmix-uwf-config.cmd" '' + @echo off + uwfmgr.exe overlay set-type disk + uwfmgr.exe overlay set-size ${toString uwfOverlaySizeMB} + uwfmgr.exe volume create-swapfile ${uwfSwapVolume} + uwfmgr.exe volume protect ${uwfProtected} + uwfmgr.exe filter enable + del /q C:\vmix-uwf-config.cmd 2>nul + shutdown /r /t 10 /c "vmix: activating the write filter" + ''; + staticDnsList = lib.optionalString (staticIP != null) (lib.concatMapStringsSep "," (s: "'${s}'") staticIP.dns); @@ -172,6 +200,14 @@ in powershell -NoProfile -Command "$a = Get-NetAdapter -Physical | Sort-Object ifIndex | Select-Object -First 1; Remove-NetIPAddress -InterfaceIndex $a.ifIndex -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue; Remove-NetRoute -InterfaceIndex $a.ifIndex -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue; New-NetIPAddress -InterfaceIndex $a.ifIndex -IPAddress '${staticIP.address}' -PrefixLength ${toString staticIP.prefixLength} -DefaultGateway '${staticIP.gateway}' | Out-Null; Set-DnsClientServerAddress -InterfaceIndex $a.ifIndex -ServerAddresses ${staticDnsList}" ''} + ${lib.optionalString (writeFilter != null) '' + :: Install the feature now, but defer configuring it: uwfmgr does not exist + :: until this has been through a reboot, and the swapfile belongs on the + :: real data volume, so RunOnce picks it up on the target's first boot. + dism /online /enable-feature /featurename:Client-UnifiedWriteFilter /all /norestart + reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v "vmixUwf" /t REG_SZ /d "C:\vmix-uwf-config.cmd" /f + ''} + :: Clean up del /q C:\oobe-unattend.xml 2>nul del /q C:\vmix-audit-script.cmd 2>nul @@ -264,7 +300,8 @@ in { { source = oobeXml; dest = "/oobe-unattend.xml"; } { source = postOobeScript; dest = "/post-oobe.cmd"; } { source = masScript; dest = "/MAS_AIO.cmd"; } - ] ++ lib.optional (dataDisk != null) { source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; }; + ] ++ lib.optional (dataDisk != null) { source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; } + ++ lib.optional (writeFilter != null) { source = uwfConfigScript; dest = "/vmix-uwf-config.cmd"; }; # delayOobeRun: sysprep + shutdown — OOBE runs on real hardware # generalize: sysprep + reboot into OOBE in the same QEMU session auditScript = ''