From 1332f546fa946e5566acf0b85e07147a4291069d Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Wed, 9 Sep 2026 10:19:36 -0600 Subject: [PATCH] Wait for Windows enrollment network readiness --- scripts/Invoke-SguClientBootstrap.ps1 | 37 ++++++++++++++++++++++++--- scripts/Publish-GiteaRelease.ps1 | 2 +- tests/BootstrapNetwork.Tests.ps1 | 7 +++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/scripts/Invoke-SguClientBootstrap.ps1 b/scripts/Invoke-SguClientBootstrap.ps1 index 6b5e2fd..532d34e 100644 --- a/scripts/Invoke-SguClientBootstrap.ps1 +++ b/scripts/Invoke-SguClientBootstrap.ps1 @@ -178,6 +178,7 @@ function Set-ClientDomainAddress { $matchingAddress = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 ` -ErrorAction SilentlyContinue | Where-Object { + $_.AddressState -eq 'Preferred' -and $_.IPAddress -notmatch '^(127\.|169\.254\.)' -and (Test-IPv4AddressesSharePrefix -FirstAddress ([ipaddress]$_.IPAddress) ` -SecondAddress $DomainControllerAddress -PrefixLength $PrefixLength) @@ -207,7 +208,20 @@ function Set-ClientDomainAddress { New-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 ` -IPAddress $RequestedAddress.IPAddressToString -PrefixLength $PrefixLength | Out-Null } - return $RequestedAddress + + $addressReadyDeadline = (Get-Date).AddSeconds(20) + do { + $configuredAddress = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex ` + -AddressFamily IPv4 -IPAddress $RequestedAddress.IPAddressToString ` + -ErrorAction SilentlyContinue + if ($configuredAddress -and $configuredAddress.AddressState -eq 'Preferred') { + return $RequestedAddress + } + Start-Sleep -Milliseconds 500 + } while ((Get-Date) -lt $addressReadyDeadline) + + $observedState = if ($configuredAddress) { $configuredAddress.AddressState } else { 'Missing' } + throw "The SGU client address '$RequestedAddress' did not become ready on '$InterfaceAlias' within 20 seconds. Observed state: $observedState." } function Test-TcpPort { @@ -234,6 +248,23 @@ function Test-TcpPort { } } +function Wait-TcpPort { + param( + [Parameter(Mandatory)][ipaddress]$Address, + [Parameter(Mandatory)][int]$Port, + [int]$TimeoutSeconds = 20 + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + do { + if (Test-TcpPort -Address $Address -Port $Port -TimeoutMilliseconds 2000) { + return $true + } + Start-Sleep -Milliseconds 750 + } while ((Get-Date) -lt $deadline) + return $false +} + function Connect-SguAzureP2s { param([Parameter(Mandatory)][string]$ConnectionName) @@ -348,8 +379,8 @@ else { -ServerAddresses $DomainControllerIPv4Address.IPAddressToString } -if (-not (Test-TcpPort -Address $DomainControllerIPv4Address -Port 5985)) { - throw "The domain controller at $DomainControllerIPv4Address is not accepting WinRM on TCP 5985. Run the server bootstrap first and verify the selected IP." +if (-not (Wait-TcpPort -Address $DomainControllerIPv4Address -Port 5985 -TimeoutSeconds 20)) { + throw "The domain controller at $DomainControllerIPv4Address did not accept WinRM on TCP 5985 after 20 seconds. Run the server bootstrap first and verify the selected IP." } if (-not $DomainCredential) { diff --git a/scripts/Publish-GiteaRelease.ps1 b/scripts/Publish-GiteaRelease.ps1 index 4b1df9c..8e13538 100644 --- a/scripts/Publish-GiteaRelease.ps1 +++ b/scripts/Publish-GiteaRelease.ps1 @@ -112,7 +112,7 @@ Bootstrap reproducible para el laboratorio SGU. - **Advertencia:** el bootstrap de servidor crea un bosque nuevo. No restaura los SID, contraseñas ni relaciones de confianza del bosque anterior; para conservarlos se requiere una recuperación de bosque desde una copia de estado del sistema. - `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio. - `sgu-client-bootstrap-$Version.zip`: registra un certificado mTLS único, instala y valida el Credential Provider antes de unir el equipo al dominio, habilita RDP/WinRM y se repara al arranque. -- En clientes Hyper-V con dos NIC, el bootstrap selecciona la red privada sin puerta de enlace, solicita o acepta la IP fija del cliente y conserva en pantalla y archivo cualquier error de enrolamiento. +- En clientes Hyper-V con dos NIC, el bootstrap selecciona la red privada sin puerta de enlace, solicita o acepta la IP fija del cliente, espera a que la dirección y WinRM estén disponibles y conserva en pantalla y archivo cualquier error de enrolamiento. - `sgu-linux-client-bootstrap-$Version.zip`: une clientes Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux con realmd, Kerberos y SSSD. Solicita interactivamente la contraseña de unión y no instala el Credential Provider de Windows. - `sgu-azure-infrastructure-$Version.zip`: despliega mediante Bicep una VM Windows Server 2025, red privada, IP pública protegida por NSG y Azure VPN Gateway P2S; también genera certificados por equipo y descarga el perfil de cliente. - El bootstrap Azure conserva la IP privada administrada por la NIC de Azure, autoriza el pool P2S en los firewalls SGU y nunca publica LDAP, Kerberos, SMB, RPC, WinRM ni el Auth Broker directamente a Internet. diff --git a/tests/BootstrapNetwork.Tests.ps1 b/tests/BootstrapNetwork.Tests.ps1 index 57f6bc9..61e7420 100644 --- a/tests/BootstrapNetwork.Tests.ps1 +++ b/tests/BootstrapNetwork.Tests.ps1 @@ -99,6 +99,13 @@ Describe 'SGU public-cloud network safety' { $source | Should Not Match "Get-NetRoute -AddressFamily IPv4 -DestinationPrefix '0\.0\.0\.0/0'" } + It 'waits for the new address and WinRM route to stabilize' { + $source = Get-Content -LiteralPath $clientBootstrapPath -Raw + $source | Should Match "AddressState -eq 'Preferred'" + $source | Should Match 'function Wait-TcpPort' + $source | Should Match 'Wait-TcpPort -Address \$DomainControllerIPv4Address -Port 5985' + } + It 'uses an all-user machine-certificate VPN profile' { $source = Get-Content -LiteralPath $azureClientPath -Raw $source | Should Match '-AuthenticationMethod MachineCertificate'