generalize: turn DHCP off before asserting a static address, and answer pings

The VM came up holding a DHCP lease rather than the address it was told to
take, while RDP -- configured a few lines earlier in the same script -- worked
fine. So post-oobe.cmd was running; only the addressing failed.

Two reasons, both fixed. The interface arrives DHCP-managed and nothing turned
DHCP off, so New-NetIPAddress had no lasting effect. And FirstLogonCommands can
run before the adapter is up, so it is now waited for rather than assumed.

Moved out of post-oobe.cmd into its own file. The command is long and full of
quotes and pipes, which is not a thing to leave at the mercy of cmd's parsing.
It also logs, so the next failure can be read off the disk instead of inferred.

Pings are now allowed too. Windows blocks ICMP by default, which makes a box
at a fixed address look dead to everything that checks it the obvious way --
including me, for a while.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g
This commit is contained in:
Git Sagar 2026-09-10 06:56:00 -03:00
parent c0e1293405
commit a378eb4ad7

View file

@ -85,6 +85,22 @@ in
staticDnsList = lib.optionalString (staticIP != null) staticDnsList = lib.optionalString (staticIP != null)
(lib.concatMapStringsSep "," (s: "'${s}'") staticIP.dns); (lib.concatMapStringsSep "," (s: "'${s}'") staticIP.dns);
# Its own file rather than inline in post-oobe.cmd: the command is long, and
# cmd's handling of quotes and pipes inside it is a needless hazard.
#
# Two things this has to get right. The adapter may not be up yet when
# FirstLogonCommands runs, so it is waited for rather than assumed. And the
# interface arrives DHCP-managed -- assigning an address without turning DHCP
# off first does not stick, which is how a VM meant to be at a fixed address
# ended up holding a lease instead.
staticIPScript = pkgs.writeText "vmix-static-ip.cmd" ''
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -Command "$a = $null; for ($n = 0; $n -lt 30; $n++) { $a = Get-NetAdapter -Physical | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1; if ($a) { break }; Start-Sleep -Seconds 2 }; if (-not $a) { Write-Output 'vmix: no adapter came up'; exit 1 }; $i = $a.ifIndex; Set-NetIPInterface -InterfaceIndex $i -Dhcp Disabled -ErrorAction SilentlyContinue; Remove-NetIPAddress -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue; Remove-NetRoute -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue; New-NetIPAddress -InterfaceIndex $i -IPAddress '${staticIP.address}' -PrefixLength ${toString staticIP.prefixLength} -DefaultGateway '${staticIP.gateway}' -ErrorAction Stop | Out-Null; Set-DnsClientServerAddress -InterfaceIndex $i -ServerAddresses ${staticDnsList}; Write-Output ('vmix: set ' + '${staticIP.address}' + ' on ifIndex ' + $i)" > C:\Windows\Temp\vmix-static-ip.log 2>&1
:: Answer pings. Windows blocks ICMP by default, which makes a box with a
:: fixed address look dead to everything that checks it the obvious way.
powershell -NoProfile -Command "New-NetFirewallRule -DisplayName 'ICMPv4 Echo' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -Enabled True | Out-Null" > nul 2>&1
'';
dataDriveLetter = if dataDisk != null then (dataDisk.driveLetter or "D") else "D"; dataDriveLetter = if dataDisk != null then (dataDisk.driveLetter or "D") else "D";
dataLabel = if dataDisk != null then (dataDisk.label or "data") else "data"; dataLabel = if dataDisk != null then (dataDisk.label or "data") else "data";
@ -223,7 +239,7 @@ in
:: This VM's only NIC sits on a macvtap bridged to the host's LAN, so its :: This VM's only NIC sits on a macvtap bridged to the host's LAN, so its
:: address is a LAN address that nothing hands out -- the guest asserts it. :: address is a LAN address that nothing hands out -- the guest asserts it.
:: Clearing first makes the command idempotent across re-runs. :: Clearing first makes the command idempotent across re-runs.
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}" call C:\vmix-static-ip.cmd
''} ''}
${lib.optionalString (writeFilter != null) '' ${lib.optionalString (writeFilter != null) ''
@ -337,7 +353,8 @@ in {
{ source = postOobeScript; dest = "/post-oobe.cmd"; } { source = postOobeScript; dest = "/post-oobe.cmd"; }
{ source = masScript; dest = "/MAS_AIO.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"; }; ++ lib.optional (writeFilter != null) { source = uwfConfigScript; dest = "/vmix-uwf-config.cmd"; }
++ lib.optional (staticIP != null) { source = staticIPScript; dest = "/vmix-static-ip.cmd"; };
# delayOobeRun: sysprep + shutdown — OOBE runs on real hardware # delayOobeRun: sysprep + shutdown — OOBE runs on real hardware
# generalize: sysprep + reboot into OOBE in the same QEMU session # generalize: sysprep + reboot into OOBE in the same QEMU session
auditScript = '' auditScript = ''