1015 lines
47 KiB
PowerShell
1015 lines
47 KiB
PowerShell
#Requires -Version 5.1
|
|
[CmdletBinding(SupportsShouldProcess)]
|
|
param(
|
|
[ipaddress]$DomainControllerIPv4Address,
|
|
[string]$NetworkInterfaceAlias,
|
|
[ipaddress]$ClientIPv4Address,
|
|
[ValidateRange(1, 32)]
|
|
[int]$ClientPrefixLength = 24,
|
|
[PSCredential]$DomainCredential,
|
|
[string]$DomainName,
|
|
[string]$DomainNetbios,
|
|
[string]$ComputerOuDn,
|
|
[string]$NewComputerName,
|
|
[ValidateSet('Auto', 'Windows10Legacy', 'Windows11Modern')]
|
|
[string]$CompatibilityProfile = 'Auto',
|
|
[ValidateSet('Direct', 'AzureP2S')]
|
|
[string]$ConnectivityMode = 'Direct',
|
|
[string]$VpnConnectionName = 'SGU Azure P2S',
|
|
[string]$VpnProfilePackagePath,
|
|
[string]$VpnClientCertificatePfxPath,
|
|
[securestring]$VpnClientCertificatePfxPassword,
|
|
[string]$VpnClientRootCertificatePath,
|
|
[string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'),
|
|
[switch]$PauseOnError,
|
|
[switch]$SkipRestart
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$brokerRecordName = 'sgu-auth'
|
|
$temporaryRoot = Join-Path $env:ProgramData ("SGU\Bootstrap\Client-" + [Guid]::NewGuid().ToString('N'))
|
|
$bootstrapLogRoot = Join-Path $env:ProgramData 'SGU\Bootstrap\Client'
|
|
$bootstrapErrorLog = Join-Path $bootstrapLogRoot 'latest-error.log'
|
|
|
|
trap {
|
|
$failure = $_
|
|
$failureText = @(
|
|
"SGU client enrollment failed at $((Get-Date).ToString('s')).",
|
|
'',
|
|
$failure.Exception.Message,
|
|
'',
|
|
$failure.ScriptStackTrace
|
|
) -join [Environment]::NewLine
|
|
try {
|
|
New-Item -ItemType Directory -Path $bootstrapLogRoot -Force | Out-Null
|
|
[IO.File]::WriteAllText($bootstrapErrorLog, $failureText, [Text.UTF8Encoding]::new($false))
|
|
}
|
|
catch {
|
|
# Keep the original enrollment error when diagnostics cannot be written.
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host 'SGU client enrollment did not complete.' -ForegroundColor Red
|
|
Write-Host $failure.Exception.Message -ForegroundColor Red
|
|
Write-Host "Diagnostic log: $bootstrapErrorLog" -ForegroundColor Yellow
|
|
if ($PauseOnError -and [Environment]::UserInteractive) {
|
|
Read-Host 'Press ENTER to close this window' | Out-Null
|
|
}
|
|
exit 1
|
|
}
|
|
|
|
function Assert-Administrator {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
throw 'Run this bootstrap from an elevated Windows PowerShell session.'
|
|
}
|
|
}
|
|
|
|
function Assert-PackageManifest {
|
|
param([Parameter(Mandatory)][string]$PackageRoot)
|
|
|
|
$manifestPath = Join-Path $PackageRoot 'package-manifest.json'
|
|
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
|
throw 'package-manifest.json is missing. Use the complete SGU client bootstrap release.'
|
|
}
|
|
|
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
|
foreach ($entry in $manifest.Files) {
|
|
$path = Join-Path $PackageRoot ([string]$entry.Path)
|
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
|
throw "Bootstrap package file is missing: $($entry.Path)"
|
|
}
|
|
$actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash
|
|
if ($actual -ne [string]$entry.Sha256) {
|
|
throw "Bootstrap package integrity check failed: $($entry.Path)"
|
|
}
|
|
}
|
|
return $manifest
|
|
}
|
|
|
|
function Resolve-ClientInterfaceAlias {
|
|
param(
|
|
[string]$RequestedAlias,
|
|
[Parameter(Mandatory)][ipaddress]$DomainControllerAddress
|
|
)
|
|
|
|
# IP interfaces include tunnel/PPP adapters that Get-NetAdapter can omit.
|
|
$interfaces = @(Get-NetIPInterface -AddressFamily IPv4 | Where-Object {
|
|
$_.ConnectionState -eq 'Connected' -and
|
|
(-not $RequestedAlias -or $_.InterfaceAlias -eq $RequestedAlias)
|
|
})
|
|
$preferred = @(Find-NetRoute -RemoteIPAddress $DomainControllerAddress.IPAddressToString `
|
|
-ErrorAction SilentlyContinue | Where-Object { $_.PSObject.Properties['IPAddress'] })
|
|
$attempts = @()
|
|
$candidates = @(foreach ($interface in $interfaces) {
|
|
$addresses = @(Get-NetIPAddress -InterfaceIndex $interface.InterfaceIndex -AddressFamily IPv4 `
|
|
-ErrorAction SilentlyContinue | Where-Object {
|
|
$_.AddressState -eq 'Preferred' -and -not $_.SkipAsSource -and
|
|
$_.IPAddress -notmatch '^(0\.|127\.|169\.254\.)'
|
|
})
|
|
if ($addresses.Count -eq 0) {
|
|
$attempts += "$($interface.InterfaceAlias): no usable IPv4 address (check DHCP/static configuration)"
|
|
continue
|
|
}
|
|
$route = Get-NetRoute -InterfaceIndex $interface.InterfaceIndex -AddressFamily IPv4 `
|
|
-PolicyStore ActiveStore -ErrorAction SilentlyContinue | Where-Object {
|
|
$parts = $_.DestinationPrefix -split '/'
|
|
Test-IPv4AddressesSharePrefix -FirstAddress $DomainControllerAddress `
|
|
-SecondAddress ([ipaddress]$parts[0]) -PrefixLength ([int]$parts[1])
|
|
} | Sort-Object @{ Expression = { [int]($_.DestinationPrefix -split '/')[1] }; Descending = $true },
|
|
RouteMetric | Select-Object -First 1
|
|
if ($route) {
|
|
foreach ($address in $addresses) {
|
|
[pscustomobject]@{
|
|
InterfaceAlias = $interface.InterfaceAlias
|
|
InterfaceIndex = [int]$interface.InterfaceIndex
|
|
IPAddress = $address.IPAddress
|
|
NextHop = $route.NextHop
|
|
Preferred = @($preferred | Where-Object IPAddress -eq $address.IPAddress).Count -gt 0
|
|
PrefixLength = [int]($route.DestinationPrefix -split '/')[1]
|
|
Metric = [int]$route.RouteMetric + [int]$interface.InterfaceMetric
|
|
}
|
|
}
|
|
}
|
|
else { $attempts += "$($interface.InterfaceAlias): no route to $DomainControllerAddress" }
|
|
})
|
|
if ($interfaces.Count -eq 0) { $attempts += 'No matching connected IPv4 interface' }
|
|
foreach ($candidate in ($candidates | Sort-Object @{ Expression = { $_.Preferred }; Descending = $true },
|
|
@{ Expression = { $_.PrefixLength }; Descending = $true }, Metric, InterfaceIndex, IPAddress)) {
|
|
Write-Host "Checking $($candidate.InterfaceAlias) ($($candidate.IPAddress)) -> $DomainControllerAddress..."
|
|
if (Test-TcpPort -Address $DomainControllerAddress -Port 5985 -TimeoutMilliseconds 2000 `
|
|
-SourceAddress ([ipaddress]$candidate.IPAddress) -InterfaceIndex $candidate.InterfaceIndex) {
|
|
return $candidate
|
|
}
|
|
$attempts += "$($candidate.InterfaceAlias) [$($candidate.IPAddress), next hop $($candidate.NextHop)]: TCP 5985 unavailable"
|
|
}
|
|
throw "Cannot reach SGU server $DomainControllerAddress. $($attempts -join '; '). Check the LAN/VPN connection, DHCP or an administrator-assigned IP, routes and the server WinRM firewall. No client IP was changed."
|
|
}
|
|
|
|
function Test-IPv4AddressesSharePrefix {
|
|
param(
|
|
[Parameter(Mandatory)][ipaddress]$FirstAddress,
|
|
[Parameter(Mandatory)][ipaddress]$SecondAddress,
|
|
[Parameter(Mandatory)][ValidateRange(0, 32)][int]$PrefixLength
|
|
)
|
|
|
|
if ($FirstAddress.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork -or
|
|
$SecondAddress.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
|
|
return $false
|
|
}
|
|
|
|
$firstBytes = $FirstAddress.GetAddressBytes()
|
|
$secondBytes = $SecondAddress.GetAddressBytes()
|
|
$remainingBits = $PrefixLength
|
|
for ($index = 0; $index -lt 4; $index++) {
|
|
$bits = [Math]::Min(8, $remainingBits)
|
|
$mask = if ($bits -eq 0) {
|
|
0
|
|
}
|
|
elseif ($bits -eq 8) {
|
|
255
|
|
}
|
|
else {
|
|
256 - [int][Math]::Pow(2, 8 - $bits)
|
|
}
|
|
if (($firstBytes[$index] -band $mask) -ne ($secondBytes[$index] -band $mask)) {
|
|
return $false
|
|
}
|
|
$remainingBits -= $bits
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Test-PrivateIPv4Address {
|
|
param([Parameter(Mandatory)][ipaddress]$Address)
|
|
|
|
if ($Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
|
|
return $false
|
|
}
|
|
$bytes = $Address.GetAddressBytes()
|
|
return $bytes[0] -eq 10 -or
|
|
($bytes[0] -eq 172 -and $bytes[1] -ge 16 -and $bytes[1] -le 31) -or
|
|
($bytes[0] -eq 192 -and $bytes[1] -eq 168)
|
|
}
|
|
|
|
function Wait-ClientInterface {
|
|
param(
|
|
[string]$RequestedAlias,
|
|
[Parameter(Mandatory)][ipaddress]$DomainControllerAddress,
|
|
[int]$TimeoutSeconds = 20
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
try {
|
|
return Resolve-ClientInterfaceAlias -RequestedAlias $RequestedAlias `
|
|
-DomainControllerAddress $DomainControllerAddress
|
|
}
|
|
catch {
|
|
$lastFailure = $_
|
|
if ((Get-Date) -ge $deadline) { throw $lastFailure }
|
|
Write-Host 'Waiting for DHCP, VPN routes or server connectivity to become ready...'
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
} while ($true)
|
|
}
|
|
|
|
function Assert-UsableClientIPv4Address {
|
|
param(
|
|
[Parameter(Mandatory)][ipaddress]$Address,
|
|
[Parameter(Mandatory)][ipaddress]$DomainControllerAddress,
|
|
[Parameter(Mandatory)][ValidateRange(1, 32)][int]$PrefixLength
|
|
)
|
|
|
|
if ($Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
|
|
throw "The SGU client address '$Address' must be IPv4."
|
|
}
|
|
if ($Address.IPAddressToString -eq $DomainControllerAddress.IPAddressToString) {
|
|
throw 'The SGU client and domain controller cannot use the same IPv4 address.'
|
|
}
|
|
if ($Address.IPAddressToString -match '^(0\.|127\.|169\.254\.|22[4-9]\.|23\d\.)') {
|
|
throw "The SGU client address '$Address' is not usable on the private domain network."
|
|
}
|
|
if (-not (Test-IPv4AddressesSharePrefix -FirstAddress $Address `
|
|
-SecondAddress $DomainControllerAddress -PrefixLength $PrefixLength)) {
|
|
throw "The SGU client address '$Address/$PrefixLength' is not on the same network as domain controller $DomainControllerAddress."
|
|
}
|
|
}
|
|
|
|
function Set-ClientDomainAddress {
|
|
param(
|
|
[Parameter(Mandatory)][string]$InterfaceAlias,
|
|
[Parameter(Mandatory)][ipaddress]$DomainControllerAddress,
|
|
[ipaddress]$RequestedAddress,
|
|
[Parameter(Mandatory)][ValidateRange(1, 32)][int]$PrefixLength
|
|
)
|
|
|
|
$adapter = Get-NetAdapter -Name $InterfaceAlias -ErrorAction Stop
|
|
$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)
|
|
} |
|
|
Select-Object -First 1
|
|
|
|
if (-not $RequestedAddress -and $matchingAddress) {
|
|
return [ipaddress]$matchingAddress.IPAddress
|
|
}
|
|
if (-not $RequestedAddress) {
|
|
throw 'Static addressing requires an explicit -ClientIPv4Address. Automatic enrollment preserves DHCP and existing addresses.'
|
|
}
|
|
Assert-UsableClientIPv4Address -Address $RequestedAddress `
|
|
-DomainControllerAddress $DomainControllerAddress -PrefixLength $PrefixLength
|
|
|
|
Set-NetIPInterface -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -Dhcp Disabled
|
|
$existingAddresses = @(Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
|
|
-ErrorAction SilentlyContinue | Where-Object PrefixOrigin -ne 'WellKnown')
|
|
foreach ($existingAddress in $existingAddresses) {
|
|
if ($existingAddress.IPAddress -ne $RequestedAddress.IPAddressToString -or
|
|
[int]$existingAddress.PrefixLength -ne $PrefixLength) {
|
|
Remove-NetIPAddress -InputObject $existingAddress -Confirm:$false
|
|
}
|
|
}
|
|
if (-not (Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
|
|
-IPAddress $RequestedAddress.IPAddressToString -ErrorAction SilentlyContinue)) {
|
|
New-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
|
|
-IPAddress $RequestedAddress.IPAddressToString -PrefixLength $PrefixLength | Out-Null
|
|
}
|
|
|
|
$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 {
|
|
param(
|
|
[Parameter(Mandatory)][ipaddress]$Address,
|
|
[Parameter(Mandatory)][int]$Port,
|
|
[int]$TimeoutMilliseconds = 5000,
|
|
[ipaddress]$SourceAddress,
|
|
[int]$InterfaceIndex
|
|
)
|
|
|
|
$client = [Net.Sockets.TcpClient]::new([Net.Sockets.AddressFamily]::InterNetwork)
|
|
$connect = $null
|
|
try {
|
|
if ($InterfaceIndex) {
|
|
# IP_UNICAST_IF (31) expects the interface index in network byte order.
|
|
$client.Client.SetSocketOption([Net.Sockets.SocketOptionLevel]::IP,
|
|
[Net.Sockets.SocketOptionName]31, [Net.IPAddress]::HostToNetworkOrder($InterfaceIndex))
|
|
}
|
|
if ($SourceAddress) {
|
|
$client.Client.Bind([Net.IPEndPoint]::new($SourceAddress, 0))
|
|
}
|
|
$connect = $client.BeginConnect($Address, $Port, $null, $null)
|
|
if (-not $connect.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)) {
|
|
return $false
|
|
}
|
|
$client.EndConnect($connect)
|
|
return $true
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
finally {
|
|
$client.Dispose()
|
|
if ($connect) { $connect.AsyncWaitHandle.Close() }
|
|
}
|
|
}
|
|
|
|
function Set-ClientServerRoute {
|
|
param(
|
|
[Parameter(Mandatory)]$SelectedInterface,
|
|
[Parameter(Mandatory)][ipaddress]$DomainControllerAddress
|
|
)
|
|
$current = @(Find-NetRoute -RemoteIPAddress $DomainControllerAddress.IPAddressToString |
|
|
Where-Object { $_.PSObject.Properties['IPAddress'] })
|
|
if (@($current | Where-Object InterfaceIndex -eq $SelectedInterface.InterfaceIndex).Count -gt 0) { return }
|
|
|
|
# Only pin this server when Windows currently chooses a different interface.
|
|
# Do not replace default routes or change interface metrics used by Internet traffic.
|
|
$route = New-NetRoute -DestinationPrefix "$DomainControllerAddress/32" `
|
|
-InterfaceIndex $SelectedInterface.InterfaceIndex -NextHop $SelectedInterface.NextHop `
|
|
-RouteMetric 1
|
|
$current = @(Find-NetRoute -RemoteIPAddress $DomainControllerAddress.IPAddressToString |
|
|
Where-Object { $_.PSObject.Properties['IPAddress'] })
|
|
if (@($current | Where-Object InterfaceIndex -eq $SelectedInterface.InterfaceIndex).Count -eq 0) {
|
|
$route | Remove-NetRoute -Confirm:$false
|
|
throw "Windows still routes $DomainControllerAddress through another interface. Resolve conflicting host routes or VPN policies and retry."
|
|
}
|
|
}
|
|
|
|
function Set-ClientDomainDns {
|
|
param(
|
|
[Parameter(Mandatory)][string]$DnsDomain,
|
|
[Parameter(Mandatory)][ipaddress]$ServerAddress
|
|
)
|
|
$displayName = "SGU domain DNS - $DnsDomain"
|
|
$existing = @(Get-DnsClientNrptRule -ErrorAction Stop | Where-Object DisplayName -eq $displayName)
|
|
if ($existing.Count -eq 1 -and @($existing[0].NameServers) -contains $ServerAddress.IPAddressToString -and
|
|
@($existing[0].Namespace) -contains ".$DnsDomain" -and @($existing[0].Namespace) -contains $DnsDomain) { return }
|
|
$existing | Remove-DnsClientNrptRule -Force
|
|
Add-DnsClientNrptRule -Namespace @($DnsDomain, ".$DnsDomain") `
|
|
-NameServers $ServerAddress.IPAddressToString -DisplayName $displayName | Out-Null
|
|
Clear-DnsClientCache
|
|
}
|
|
|
|
function Test-ClientDomainDns {
|
|
param([Parameter(Mandatory)][string]$DnsDomain)
|
|
|
|
try {
|
|
$records = @(Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DnsDomain" `
|
|
-DnsOnly -ErrorAction Stop)
|
|
return @($records | Where-Object {
|
|
$_.Type -eq 'SRV' -and -not [string]::IsNullOrWhiteSpace([string]$_.NameTarget)
|
|
}).Count -gt 0
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Set-ClientHostMappings {
|
|
param(
|
|
[Parameter(Mandatory)][ipaddress]$ServerAddress,
|
|
[Parameter(Mandatory)][string[]]$HostNames
|
|
)
|
|
|
|
$hostsPath = Join-Path $env:SystemRoot 'System32\drivers\etc\hosts'
|
|
$managedNames = @($HostNames |
|
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
|
ForEach-Object { $_.Trim().ToLowerInvariant() } |
|
|
Select-Object -Unique)
|
|
$preservedLines = foreach ($line in [IO.File]::ReadAllLines($hostsPath)) {
|
|
$data = ($line -split '#', 2)[0].Trim()
|
|
$tokens = @($data -split '\s+' | Where-Object { $_ })
|
|
$lineNames = if ($tokens.Count -gt 1) {
|
|
@($tokens[1..($tokens.Count - 1)] | ForEach-Object { $_.ToLowerInvariant() })
|
|
}
|
|
else { @() }
|
|
if (@($lineNames | Where-Object { $managedNames -contains $_ }).Count -eq 0) {
|
|
$line
|
|
}
|
|
}
|
|
$mapping = '{0} {1} # SGU managed direct enrollment' -f
|
|
$ServerAddress.IPAddressToString,($managedNames -join ' ')
|
|
[IO.File]::WriteAllLines($hostsPath, @($preservedLines) + $mapping,
|
|
[Text.UTF8Encoding]::new($false))
|
|
Clear-DnsClientCache
|
|
}
|
|
|
|
function Enable-ClientDnsOverHttps {
|
|
param(
|
|
[Parameter(Mandatory)][ipaddress]$ServerAddress,
|
|
[Parameter(Mandatory)][string]$DohTemplate,
|
|
[Parameter(Mandatory)][string]$CertificateBase64
|
|
)
|
|
|
|
if (-not (Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue)) {
|
|
throw 'This Windows build cannot configure DNS over HTTPS. Permit traditional DNS to the supplied server or update Windows, then retry.'
|
|
}
|
|
|
|
$certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
|
|
[Convert]::FromBase64String($CertificateBase64))
|
|
$store = [Security.Cryptography.X509Certificates.X509Store]::new(
|
|
[Security.Cryptography.X509Certificates.StoreName]::Root,
|
|
[Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)
|
|
try {
|
|
$store.Open([Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
|
if (-not @($store.Certificates | Where-Object Thumbprint -eq $certificate.Thumbprint).Count) {
|
|
$store.Add($certificate)
|
|
}
|
|
}
|
|
finally {
|
|
$store.Close()
|
|
$certificate.Dispose()
|
|
}
|
|
|
|
$existing = Get-DnsClientDohServerAddress -ErrorAction SilentlyContinue |
|
|
Where-Object ServerAddress -eq $ServerAddress.IPAddressToString |
|
|
Select-Object -First 1
|
|
if ($existing) {
|
|
Set-DnsClientDohServerAddress -ServerAddress $ServerAddress.IPAddressToString `
|
|
-DohTemplate $DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true | Out-Null
|
|
}
|
|
else {
|
|
Add-DnsClientDohServerAddress -ServerAddress $ServerAddress.IPAddressToString `
|
|
-DohTemplate $DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true | Out-Null
|
|
}
|
|
& "$env:SystemRoot\System32\netsh.exe" dnsclient set global doh=yes | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'Windows did not enable its global DNS over HTTPS client setting.'
|
|
}
|
|
Clear-DnsClientCache
|
|
}
|
|
|
|
function Assert-ClientOperatingSystem {
|
|
param(
|
|
[Parameter(Mandatory)]$OperatingSystem,
|
|
[Parameter(Mandatory)][string]$Edition,
|
|
[Parameter(Mandatory)][string]$Architecture
|
|
)
|
|
if ([int]$OperatingSystem.ProductType -ne 1) {
|
|
throw 'The client bootstrap supports Windows 10/11 workstations. Use the server bootstrap on Windows Server.'
|
|
}
|
|
if ([int]$OperatingSystem.BuildNumber -lt 14393 -or $Architecture -ne 'AMD64') {
|
|
throw 'This package requires Windows 10 1607 or later, or Windows 11, running x64 Windows PowerShell.'
|
|
}
|
|
if ($Edition -match '^Core' -or $Edition -match 'Home') {
|
|
throw "Windows edition '$Edition' cannot join an Active Directory domain. Upgrade to Pro, Enterprise, or Education, then run this same bootstrap again."
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection `
|
|
-ErrorAction SilentlyContinue
|
|
if (-not $connection) {
|
|
throw "The all-user VPN connection '$ConnectionName' is not installed. Run Install-SguAzureP2sClient.ps1 in this VM first."
|
|
}
|
|
if ($connection.TunnelType -notcontains 'Ikev2' -and $connection.TunnelType -ne 'Ikev2') {
|
|
throw "The VPN connection '$ConnectionName' is not configured for IKEv2."
|
|
}
|
|
if ($connection.ConnectionStatus -ne 'Connected') {
|
|
& "$env:SystemRoot\System32\rasdial.exe" $ConnectionName
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Could not connect the Azure P2S profile '$ConnectionName'. Verify the machine certificate and that UDP 500/4500 is permitted by the local network."
|
|
}
|
|
}
|
|
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection
|
|
if ($connection.ConnectionStatus -ne 'Connected') {
|
|
throw "The Azure P2S profile '$ConnectionName' did not reach Connected state."
|
|
}
|
|
return $connection
|
|
}
|
|
|
|
Assert-Administrator
|
|
$operatingSystem = Get-CimInstance Win32_OperatingSystem
|
|
$edition = (Get-WindowsEdition -Online).Edition
|
|
Assert-ClientOperatingSystem -OperatingSystem $operatingSystem -Edition $edition `
|
|
-Architecture $env:PROCESSOR_ARCHITECTURE
|
|
$windowsBuild = [int]$operatingSystem.BuildNumber
|
|
$windowsName = if ($windowsBuild -ge 22000) { 'Windows 11' } else { 'Windows 10' }
|
|
Write-Host "$windowsName (build $windowsBuild): unified SGU enrollment."
|
|
|
|
if (-not $DomainControllerIPv4Address) {
|
|
$DomainControllerIPv4Address = [ipaddress](Read-Host 'Fixed IPv4 address of the SGU domain controller')
|
|
}
|
|
if ($DomainControllerIPv4Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork -or
|
|
$DomainControllerIPv4Address.IPAddressToString -match '^(0\.|127\.|169\.254\.|22[4-9]\.|23\d\.|24\d\.|25[0-5]\.)') {
|
|
throw 'Enter a reachable unicast IPv4 address for the domain controller.'
|
|
}
|
|
$publicDirectEnrollment = $ConnectivityMode -eq 'Direct' -and
|
|
-not (Test-PrivateIPv4Address -Address $DomainControllerIPv4Address)
|
|
if ($publicDirectEnrollment) {
|
|
Write-Host 'Public domain-controller address detected. Direct DNS and domain discovery will be configured automatically.'
|
|
}
|
|
|
|
$packageRoot = $PSScriptRoot
|
|
$packageManifest = Assert-PackageManifest -PackageRoot $packageRoot
|
|
# Retain the old parameter for existing automation; neither name restricts networking.
|
|
if ($CompatibilityProfile -ne 'Auto') {
|
|
Write-Warning 'CompatibilityProfile is deprecated. This package uses the same implementation on Windows 10 and 11.'
|
|
}
|
|
$CompatibilityProfile = 'Auto'
|
|
$scriptsRoot = Join-Path $packageRoot 'payload\scripts'
|
|
$providerPublishPath = Join-Path $packageRoot 'payload\credential-provider'
|
|
$runtimeInstaller = Get-ChildItem (Join-Path $packageRoot 'payload\prerequisites') `
|
|
-Filter '*x64*.exe' -File -ErrorAction SilentlyContinue |
|
|
Sort-Object Name -Descending |
|
|
Select-Object -First 1
|
|
foreach ($requiredPath in @(
|
|
(Join-Path $scriptsRoot 'Enroll-SguDomainClient.ps1'),
|
|
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
|
|
(Join-Path $scriptsRoot 'Register-SguClientCertificate.ps1'),
|
|
(Join-Path $providerPublishPath 'SGU.CredentialProvider.comhost.dll'))) {
|
|
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
|
|
throw "The client bootstrap package is incomplete: $requiredPath"
|
|
}
|
|
}
|
|
if (-not $runtimeInstaller) {
|
|
throw 'The offline Microsoft .NET 10 x64 runtime installer is missing from the client package.'
|
|
}
|
|
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enroll with SGU server $DomainControllerIPv4Address")) { return }
|
|
if ($ClientIPv4Address) {
|
|
if (-not $NetworkInterfaceAlias -or $ConnectivityMode -eq 'AzureP2S') {
|
|
throw 'Explicit static IP setup requires -NetworkInterfaceAlias with Direct connectivity. Omit -ClientIPv4Address to preserve the current LAN/VPN configuration.'
|
|
}
|
|
$ClientIPv4Address = Set-ClientDomainAddress -InterfaceAlias $NetworkInterfaceAlias `
|
|
-DomainControllerAddress $DomainControllerIPv4Address `
|
|
-RequestedAddress $ClientIPv4Address -PrefixLength $ClientPrefixLength
|
|
}
|
|
|
|
if ($ConnectivityMode -eq 'AzureP2S') {
|
|
$existingVpnConnection = Get-VpnConnection -Name $VpnConnectionName -AllUserConnection `
|
|
-ErrorAction SilentlyContinue
|
|
if (-not $existingVpnConnection) {
|
|
$installerPath = Join-Path $packageRoot 'Install-SguAzureP2sClient.ps1'
|
|
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
|
|
throw 'Install-SguAzureP2sClient.ps1 is missing from the client bootstrap package.'
|
|
}
|
|
foreach ($vpnInput in @(
|
|
@{ Name = 'VpnProfilePackagePath'; Value = $VpnProfilePackagePath },
|
|
@{ Name = 'VpnClientCertificatePfxPath'; Value = $VpnClientCertificatePfxPath },
|
|
@{ Name = 'VpnClientRootCertificatePath'; Value = $VpnClientRootCertificatePath })) {
|
|
if ([string]::IsNullOrWhiteSpace([string]$vpnInput.Value)) {
|
|
throw "$($vpnInput.Name) is required the first time an Azure P2S client is enrolled."
|
|
}
|
|
}
|
|
$vpnInstallParameters = @{
|
|
VpnProfilePackagePath = $VpnProfilePackagePath
|
|
ClientCertificatePfxPath = $VpnClientCertificatePfxPath
|
|
ClientRootCertificatePath = $VpnClientRootCertificatePath
|
|
ConnectionName = $VpnConnectionName
|
|
AzureNetworkPrefixes = $AzureNetworkPrefixes
|
|
DomainControllerIPv4Address = $DomainControllerIPv4Address
|
|
DomainName = $DomainName
|
|
}
|
|
if ($VpnClientCertificatePfxPassword) {
|
|
$vpnInstallParameters.ClientCertificatePfxPassword = $VpnClientCertificatePfxPassword
|
|
}
|
|
& $installerPath @vpnInstallParameters | Out-Null
|
|
}
|
|
$vpnConnection = Connect-SguAzureP2s -ConnectionName $VpnConnectionName
|
|
}
|
|
$selectedInterface = Wait-ClientInterface -RequestedAlias $NetworkInterfaceAlias `
|
|
-DomainControllerAddress $DomainControllerIPv4Address
|
|
$NetworkInterfaceAlias = $selectedInterface.InterfaceAlias
|
|
$ClientIPv4Address = [ipaddress]$selectedInterface.IPAddress
|
|
Set-ClientServerRoute -SelectedInterface $selectedInterface -DomainControllerAddress $DomainControllerIPv4Address
|
|
Write-Host "Using $NetworkInterfaceAlias ($ClientIPv4Address)."
|
|
|
|
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) {
|
|
$suggestedUser = if ($DomainNetbios) { "$DomainNetbios\Administrator" }
|
|
elseif ($DomainName) { "Administrator@$DomainName" } else { 'Administrator' }
|
|
$DomainCredential = Get-Credential `
|
|
-UserName $suggestedUser `
|
|
-Message "Domain account permitted to enroll this computer (DOMAIN\user or user@domain)"
|
|
}
|
|
if (-not $DomainCredential) { throw 'Enrollment cancelled: no domain credential was provided.' }
|
|
|
|
New-Item -ItemType Directory -Path $temporaryRoot -Force | Out-Null
|
|
$clientCertificatePath = Join-Path $temporaryRoot 'client.cer'
|
|
$serverCertificatePath = Join-Path $temporaryRoot 'server.cer'
|
|
$remoteRegistrationScript = $null
|
|
$remoteClientCertificate = $null
|
|
$session = $null
|
|
$winRmWasRunning = (Get-Service WinRM).Status -eq 'Running'
|
|
$priorTrustedHosts = $null
|
|
|
|
try {
|
|
if (-not $winRmWasRunning) {
|
|
Set-Service WinRM -StartupType Manual
|
|
Start-Service WinRM
|
|
}
|
|
$priorTrustedHosts = [string](Get-Item WSMan:\localhost\Client\TrustedHosts).Value
|
|
$trustedHostValues = @($priorTrustedHosts -split ',' | ForEach-Object Trim | Where-Object { $_ })
|
|
if ($trustedHostValues -notcontains $DomainControllerIPv4Address.IPAddressToString) {
|
|
$trustedHostValues += $DomainControllerIPv4Address.IPAddressToString
|
|
Set-Item WSMan:\localhost\Client\TrustedHosts -Value ($trustedHostValues -join ',') -Force
|
|
}
|
|
|
|
$session = New-PSSession `
|
|
-ComputerName $DomainControllerIPv4Address.IPAddressToString `
|
|
-Credential $DomainCredential `
|
|
-Authentication Negotiate
|
|
|
|
$serverIdentity = Invoke-Command -Session $session -ScriptBlock {
|
|
$computer = Get-CimInstance Win32_ComputerSystem
|
|
if ([int]$computer.DomainRole -lt 4) { throw 'The supplied server is not an Active Directory domain controller.' }
|
|
Import-Module ActiveDirectory -ErrorAction Stop
|
|
$domain = Get-ADDomain -ErrorAction Stop
|
|
$labOu = Get-ADOrganizationalUnit -LDAPFilter '(ou=Laboratorio)' `
|
|
-SearchBase $domain.DistinguishedName -SearchScope OneLevel -ErrorAction Stop |
|
|
Select-Object -First 1
|
|
$brokerService = Get-Service SGUAuthBroker -ErrorAction SilentlyContinue
|
|
$rustDeskStatusPath = Join-Path $env:ProgramData 'SGU\RustDesk\server.json'
|
|
$rustDeskStatus = if (Test-Path -LiteralPath $rustDeskStatusPath -PathType Leaf) {
|
|
Get-Content -LiteralPath $rustDeskStatusPath -Raw | ConvertFrom-Json
|
|
}
|
|
else {
|
|
$null
|
|
}
|
|
$hbbsTask = Get-ScheduledTask -TaskName 'SGU-RustDesk-hbbs' -ErrorAction SilentlyContinue
|
|
$hbbrTask = Get-ScheduledTask -TaskName 'SGU-RustDesk-hbbr' -ErrorAction SilentlyContinue
|
|
[pscustomobject]@{
|
|
ComputerName = $env:COMPUTERNAME
|
|
Domain = $computer.Domain
|
|
DomainNetbios = $domain.NetBIOSName
|
|
ComputerContainer = if ($labOu) { $labOu.DistinguishedName } else { $domain.ComputersContainer }
|
|
BrokerService = if ($brokerService) { $brokerService.Status.ToString() } else { 'Missing' }
|
|
RustDeskServerAddress = if ($rustDeskStatus) { [string]$rustDeskStatus.ServerAddress } else { $null }
|
|
RustDeskPublicKey = if ($rustDeskStatus) { [string]$rustDeskStatus.PublicKey } else { $null }
|
|
RustDeskHbbsTask = if ($hbbsTask) { $hbbsTask.State.ToString() } else { 'Missing' }
|
|
RustDeskHbbrTask = if ($hbbrTask) { $hbbrTask.State.ToString() } else { 'Missing' }
|
|
}
|
|
}
|
|
if (-not $serverIdentity.Domain -or ($DomainName -and
|
|
-not $serverIdentity.Domain.Equals($DomainName, [StringComparison]::OrdinalIgnoreCase))) {
|
|
throw "The server at $DomainControllerIPv4Address belongs to $($serverIdentity.Domain), not $DomainName."
|
|
}
|
|
if ($DomainNetbios -and $DomainNetbios -ne $serverIdentity.DomainNetbios) {
|
|
throw "The supplied NetBIOS domain '$DomainNetbios' does not match '$($serverIdentity.DomainNetbios)'."
|
|
}
|
|
$DomainName = [string]$serverIdentity.Domain
|
|
$DomainNetbios = [string]$serverIdentity.DomainNetbios
|
|
if (-not $ComputerOuDn) { $ComputerOuDn = [string]$serverIdentity.ComputerContainer }
|
|
$brokerDnsName = "$brokerRecordName.$DomainName"
|
|
$brokerEndpoint = "https://${brokerDnsName}:8443/v1/authenticate"
|
|
Write-Host "Discovered domain: $DomainName ($DomainNetbios). Computer container: $ComputerOuDn"
|
|
if ($serverIdentity.BrokerService -ne 'Running') {
|
|
throw "The SGU Authentication Broker is not running on $($serverIdentity.ComputerName)."
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($serverIdentity.RustDeskServerAddress) -or
|
|
[string]::IsNullOrWhiteSpace($serverIdentity.RustDeskPublicKey) -or
|
|
$serverIdentity.RustDeskHbbsTask -ne 'Running' -or
|
|
$serverIdentity.RustDeskHbbrTask -ne 'Running') {
|
|
throw "The RustDesk server is not ready on $($serverIdentity.ComputerName). Run the current server bootstrap first."
|
|
}
|
|
|
|
$targetComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
|
|
Invoke-Command -Session $session -ScriptBlock {
|
|
param($ComputerName, $ComputerPath)
|
|
Import-Module ActiveDirectory -ErrorAction Stop
|
|
$samAccountName = "$ComputerName`$"
|
|
$account = Get-ADComputer -Filter "SamAccountName -eq '$samAccountName'" |
|
|
Select-Object -First 1
|
|
if (-not $account) {
|
|
New-ADComputer -Name $ComputerName -SamAccountName $samAccountName `
|
|
-Path $ComputerPath -Enabled $true -ErrorAction Stop
|
|
}
|
|
} -ArgumentList $targetComputerName,$ComputerOuDn
|
|
|
|
if ($publicDirectEnrollment) {
|
|
$directDns = Invoke-Command -Session $session -ScriptBlock {
|
|
param($DnsDomain, $DomainControllerComputerName)
|
|
|
|
$domainControllerFqdn = "$DomainControllerComputerName.$DnsDomain".ToLowerInvariant()
|
|
$dohTemplate = "https://${domainControllerFqdn}:443/dns-query"
|
|
$dohCommand = Get-Command Set-DnsServerEncryptionProtocol -ErrorAction SilentlyContinue
|
|
if (-not $dohCommand) {
|
|
return [pscustomobject]@{
|
|
DohSupported = $false
|
|
DomainControllerFqdn = $domainControllerFqdn
|
|
}
|
|
}
|
|
|
|
$certificate = Get-ChildItem Cert:\LocalMachine\My |
|
|
Where-Object {
|
|
$_.Subject -eq "CN=$domainControllerFqdn" -and
|
|
$_.HasPrivateKey -and
|
|
$_.NotAfter -gt (Get-Date).AddDays(30)
|
|
} |
|
|
Sort-Object NotAfter -Descending |
|
|
Select-Object -First 1
|
|
if (-not $certificate) {
|
|
$certificate = New-SelfSignedCertificate `
|
|
-DnsName $domainControllerFqdn `
|
|
-CertStoreLocation Cert:\LocalMachine\My `
|
|
-FriendlyName 'SGU Direct Enrollment DoH' `
|
|
-Type SSLServerAuthentication `
|
|
-KeyAlgorithm RSA `
|
|
-KeyLength 2048 `
|
|
-HashAlgorithm SHA256 `
|
|
-KeyExportPolicy NonExportable `
|
|
-NotAfter (Get-Date).AddYears(2)
|
|
}
|
|
|
|
$bindingOutput = @(& "$env:SystemRoot\System32\netsh.exe" http show sslcert ipport=0.0.0.0:443 2>&1)
|
|
$bindingExists = $LASTEXITCODE -eq 0
|
|
$normalizedBinding = (($bindingOutput -join '') -replace '[^0-9A-Fa-f]', '').ToUpperInvariant()
|
|
$normalizedThumbprint = ($certificate.Thumbprint -replace ' ', '').ToUpperInvariant()
|
|
if ($bindingExists -and -not $normalizedBinding.Contains($normalizedThumbprint)) {
|
|
throw 'TCP 443 already has an HTTPS certificate binding that is not managed by SGU. Free that port or configure SGU DoH before enrolling this client.'
|
|
}
|
|
if (-not $bindingExists) {
|
|
& "$env:SystemRoot\System32\netsh.exe" http add sslcert `
|
|
ipport=0.0.0.0:443 "certhash=$($certificate.Thumbprint)" `
|
|
"appid={47E9CF26-79B7-4C9D-A0AE-ADFA22447A41}" certstorename=MY | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw 'Could not bind the SGU DoH certificate to TCP 443.' }
|
|
}
|
|
|
|
$dnsChanged = $false
|
|
$encryption = Get-DnsServerEncryptionProtocol
|
|
if (-not $encryption.EnableDoh -or $encryption.UriTemplate -ne $dohTemplate) {
|
|
Set-DnsServerEncryptionProtocol -EnableDoh $true -UriTemplate $dohTemplate
|
|
$dnsChanged = $true
|
|
}
|
|
|
|
Import-Module ActiveDirectory -ErrorAction Stop
|
|
$domainController = Get-ADComputer -Identity $DomainControllerComputerName `
|
|
-Properties ServicePrincipalName
|
|
if (@($domainController.ServicePrincipalName) -notcontains "cifs/$DnsDomain") {
|
|
& "$env:SystemRoot\System32\setspn.exe" -S "cifs/$DnsDomain" $DomainControllerComputerName | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "Could not register cifs/$DnsDomain on $DomainControllerComputerName." }
|
|
}
|
|
|
|
$lanmanPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters'
|
|
$optionalNames = @((Get-ItemProperty $lanmanPath -Name OptionalNames `
|
|
-ErrorAction SilentlyContinue).OptionalNames | Where-Object { $_ })
|
|
$serverChanged = $false
|
|
if ($optionalNames -notcontains $DnsDomain) {
|
|
New-ItemProperty -Path $lanmanPath -Name OptionalNames -PropertyType MultiString `
|
|
-Value (@($optionalNames) + $DnsDomain) -Force | Out-Null
|
|
$serverChanged = $true
|
|
}
|
|
New-ItemProperty -Path $lanmanPath -Name DisableStrictNameChecking `
|
|
-PropertyType DWord -Value 1 -Force | Out-Null
|
|
|
|
if ($serverChanged) {
|
|
Restart-Service LanmanServer -Force
|
|
Start-Service Netlogon
|
|
}
|
|
if ($dnsChanged) {
|
|
Restart-Service DNS -Force
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
DohSupported = $true
|
|
DohTemplate = $dohTemplate
|
|
DohCertificateBase64 = [Convert]::ToBase64String($certificate.RawData)
|
|
DomainControllerFqdn = $domainControllerFqdn
|
|
}
|
|
} -ArgumentList $DomainName,$serverIdentity.ComputerName
|
|
|
|
$directHostNames = @(
|
|
$directDns.DomainControllerFqdn,
|
|
$DomainName,
|
|
$brokerDnsName
|
|
)
|
|
if ([string]$serverIdentity.RustDeskServerAddress -match '[A-Za-z]') {
|
|
$directHostNames += [string]$serverIdentity.RustDeskServerAddress
|
|
}
|
|
Set-ClientHostMappings -ServerAddress $DomainControllerIPv4Address `
|
|
-HostNames $directHostNames
|
|
|
|
if ($directDns.DohSupported -and
|
|
(Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue)) {
|
|
Enable-ClientDnsOverHttps -ServerAddress $DomainControllerIPv4Address `
|
|
-DohTemplate $directDns.DohTemplate `
|
|
-CertificateBase64 $directDns.DohCertificateBase64
|
|
}
|
|
elseif (-not $directDns.DohSupported) {
|
|
Write-Warning 'The server does not support DNS over HTTPS; enrollment will use traditional DNS.'
|
|
}
|
|
else {
|
|
Write-Warning 'This Windows build does not support DNS over HTTPS; enrollment will use traditional DNS.'
|
|
}
|
|
}
|
|
Set-ClientDomainDns -DnsDomain $DomainName -ServerAddress $DomainControllerIPv4Address
|
|
if (-not (Test-ClientDomainDns -DnsDomain $DomainName)) {
|
|
throw "The domain DNS service at $DomainControllerIPv4Address did not return an Active Directory SRV record. For a public server, permit DNS over HTTPS on TCP 443 or traditional DNS from this client network."
|
|
}
|
|
foreach ($port in @(53, 88, 135, 389, 445, 8443)) {
|
|
if (-not (Test-TcpPort -Address $DomainControllerIPv4Address -Port $port -TimeoutMilliseconds 2000)) {
|
|
throw "Server $DomainControllerIPv4Address is reachable, but required TCP port $port is unavailable. Check AD/SGU services and the LAN/VPN firewall. Domain join has not started."
|
|
}
|
|
}
|
|
|
|
$certificateSubject = "CN=SGU Credential Provider Client $env:COMPUTERNAME"
|
|
$clientCertificate = Get-ChildItem Cert:\LocalMachine\My |
|
|
Where-Object {
|
|
$_.Subject -eq $certificateSubject -and
|
|
$_.HasPrivateKey -and
|
|
$_.NotAfter -gt (Get-Date).AddDays(30)
|
|
} |
|
|
Sort-Object NotAfter -Descending |
|
|
Select-Object -First 1
|
|
if (-not $clientCertificate) {
|
|
$clientCertificate = New-SelfSignedCertificate `
|
|
-Subject $certificateSubject `
|
|
-CertStoreLocation Cert:\LocalMachine\My `
|
|
-KeyAlgorithm RSA `
|
|
-KeyLength 3072 `
|
|
-HashAlgorithm SHA256 `
|
|
-KeyExportPolicy NonExportable `
|
|
-NotAfter (Get-Date).AddYears(2) `
|
|
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.2')
|
|
}
|
|
Export-Certificate -Cert $clientCertificate -FilePath $clientCertificatePath -Force | Out-Null
|
|
if (-not (Get-ChildItem Cert:\LocalMachine\Root | Where-Object Thumbprint -eq $clientCertificate.Thumbprint)) {
|
|
Import-Certificate -FilePath $clientCertificatePath `
|
|
-CertStoreLocation Cert:\LocalMachine\Root | Out-Null
|
|
}
|
|
|
|
$remoteTemporaryRoot = Invoke-Command -Session $session -ScriptBlock {
|
|
$path = Join-Path $env:ProgramData ("SGU\Enrollment\Incoming-" + [Guid]::NewGuid().ToString('N'))
|
|
New-Item -ItemType Directory -Path $path -Force | Out-Null
|
|
$path
|
|
}
|
|
$remoteRegistrationScript = Join-Path $remoteTemporaryRoot 'Register-SguClientCertificate.ps1'
|
|
$remoteClientCertificate = Join-Path $remoteTemporaryRoot 'client.cer'
|
|
Copy-Item -LiteralPath (Join-Path $scriptsRoot 'Register-SguClientCertificate.ps1') `
|
|
-Destination $remoteRegistrationScript -ToSession $session
|
|
Copy-Item -LiteralPath $clientCertificatePath `
|
|
-Destination $remoteClientCertificate -ToSession $session
|
|
|
|
Invoke-Command -Session $session -ScriptBlock {
|
|
param($RegistrationScript, $CertificatePath)
|
|
& $RegistrationScript -CertificatePath $CertificatePath | Out-Null
|
|
} -ArgumentList $remoteRegistrationScript,$remoteClientCertificate
|
|
|
|
$serverCertificateBase64 = Invoke-Command -Session $session -ScriptBlock {
|
|
param($ExpectedSubject)
|
|
$certificate = Get-ChildItem Cert:\LocalMachine\My |
|
|
Where-Object {
|
|
$_.Subject -eq "CN=$ExpectedSubject" -and
|
|
$_.HasPrivateKey -and
|
|
$_.NotAfter -gt (Get-Date)
|
|
} |
|
|
Sort-Object NotAfter -Descending |
|
|
Select-Object -First 1
|
|
if (-not $certificate) {
|
|
throw "The broker certificate for $ExpectedSubject is missing."
|
|
}
|
|
[Convert]::ToBase64String($certificate.RawData)
|
|
} -ArgumentList $brokerDnsName
|
|
[IO.File]::WriteAllBytes($serverCertificatePath, [Convert]::FromBase64String($serverCertificateBase64))
|
|
$serverCertificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new($serverCertificatePath)
|
|
Import-Certificate -FilePath $serverCertificatePath `
|
|
-CertStoreLocation Cert:\LocalMachine\Root | Out-Null
|
|
|
|
Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DomainName" -ErrorAction Stop | Out-Null
|
|
$brokerAddress = Resolve-DnsName -Name $brokerDnsName -Type A -ErrorAction Stop |
|
|
Where-Object IPAddress -eq $DomainControllerIPv4Address.IPAddressToString
|
|
if (-not $brokerAddress) {
|
|
throw "$brokerDnsName does not resolve to $DomainControllerIPv4Address."
|
|
}
|
|
|
|
$enrollmentParameters = @{
|
|
PublishPath = $providerPublishPath
|
|
BrokerEndpoint = $brokerEndpoint
|
|
ClientCertificateThumbprint = $clientCertificate.Thumbprint
|
|
ServerCertificateThumbprint = $serverCertificate.Thumbprint
|
|
DomainCredential = $DomainCredential
|
|
DomainName = $DomainName
|
|
DomainNetbios = $DomainNetbios
|
|
DomainControllerDnsName = "$($serverIdentity.ComputerName).$DomainName"
|
|
ComputerOuDn = $ComputerOuDn
|
|
NetworkInterfaceAlias = $NetworkInterfaceAlias
|
|
DomainDnsServerAddresses = @($DomainControllerIPv4Address.IPAddressToString)
|
|
DomainDnsConfigured = $true
|
|
ConnectivityMode = $ConnectivityMode
|
|
RemoteDesktopPrincipal = "$DomainNetbios\SG-Laboratorio-Usuarios-RDP"
|
|
DotNetRuntimeInstallerPath = $runtimeInstaller.FullName
|
|
RustDeskServerAddress = $serverIdentity.RustDeskServerAddress
|
|
RustDeskServerPublicKey = $serverIdentity.RustDeskPublicKey
|
|
SkipRestart = $true
|
|
}
|
|
if ($NewComputerName) {
|
|
$enrollmentParameters.NewComputerName = $NewComputerName
|
|
}
|
|
|
|
$result = & (Join-Path $scriptsRoot 'Enroll-SguDomainClient.ps1') @enrollmentParameters
|
|
|
|
$rustDeskEnrollment = $result.RustDesk
|
|
if (-not $rustDeskEnrollment -or -not $rustDeskEnrollment.RustDeskId -or
|
|
-not $rustDeskEnrollment.AccessPassword) {
|
|
throw 'The client RustDesk enrollment did not provide an ID and protected access credential.'
|
|
}
|
|
$rustDeskPasswordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR(
|
|
$rustDeskEnrollment.AccessPassword)
|
|
try {
|
|
$rustDeskPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($rustDeskPasswordPointer)
|
|
Invoke-Command -Session $session -ScriptBlock {
|
|
param($ComputerName, $RustDeskId, $AccessPassword)
|
|
$registrationScript = Join-Path $env:ProgramData 'SGU\RustDesk\Register-SguRustDeskDevice.ps1'
|
|
if (-not (Test-Path -LiteralPath $registrationScript -PathType Leaf)) {
|
|
throw 'The RustDesk device-registration script is missing on the domain controller.'
|
|
}
|
|
& $registrationScript -ComputerName $ComputerName -RustDeskId $RustDeskId `
|
|
-AccessPassword $AccessPassword | Out-Null
|
|
} -ArgumentList $env:COMPUTERNAME,$rustDeskEnrollment.RustDeskId,$rustDeskPassword
|
|
}
|
|
finally {
|
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($rustDeskPasswordPointer)
|
|
$rustDeskPassword = $null
|
|
}
|
|
$rustDeskEnrollment.PSObject.Properties.Remove('AccessPassword')
|
|
}
|
|
finally {
|
|
if ($session) {
|
|
if ($remoteRegistrationScript -or $remoteClientCertificate) {
|
|
Invoke-Command -Session $session -ScriptBlock {
|
|
param($Paths)
|
|
foreach ($path in $Paths) {
|
|
if ($path) {
|
|
Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
if ($Paths.Count -gt 0 -and $Paths[0]) {
|
|
Remove-Item -LiteralPath (Split-Path $Paths[0] -Parent) `
|
|
-Force -ErrorAction SilentlyContinue
|
|
}
|
|
} -ArgumentList (,@($remoteRegistrationScript,$remoteClientCertificate)) `
|
|
-ErrorAction SilentlyContinue
|
|
}
|
|
Remove-PSSession $session
|
|
}
|
|
if ($null -ne $priorTrustedHosts) {
|
|
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $priorTrustedHosts -Force
|
|
}
|
|
if (-not $winRmWasRunning) {
|
|
Stop-Service WinRM -Force -NoWait -WarningAction SilentlyContinue `
|
|
-ErrorAction SilentlyContinue
|
|
}
|
|
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
$DomainCredential = $null
|
|
$VpnClientCertificatePfxPassword = $null
|
|
}
|
|
|
|
if ($SkipRestart) {
|
|
[pscustomobject]@{
|
|
ComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
|
|
DomainName = $DomainName
|
|
ProviderInstalled = $true
|
|
ClientCertificateRegistered = $true
|
|
BrokerEndpoint = $brokerEndpoint
|
|
ConnectivityMode = $ConnectivityMode
|
|
CompatibilityProfile = $CompatibilityProfile
|
|
VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null }
|
|
RestartRequired = $true
|
|
RustDesk = if ($result) { $result.RustDesk } else { $null }
|
|
EnrollmentResult = $result
|
|
}
|
|
return
|
|
}
|
|
|
|
Restart-Computer -Force
|