Public/Get-HDTNetworkConfiguration.ps1
|
function Get-HDTNetworkConfiguration { <# .SYNOPSIS Reads the machine's current IPv4 configuration - the DHCP lease, or whatever is set. .DESCRIPTION W2's network pane shows what the machine ACTUALLY GOT before asking a technician whether to override it. An empty set of boxes under "Obtain an IP address automatically" tells them nothing; the lease tells them whether the network is working at all, which is usually the question they are really asking. WMI, NOT NetTCPIP, AND THAT IS NOT A STYLE CHOICE. Get-NetIPAddress does not exist in a WinPE image built from the ADK - the NetTCPIP module is simply not there, and it is what killed the first SMB probe. Win32_NetworkAdapterConfiguration is present by construction, because WinPE-WMI is one of the six components Get-HDTBootImageComponent always injects. THE ADAPTER WITH A DEFAULT GATEWAY WINS, and among equals the first one WMI enumerates. On a machine carrying virtual switches the first IPEnabled adapter is routinely a VMnet with an address and no route to anything, so "the first one with an address" is the wrong answer on every developer machine this is built on. IPv4 ONLY, AND ROUTABLE ONLY. Loopback and APIPA are filtered out: 169.254.x.x is precisely the "DHCP did not answer" case, and showing it in the IP box as though it were a lease would tell a technician the opposite of the truth. When nothing routable is present the fields come back empty and HasLease is false, which is the honest answer and the one the pane should display. .PARAMETER CimProvider An ICimProvider. Defaults to the real adapter. .OUTPUTS System.Management.Automation.PSCustomObject with HasLease, IPAddress, SubnetMask, Gateway, DnsServer (string[]), DnsServerText and AdapterDescription. .EXAMPLE $network = Get-HDTNetworkConfiguration if ($network.HasLease) { $network.IPAddress } #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter()] [AllowNull()] [object] $CimProvider ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($null -eq $CimProvider) { $CimProvider = New-HDTCimProvider } $result = [ordered] @{ HasLease = $false IPAddress = '' SubnetMask = '' Gateway = '' DnsServer = [string[]] @() DnsServerText = '' AdapterDescription = '' } # THE ADAPTER WITH A GATEWAY WINS, and this is not cosmetic. On a machine # with virtual switches - this build host has five VMware VMnets, three # Hyper-V vEthernets and a Tailscale tunnel - the FIRST IPEnabled adapter is # a VMnet with an address, no gateway and no route to anywhere. Showing it # in a deployment wizard would tell a technician the network is fine when # the share is unreachable. # # A gateway is the cheapest available proxy for "this adapter can reach # something". Adapters without one are still considered, but last, so a # genuinely gateway-less lab still shows an address rather than nothing. $candidate = @() foreach ($current in @($CimProvider.GetInstance('Win32_NetworkAdapterConfiguration'))) { if (-not $current.IPEnabled) { continue } # @($null).Count IS 1 IN POWERSHELL, and that is what made the first # version of this selection a no-op: an adapter with NO gateway counted # as having one, every adapter compared equal, and the machine showed # whichever VMnet the sort happened to surface. An empty string is not a # route either. tests/unit/Get-HDTNetworkConfiguration.Tests.ps1 pins it # against the captured shape of this host. $gateway = @(@($current.DefaultIPGateway) | Where-Object { -not [string]::IsNullOrWhiteSpace([string] $_) }) $candidate += [pscustomobject] @{ Adapter = $current Gateway = $gateway } } # ROUTED FIRST, ENUMERATION ORDER WITHIN EACH GROUP. Two filtered passes # rather than Sort-Object: sorting is NOT STABLE in Windows PowerShell 5.1 # and -Stable does not exist before 6.2, so which of two equally routed # adapters the wizard displays would otherwise be unspecified. Where-Object # preserves input order, so this is the same answer every run. $ordered = @(@($candidate | Where-Object { $_.Gateway.Count -ge 1 }) + @($candidate | Where-Object { $_.Gateway.Count -eq 0 })) foreach ($entry in $ordered) { $current = $entry.Adapter $address = @($current.IPAddress) $mask = @($current.IPSubnet) # BY POSITION, because IPAddress and IPSubnet are PARALLEL ARRAYS. An # adapter that lists its IPv6 address first would otherwise report that # address's prefix length - '64' - in the subnet mask box. for ($position = 0; $position -lt $address.Count; $position++) { $current4 = [string] $address[$position] # IPv4, routable. See the header: APIPA is the failure case, not a # lease, and must not be shown as one. if ([string]::IsNullOrWhiteSpace($current4)) { continue } if ($current4 -like '*:*' -or $current4 -like '127.*' -or $current4 -like '169.254.*') { continue } $result['HasLease'] = $true $result['IPAddress'] = $current4 $result['AdapterDescription'] = [string] $current.Description if ($position -lt $mask.Count) { $result['SubnetMask'] = [string] $mask[$position] } if ($entry.Gateway.Count -ge 1) { $result['Gateway'] = [string] $entry.Gateway[0] } $dns = @(@($current.DNSServerSearchOrder) | Where-Object { -not [string]::IsNullOrWhiteSpace([string] $_) } | ForEach-Object { [string] $_ }) $result['DnsServer'] = [string[]] $dns $result['DnsServerText'] = ($dns -join ', ') break } if ($result['HasLease']) { break } } return [pscustomobject] $result } |