diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix index 9c2e7e8..b38811b 100644 --- a/lib/images/windows/templates/generalize.nix +++ b/lib/images/windows/templates/generalize.nix @@ -93,12 +93,48 @@ in # 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. + # PowerShell in its own file: it grew a wait loop and a retry, which are no + # fun to keep correct inside a cmd one-liner. + # + # Two things it must survive. DHCP is turned off before the address is set, + # so any failure to set it strands the box with no address at all -- which is + # exactly what happened after an internal reboot, where a stale ARP entry for + # the address from the previous instance tripped duplicate-address detection + # and New-NetIPAddress threw. DadTransmits 0 turns that detection off so the + # static always binds, and the assignment is retried rather than fatal. + staticIPScriptPs1 = pkgs.writeText "vmix-static-ip.ps1" '' + $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 -DadTransmits 0 -ErrorAction SilentlyContinue + Remove-NetIPAddress -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue + Remove-NetRoute -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue + $ok = $false + for ($k = 0; $k -lt 5 -and -not $ok; $k++) { + try { + New-NetIPAddress -InterfaceIndex $i -IPAddress '${staticIP.address}' -PrefixLength ${toString staticIP.prefixLength} -DefaultGateway '${staticIP.gateway}' -ErrorAction Stop | Out-Null + $ok = $true + } catch { + Write-Output ('vmix: assign attempt ' + $k + ' failed: ' + $_.Exception.Message) + Start-Sleep -Seconds 2 + Remove-NetIPAddress -InterfaceIndex $i -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue + } + } + if (-not $ok) { Write-Output 'vmix: could not set static address'; exit 1 } + Set-DnsClientServerAddress -InterfaceIndex $i -ServerAddresses ${staticDnsList} + New-NetFirewallRule -DisplayName 'ICMPv4 Echo' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -Enabled True -ErrorAction SilentlyContinue | Out-Null + Write-Output ('vmix: set ${staticIP.address} on ifIndex ' + $i) + ''; + + # Thin launcher, so the scheduled task has a cmd to point at. 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 + powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-static-ip.ps1 > C:\Windows\Temp\vmix-static-ip.log 2>&1 ''; dataDriveLetter = if dataDisk != null then (dataDisk.driveLetter or "D") else "D"; @@ -413,7 +449,10 @@ in { { source = bootDataProfileScript; dest = "/vmix-data-profile.cmd"; } ] ++ lib.optional (writeFilter != null) { source = uwfConfigScript; dest = "/vmix-uwf-config.cmd"; } - ++ lib.optional (staticIP != null) { source = staticIPScript; dest = "/vmix-static-ip.cmd"; }; + ++ lib.optionals (staticIP != null) [ + { source = staticIPScript; dest = "/vmix-static-ip.cmd"; } + { source = staticIPScriptPs1; dest = "/vmix-static-ip.ps1"; } + ]; # delayOobeRun: sysprep + shutdown — OOBE runs on real hardware # generalize: sysprep + reboot into OOBE in the same QEMU session auditScript = ''