vmix.nix/lib/images/windows/templates/generalize.nix
Git Sagar b9375c572f re-enable MAS activation for all images
Win11 LTSC 2024 RDP works with MAS. The edition switch issue was
specific to Win10 LTSC 2021.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-08 17:01:10 +05:30

212 lines
10 KiB
Nix

# 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,
# Enable Remote Desktop for the created user (re-applied after sysprep)
enableRDP ? false,
# delayOobeRun = true: sysprep only, OOBE + activation on real hardware
# delayOobeRun = false: sysprep + OOBE + activation in build VM
delayOobeRun ? false,
}: 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
''}
${lib.optionalString (password != "") ''
:: Set user password (OOBE creates with blank password for reliable AutoLogon)
net user "${username}" "${password}"
''}
:: Set AutoLogon via registry (OOBE unattend AutoLogon is unreliable)
${lib.optionalString autoLogon ''
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon /t REG_SZ /d "1" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v DefaultUserName /t REG_SZ /d "${username}" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v DefaultPassword /t REG_SZ /d "${password}" /f
''}
:: Kill sysprep if it was triggered via CopyProfile'd startup entries
taskkill /f /im sysprep.exe 2>nul
:: Clean any leftover RunOnce/Run entries from audit phase
reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v "vmixAudit" /f 2>nul
reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /v "vmixAudit" /f 2>nul
:: 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
${lib.optionalString enableRDP ''
:: Enable RDP
powershell -Command "Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0"
powershell -Command "Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name UserAuthentication -Value 1"
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Lsa" /v LimitBlankPasswordUse /t REG_DWORD /d 0 /f
:: Create firewall rules for all profiles (New-NetFirewallRule is more reliable than Enable-NetFirewallRule)
powershell -Command "New-NetFirewallRule -DisplayName 'RDP (TCP)' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3389 -RemoteAddress Any -Profile Any -Enabled True | Out-Null"
powershell -Command "New-NetFirewallRule -DisplayName 'RDP (UDP)' -Direction Inbound -Action Allow -Protocol UDP -LocalPort 3389 -RemoteAddress Any -Profile Any -Enabled True | Out-Null"
:: Set all RDP services to auto-start
reg add "HKLM\SYSTEM\CurrentControlSet\Services\SessionEnv" /v Start /t REG_DWORD /d 2 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\UmRdpService" /v Start /t REG_DWORD /d 2 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\TermService" /v Start /t REG_DWORD /d 2 /f
''}
:: 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
${if delayOobeRun then "" else "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></Value>
<PlainText>true</PlainText>
</Password>
<Group>Administrators</Group>
<Name>${username}</Name>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<AutoLogon>
<Password>
<Value></Value>
<PlainText>true</PlainText>
</Password>
<Enabled>true</Enabled>
<LogonCount>999</LogonCount>
<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 = if delayOobeRun then "generalize-delay-oobe" else "generalize";
uploads = [
{ source = oobeXml; dest = "/oobe-unattend.xml"; }
{ source = postOobeScript; dest = "/post-oobe.cmd"; }
{ source = masScript; dest = "/MAS_AIO.cmd"; }
];
# delayOobeRun: sysprep + shutdown — OOBE runs on real hardware
# generalize: sysprep + reboot into OOBE in the same QEMU session
auditScript = ''
@echo off
:: Remove cached Autounattend from initial install (contains Audit Mode reseal)
del /q C:\Windows\Panther\unattend.xml 2>nul
del /q C:\Windows\Panther\Unattend\unattend.xml 2>nul
del /q C:\Windows\System32\Sysprep\Panther\unattend.xml 2>nul
C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe ${if delayOobeRun then "/shutdown" else "/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