$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $serverBootstrapPath = Join-Path $repositoryRoot 'scripts\Initialize-SguDomainController.ps1' $clientBootstrapPath = Join-Path $repositoryRoot 'scripts\Invoke-SguClientBootstrap.ps1' $azureClientPath = Join-Path $repositoryRoot 'scripts\Install-SguAzureP2sClient.ps1' $domainEnrollmentPath = Join-Path $repositoryRoot 'scripts\Enroll-SguDomainClient.ps1' $repairEnrollmentPath = Join-Path $repositoryRoot 'scripts\Repair-SguClientEnrollment.ps1' $bicepPath = Join-Path $repositoryRoot 'infra\azure\main.bicep' $tokens = $null $parseErrors = $null $serverAst = [Management.Automation.Language.Parser]::ParseFile( $serverBootstrapPath, [ref]$tokens, [ref]$parseErrors) if ($parseErrors.Count -gt 0) { throw ($parseErrors -join [Environment]::NewLine) } $networkFunctionNames = @( 'Test-PrivateIPv4Address', 'ConvertTo-NetworkCidr', 'ConvertTo-PrivateNetworkCidr', 'ConvertTo-PublicNetworkCidr', 'Get-ActiveIPv4Adapters', 'Resolve-PrivateInterfaceAlias' ) $networkFunctions = $serverAst.FindAll({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $networkFunctionNames -contains $node.Name }, $true) Invoke-Expression (($networkFunctions | ForEach-Object { $_.Extent.Text }) -join [Environment]::NewLine) $clientTokens = $null $clientParseErrors = $null $clientAst = [Management.Automation.Language.Parser]::ParseFile( $clientBootstrapPath, [ref]$clientTokens, [ref]$clientParseErrors) if ($clientParseErrors.Count -gt 0) { throw ($clientParseErrors -join [Environment]::NewLine) } $clientNetworkFunctions = $clientAst.FindAll({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -in @('Test-IPv4AddressesSharePrefix', 'Resolve-ClientInterfaceAlias', 'Set-ClientServerRoute', 'Set-ClientDomainDns', 'Test-TcpPort', 'Assert-ClientOperatingSystem', 'Wait-ClientInterface', 'Test-PrivateIPv4Address', 'Test-ClientDomainDns') }, $true) Invoke-Expression (($clientNetworkFunctions | ForEach-Object { $_.Extent.Text }) -join [Environment]::NewLine) Describe 'SGU public-cloud network safety' { It 'canonicalizes a host address to its IPv4 network' { ConvertTo-NetworkCidr -Address ([ipaddress]'10.77.0.4') ` -NetworkPrefixLength 24 | Should Be '10.77.0.0/24' } It 'canonicalizes the trusted P2S pool' { ConvertTo-PrivateNetworkCidr -Cidr '172.30.4.19/16' | Should Be '172.30.0.0/16' } It 'rejects a public trusted-client CIDR' { $wasRejected = $false try { ConvertTo-PrivateNetworkCidr -Cidr '8.8.8.0/24' | Out-Null } catch { $wasRejected = $true } $wasRejected | Should Be $true } It 'canonicalizes an explicitly authorized public enrollment network' { ConvertTo-PublicNetworkCidr -Cidr '200.13.89.183/24' | Should Be '200.13.89.0/24' } It 'rejects private space in the public enrollment allowlist' { $wasRejected = $false try { ConvertTo-PublicNetworkCidr -Cidr '10.77.0.0/16' | Out-Null } catch { $wasRejected = $true } $wasRejected | Should Be $true } It 'exposes explicit Azure modes on both bootstraps' { ((Get-Command $serverBootstrapPath).Parameters.Keys -contains 'NetworkConfigurationMode') | Should Be $true ((Get-Command $serverBootstrapPath).Parameters.Keys -contains 'TrustedClientNetworks') | Should Be $true ((Get-Command $serverBootstrapPath).Parameters.Keys -contains 'PublicEnrollmentNetworks') | Should Be $true ((Get-Command $clientBootstrapPath).Parameters.Keys -contains 'ConnectivityMode') | Should Be $true ((Get-Command $clientBootstrapPath).Parameters.Keys -contains 'VpnProfilePackagePath') | Should Be $true ((Get-Command $clientBootstrapPath).Parameters.Keys -contains 'CompatibilityProfile') | Should Be $true } It 'accepts an explicit static IPv4 address for a private Windows adapter' { ((Get-Command $clientBootstrapPath).Parameters.Keys -contains 'ClientIPv4Address') | Should Be $true ((Get-Command $clientBootstrapPath).Parameters.Keys -contains 'ClientPrefixLength') | Should Be $true } It 'matches a client and domain controller within the requested prefix' { Test-IPv4AddressesSharePrefix -FirstAddress ([ipaddress]'192.168.50.11') ` -SecondAddress ([ipaddress]'192.168.50.10') -PrefixLength 24 | Should Be $true Test-IPv4AddressesSharePrefix -FirstAddress ([ipaddress]'192.168.51.11') ` -SecondAddress ([ipaddress]'192.168.50.10') -PrefixLength 24 | Should Be $false Test-IPv4AddressesSharePrefix -FirstAddress ([ipaddress]'10.77.15.20') ` -SecondAddress ([ipaddress]'10.77.0.4') -PrefixLength 16 | Should Be $true } 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 the unified implementation without OS-specific network restrictions' { $source = Get-Content -LiteralPath $clientBootstrapPath -Raw $source | Should Not Match 'package cannot enroll|belongs to the Windows 11' $source | Should Not Match 'Read-Host "Fixed IPv4 address for this SGU client' } It 'uses an all-user machine-certificate VPN profile' { $source = Get-Content -LiteralPath $azureClientPath -Raw $source | Should Match '-AuthenticationMethod MachineCertificate' $source | Should Match '-AllUserConnection' $source | Should Match 'Add-DnsClientNrptRule' } It 'keeps public enrollment closed unless explicit source CIDRs are supplied' { $template = Get-Content -LiteralPath $bicepPath -Raw $template | Should Match "name: 'Allow-RDP-from-administrator'" $template | Should Match "destinationPortRange: '3389'" $template | Should Match 'param publicEnrollmentSourceAddressPrefixes array = \[\]' $template | Should Match "name: 'Allow-Direct-AD-TCP'" $template | Should Match 'sourceAddressPrefixes: publicEnrollmentSourceAddressPrefixes' $template | Should Match 'param deployVpnGateway bool = true' $template | Should Not Match "sourceAddressPrefix: '0\.0\.0\.0/0'" } } Describe 'SGU direct public enrollment discovery' { It 'distinguishes public server addresses from LAN and VPN addresses' { Test-PrivateIPv4Address -Address ([ipaddress]'10.77.0.4') | Should Be $true Test-PrivateIPv4Address -Address ([ipaddress]'172.30.0.4') | Should Be $true Test-PrivateIPv4Address -Address ([ipaddress]'192.168.50.10') | Should Be $true Test-PrivateIPv4Address -Address ([ipaddress]'20.9.81.130') | Should Be $false } It 'bootstraps DoH and host mappings after authenticated server discovery' { $source = Get-Content -LiteralPath $clientBootstrapPath -Raw $source | Should Match 'Set-DnsServerEncryptionProtocol' $source | Should Match 'Enable-ClientDnsOverHttps' $source | Should Match 'Set-ClientHostMappings' $source | Should Match 'Test-ClientDomainDns' $source | Should Match 'Get-DnsClientDohServerAddress' $source | Should Match 'Add-DnsClientDohServerAddress' } } Describe 'Azure accelerated server adapters' { It 'ignores an Up accelerated VF that has no IPv4 interface' { Mock Get-NetAdapter { [pscustomobject]@{ Name = 'Ethernet'; ifIndex = 4; Status = 'Up' } [pscustomobject]@{ Name = 'Ethernet VF'; ifIndex = 10; Status = 'Up' } [pscustomobject]@{ Name = 'Disconnected'; ifIndex = 12; Status = 'Disconnected' } } Mock Get-NetIPInterface { if ($InterfaceIndex -eq 4) { [pscustomobject]@{ InterfaceIndex = 4; ConnectionState = 'Connected' } } } Mock Get-NetIPConfiguration { [pscustomobject]@{ IPv4DefaultGateway = '10.77.0.1' } } $adapters = @(Get-ActiveIPv4Adapters) $adapters.Count | Should Be 1 $adapters[0].Name | Should Be 'Ethernet' Resolve-PrivateInterfaceAlias | Should Be 'Ethernet' } } Describe 'SGU route and interface discovery' { BeforeEach { Mock Get-NetIPInterface { [pscustomobject]@{ InterfaceIndex = 4; InterfaceAlias = 'Internet'; ConnectionState = 'Connected'; InterfaceMetric = 5 } [pscustomobject]@{ InterfaceIndex = 8; InterfaceAlias = 'AD VPN'; ConnectionState = 'Connected'; InterfaceMetric = 30 } } Mock Get-NetIPAddress { if ($InterfaceIndex -eq 4) { [pscustomobject]@{ IPAddress = '192.168.1.2'; AddressState = 'Preferred'; SkipAsSource = $false } } else { [pscustomobject]@{ IPAddress = '172.30.0.2'; AddressState = 'Preferred'; SkipAsSource = $false } } } Mock Get-NetRoute { if ($InterfaceIndex -eq 4) { [pscustomobject]@{ DestinationPrefix = '0.0.0.0/0'; NextHop = '192.168.1.1'; RouteMetric = 0 } } else { [pscustomobject]@{ DestinationPrefix = '10.77.0.0/16'; NextHop = '0.0.0.0'; RouteMetric = 10 } } } Mock Find-NetRoute { [pscustomobject]@{ IPAddress = '192.168.1.2'; InterfaceIndex = 4 } } Mock Test-TcpPort { $InterfaceIndex -eq 8 } } It 'tries another interface when the Internet route cannot reach WinRM' { $result = Resolve-ClientInterfaceAlias -DomainControllerAddress '10.77.0.4' $result.InterfaceAlias | Should Be 'AD VPN' $result.IPAddress | Should Be '172.30.0.2' Assert-MockCalled Test-TcpPort -Scope It -Times 1 -Exactly -ParameterFilter { $InterfaceIndex -eq 4 } Assert-MockCalled Test-TcpPort -Scope It -Times 1 -Exactly -ParameterFilter { $InterfaceIndex -eq 8 -and $SourceAddress -eq [ipaddress]'172.30.0.2' } } It 'uses a functioning Windows route first even with multiple interfaces' { Mock Test-TcpPort { $true } (Resolve-ClientInterfaceAlias -DomainControllerAddress '10.77.0.4').InterfaceAlias | Should Be 'Internet' Assert-MockCalled Test-TcpPort -Scope It -Times 0 -Exactly -ParameterFilter { $InterfaceIndex -eq 8 } } It 'honors an explicit interface and never falls back to another' { Mock Test-TcpPort { $false } $rejected = $false try { Resolve-ClientInterfaceAlias -RequestedAlias 'AD VPN' -DomainControllerAddress '10.77.0.4' | Out-Null } catch { $rejected = $true } $rejected | Should Be $true Assert-MockCalled Test-TcpPort -Scope It -Times 0 -Exactly -ParameterFilter { $InterfaceIndex -eq 4 } } It 'does not require a client to share the server subnet' { (Resolve-ClientInterfaceAlias -RequestedAlias 'AD VPN' -DomainControllerAddress '10.77.0.4').IPAddress | Should Be '172.30.0.2' } It 'accepts a normal default route when it is the only way to reach AD' { Mock Get-NetRoute { [pscustomobject]@{ DestinationPrefix = '0.0.0.0/0'; NextHop = '172.30.0.1'; RouteMetric = 0 } } (Resolve-ClientInterfaceAlias -RequestedAlias 'AD VPN' -DomainControllerAddress '10.77.0.4').NextHop | Should Be '172.30.0.1' } It 'does not probe disconnected or APIPA-only interfaces' { Mock Get-NetIPAddress { [pscustomobject]@{ IPAddress = '169.254.1.2'; AddressState = 'Preferred' } } $rejected = $false try { Resolve-ClientInterfaceAlias -DomainControllerAddress '10.77.0.4' | Out-Null } catch { $rejected = $true } $rejected | Should Be $true Assert-MockCalled Test-TcpPort -Scope It -Times 0 -Exactly } It 'does not select an adapter without a matching route' { Mock Get-NetRoute { [pscustomobject]@{ DestinationPrefix = '192.168.60.0/24'; NextHop = '0.0.0.0'; RouteMetric = 0 } } $rejected = $false try { Resolve-ClientInterfaceAlias -DomainControllerAddress '10.77.0.4' | Out-Null } catch { $rejected = $true } $rejected | Should Be $true Assert-MockCalled Test-TcpPort -Scope It -Times 0 -Exactly } It 'selects the longest matching prefix on an interface' { Mock Get-NetRoute { [pscustomobject]@{ DestinationPrefix = '0.0.0.0/0'; NextHop = '172.30.0.1'; RouteMetric = 0 } [pscustomobject]@{ DestinationPrefix = '10.77.0.4/32'; NextHop = '172.30.0.3'; RouteMetric = 100 } } (Resolve-ClientInterfaceAlias -RequestedAlias 'AD VPN' -DomainControllerAddress '10.77.0.4').NextHop | Should Be '172.30.0.3' } It 'does not change a working system route' { Mock New-NetRoute { throw 'Unexpected route mutation' } Set-ClientServerRoute -SelectedInterface ([pscustomobject]@{ InterfaceIndex = 4 }) -DomainControllerAddress '10.77.0.4' Assert-MockCalled New-NetRoute -Scope It -Times 0 -Exactly } It 'pins only the server when the working adapter differs from the system route' { $script:routeAdded = $false Mock Find-NetRoute { [pscustomobject]@{ IPAddress = '172.30.0.2'; InterfaceIndex = $(if ($script:routeAdded) { 8 } else { 4 }) } } Mock New-NetRoute { $script:routeAdded = $true } Set-ClientServerRoute -SelectedInterface ([pscustomobject]@{ InterfaceIndex = 8; NextHop = '172.30.0.1' }) ` -DomainControllerAddress '10.77.0.4' Assert-MockCalled New-NetRoute -Scope It -Times 1 -Exactly -ParameterFilter { $DestinationPrefix -eq '10.77.0.4/32' -and $InterfaceIndex -eq 8 -and $NextHop -eq '172.30.0.1' } } It 'removes its new route and reports a conflicting system route' { Mock New-NetRoute { [pscustomobject]@{ DestinationPrefix = '10.77.0.4/32'; InterfaceIndex = 8 } } Mock Remove-NetRoute { } $rejected = $false try { Set-ClientServerRoute -SelectedInterface ([pscustomobject]@{ InterfaceIndex = 8; NextHop = '172.30.0.1' }) ` -DomainControllerAddress '10.77.0.4' } catch { $rejected = $true } $rejected | Should Be $true Assert-MockCalled Remove-NetRoute -Scope It -Times 1 -Exactly } } Describe 'SGU split DNS' { It 'scopes DNS to the discovered domain and leaves adapter DNS untouched' { Mock Get-DnsClientNrptRule { } Mock Remove-DnsClientNrptRule { } Mock Add-DnsClientNrptRule { } Mock Clear-DnsClientCache { } Mock Set-DnsClientServerAddress { throw 'Unexpected adapter DNS change' } Set-ClientDomainDns -DnsDomain 'example.test' -ServerAddress '10.77.0.4' Assert-MockCalled Add-DnsClientNrptRule -Scope It -Times 1 -Exactly -ParameterFilter { $Namespace -contains '.example.test' -and $Namespace -contains 'example.test' -and $NameServers -eq '10.77.0.4' } Assert-MockCalled Set-DnsClientServerAddress -Scope It -Times 0 -Exactly } It 'reuses the managed DNS rule on a repeated enrollment' { Mock Get-DnsClientNrptRule { [pscustomobject]@{ DisplayName = 'SGU domain DNS - example.test'; NameServers = @('10.77.0.4'); Namespace = @('example.test', '.example.test') } } Mock Add-DnsClientNrptRule { throw 'Unexpected DNS rule duplication' } Set-ClientDomainDns -DnsDomain 'example.test' -ServerAddress '10.77.0.4' Assert-MockCalled Add-DnsClientNrptRule -Scope It -Times 0 -Exactly } } Describe 'SGU Windows capability checks' { It 'accepts the same enrollment on Windows 10 LTSC, Windows 10 and Windows 11' { foreach ($build in @(14393, 17763, 19044, 19045, 22000, 22631, 26100)) { Assert-ClientOperatingSystem -OperatingSystem ([pscustomobject]@{ ProductType = 1; BuildNumber = $build }) ` -Edition Enterprise -Architecture AMD64 } } It 'rejects Home, Server, pre-Windows 10 and incompatible architectures' { foreach ($sample in @( @{ ProductType = 1; BuildNumber = 26100; Edition = 'Core'; Architecture = 'AMD64' }, @{ ProductType = 3; BuildNumber = 26100; Edition = 'ServerStandard'; Architecture = 'AMD64' }, @{ ProductType = 1; BuildNumber = 9600; Edition = 'Professional'; Architecture = 'AMD64' }, @{ ProductType = 1; BuildNumber = 26100; Edition = 'Professional'; Architecture = 'ARM64' }, @{ ProductType = 1; BuildNumber = 19045; Edition = 'Professional'; Architecture = 'x86' } )) { $rejected = $false try { Assert-ClientOperatingSystem -OperatingSystem ([pscustomobject]$sample) ` -Edition $sample.Edition -Architecture $sample.Architecture } catch { $rejected = $true } $rejected | Should Be $true } } } Describe 'SGU repeated domain enrollment' { It 'rejoins a same-name forest when the machine secure channel is broken' { $source = Get-Content -LiteralPath $domainEnrollmentPath -Raw $source | Should Match 'Test-ComputerSecureChannel' $source | Should Match '\$computer\.PartOfDomain -and \$domainMembershipHealthy' $source | Should Match 'Reset-ComputerMachinePassword' $source | Should Match 'DomainControllerDnsName' $source | Should Match 'Add-Computer @joinParams' } It 'defers domain-only repair until the secure channel is healthy' { $source = Get-Content -LiteralPath $repairEnrollmentPath -Raw $source | Should Match 'Test-ComputerSecureChannel' $source | Should Match 'if \(\$domainReady\)' } } Describe 'SGU real TCP probe' { It 'connects with a bound source and interface without relying on ICMP' { $listener = [Net.Sockets.TcpListener]::new([ipaddress]'127.0.0.1', 0) try { $listener.Start() $loopback = Get-NetIPAddress -IPAddress '127.0.0.1' -AddressFamily IPv4 | Select-Object -First 1 Test-TcpPort -Address '127.0.0.1' -Port $listener.LocalEndpoint.Port ` -SourceAddress '127.0.0.1' -InterfaceIndex $loopback.InterfaceIndex | Should Be $true } finally { $listener.Stop() } } It 'returns false when the TCP service is closed' { $listener = [Net.Sockets.TcpListener]::new([ipaddress]'127.0.0.1', 0) $listener.Start() $port = $listener.LocalEndpoint.Port $listener.Stop() Test-TcpPort -Address '127.0.0.1' -Port $port -TimeoutMilliseconds 200 | Should Be $false } } Describe 'SGU network readiness retries' { It 'retries discovery while DHCP or VPN routes are initializing' { $script:discoveryAttempts = 0 Mock Start-Sleep { } Mock Resolve-ClientInterfaceAlias { $script:discoveryAttempts++ if ($script:discoveryAttempts -eq 1) { throw 'Address still tentative' } [pscustomobject]@{ InterfaceAlias = 'AD VPN'; IPAddress = '172.30.0.2' } } (Wait-ClientInterface -DomainControllerAddress '10.77.0.4').InterfaceAlias | Should Be 'AD VPN' Assert-MockCalled Resolve-ClientInterfaceAlias -Scope It -Times 2 -Exactly } It 'reports the last network diagnostic when the timeout expires' { Mock Resolve-ClientInterfaceAlias { throw 'No route to the server' } $message = '' try { Wait-ClientInterface -DomainControllerAddress '10.77.0.4' -TimeoutSeconds 0 } catch { $message = $_.Exception.Message } $message | Should Match 'No route to the server' } }