###################################################################################### #### windows-network-smoke-tests.ps1 — Verify Windows network configuration #### #### Checks NIC status, IP config, gateway, DNS, DC, firewall, NTP, link speed. #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version: 1.0 #### #### #### #### Usage: .\windows-network-smoke-tests.ps1 #### #### .\windows-network-smoke-tests.ps1 -Gateway 10.0.0.1 #### #### .\windows-network-smoke-tests.ps1 -OutputFormat tap #### #### #### #### See -Help for all options. #### ###################################################################################### [CmdletBinding()] param( [string]$Gateway, [string]$DnsServer, [string]$DomainController, [int]$ExpectedLinkSpeedMbps = 1000, [string]$NtpServer, [string]$TestUrl = "http://www.msftconnecttest.com/connecttest.txt", [ValidateSet("text", "tap")] [string]$OutputFormat = "text", [switch]$NoColor, [switch]$Help ) $ErrorActionPreference = "Continue" # ============================================================================ # STATE & HELPERS # ============================================================================ $script:Pass = 0; $script:Fail = 0; $script:Skip = 0; $script:Total = 0 $script:Results = @(); $script:StartTime = $null function Write-Color { param([string]$Text, [string]$Color = "White") if ($NoColor) { Write-Host $Text } else { Write-Host $Text -ForegroundColor $Color } } function Write-Log { param([string]$Msg) Write-Color "[INFO] $Msg" "Cyan" } function Write-Warn { param([string]$Msg) Write-Color "[WARN] $Msg" "Yellow" } function Write-Err { param([string]$Msg) Write-Color "[ERROR] $Msg" "Red" } function Record-Pass { param([string]$Name, [string]$Detail = "") $script:Pass++; $script:Total++ $script:Results += [PSCustomObject]@{ Status = "PASS"; Name = $Name; Detail = $Detail } if ($OutputFormat -eq "tap") { Write-Host "ok $($script:Total) - $Name" } else { $msg = " $(if ($NoColor) { '[PASS]' } else { [char]0x2713 }) $Name" if ($Detail) { $msg += " - $Detail" } Write-Color $msg "Green" } } function Record-Fail { param([string]$Name, [string]$Detail = "") $script:Fail++; $script:Total++ $script:Results += [PSCustomObject]@{ Status = "FAIL"; Name = $Name; Detail = $Detail } if ($OutputFormat -eq "tap") { Write-Host "not ok $($script:Total) - $Name" if ($Detail) { Write-Host " # $Detail" } } else { $msg = " $(if ($NoColor) { '[FAIL]' } else { [char]0x2717 }) $Name" if ($Detail) { $msg += " - $Detail" } Write-Color $msg "Red" } } function Record-Skip { param([string]$Name, [string]$Reason = "") $script:Skip++; $script:Total++ $script:Results += [PSCustomObject]@{ Status = "SKIP"; Name = $Name; Detail = $Reason } if ($OutputFormat -eq "tap") { Write-Host "ok $($script:Total) - $Name # SKIP $Reason" } else { $msg = " $(if ($NoColor) { '[SKIP]' } else { [char]0x2298 }) $Name" if ($Reason) { $msg += " - $Reason" } Write-Color $msg "Yellow" } } function Write-TapHeader { Write-Host "TAP version 13" } function Write-TapFooter { Write-Host "1..$($script:Total)" Write-Host "# pass $($script:Pass)" Write-Host "# fail $($script:Fail)" Write-Host "# skip $($script:Skip)" } function Write-Summary { $duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds) Write-Host "" $separator = [string]::new([char]0x2500, 50) Write-Color $separator "White" Write-Color "Windows Network Smoke Tests - Summary" "White" $summaryLine = " $($script:Pass) passed $($script:Fail) failed $($script:Skip) skipped (${duration}s)" Write-Host $summaryLine Write-Color $separator "White" if ($script:Fail -eq 0) { Write-Color "All tests passed." "Green" } else { Write-Color "$($script:Fail) test(s) failed." "Red" } } function Show-Usage { @" Usage: .\windows-network-smoke-tests.ps1 [OPTIONS] Smoke-test Windows network configuration. PowerShell 5.1+, no modules. Auto-detects gateway, DNS, DC, and NTP when not specified. Parameters: -Gateway STR Default gateway IP (default: auto-detect) -DnsServer STR DNS server IP (default: auto-detect) -DomainController STR Domain controller hostname/IP (default: auto-detect) -ExpectedLinkSpeedMbps N Minimum link speed in Mbps (default: 1000) -NtpServer STR NTP server (default: auto-detect from w32tm) -TestUrl STR URL for internet test (default: msftconnecttest.com) -OutputFormat FORMAT Output: text (default), tap -NoColor Disable colored output -Help Show this help message Examples: .\windows-network-smoke-tests.ps1 .\windows-network-smoke-tests.ps1 -Gateway 10.0.0.1 -DnsServer 10.0.0.53 .\windows-network-smoke-tests.ps1 -DomainController dc01.corp.local .\windows-network-smoke-tests.ps1 -ExpectedLinkSpeedMbps 10000 .\windows-network-smoke-tests.ps1 -OutputFormat tap .\windows-network-smoke-tests.ps1 -NoColor "@ } # ============================================================================ # TESTS # ============================================================================ function Test-NicStatus { try { $adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" } if ($adapters -and @($adapters).Count -gt 0) { $first = @($adapters)[0].Name Record-Pass "NIC status ($first is Up)" } else { Record-Fail "NIC status" "no adapter is Up" } } catch { Record-Fail "NIC status" $_.Exception.Message } } function Test-IpConfiguration { try { $ips = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.PrefixOrigin -ne "WellKnown" } if ($ips) { $first = @($ips)[0] Record-Pass "IPv4 address assigned ($($first.IPAddress)/$($first.PrefixLength))" } else { Record-Fail "IPv4 address assigned" "no IPv4 address found" } } catch { Record-Fail "IPv4 address assigned" $_.Exception.Message } } function Test-DefaultGateway { $gw = $Gateway if (-not $gw) { try { $route = Get-NetRoute -DestinationPrefix "0.0.0.0/0" -ErrorAction Stop | Select-Object -First 1 $gw = $route.NextHop } catch { Record-Fail "default gateway exists" "no default route found" return } } if (-not $gw -or $gw -eq "0.0.0.0") { Record-Fail "default gateway exists" "no gateway configured" return } Record-Pass "default gateway exists ($gw)" try { $ping = Test-Connection -ComputerName $gw -Count 2 -Quiet -ErrorAction Stop if ($ping) { Record-Pass "default gateway reachable" } else { Record-Fail "default gateway reachable" "$gw did not respond" } } catch { Record-Fail "default gateway reachable" $_.Exception.Message } } function Test-DnsServers { $dns = $DnsServer if (-not $dns) { try { $dnsAddrs = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { $_.ServerAddresses.Count -gt 0 } | Select-Object -ExpandProperty ServerAddresses -First 1 if ($dnsAddrs) { $dns = @($dnsAddrs)[0] } } catch { } } if ($dns) { Record-Pass "DNS servers configured ($dns)" } else { Record-Fail "DNS servers configured" "no DNS servers found" } } function Test-DnsResolution { try { $result = Resolve-DnsName -Name "www.microsoft.com" -Type A -DnsOnly -ErrorAction Stop if ($result) { Record-Pass "DNS resolution (www.microsoft.com)" } else { Record-Fail "DNS resolution" "no result for www.microsoft.com" } } catch { Record-Fail "DNS resolution" $_.Exception.Message } } function Test-DomainController { $dc = $DomainController if (-not $dc) { try { $nltest = nltest /dsgetdc: 2>&1 $dcLine = $nltest | Select-String "DC: \\\\" if ($dcLine) { $dc = ($dcLine -replace ".*DC: \\\\", "").Trim() } } catch { } } if (-not $dc) { $isDomain = $false try { $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop if ($cs.PartOfDomain) { $isDomain = $true } } catch { } if (-not $isDomain) { Record-Skip "domain controller reachable" "not domain-joined" Record-Skip "domain controller LDAP port 389" "not domain-joined" } else { Record-Fail "domain controller reachable" "domain-joined but DC not found" Record-Skip "domain controller LDAP port 389" "DC not identified" } return } try { $ping = Test-Connection -ComputerName $dc -Count 2 -Quiet -ErrorAction Stop if ($ping) { Record-Pass "domain controller reachable ($dc)" } else { Record-Fail "domain controller reachable" "$dc did not respond" } } catch { Record-Fail "domain controller reachable" $_.Exception.Message } try { $ldap = Test-NetConnection -ComputerName $dc -Port 389 -WarningAction SilentlyContinue -ErrorAction Stop if ($ldap.TcpTestSucceeded) { Record-Pass "domain controller LDAP port 389" } else { Record-Fail "domain controller LDAP port 389" "TCP 389 connection failed" } } catch { Record-Fail "domain controller LDAP port 389" $_.Exception.Message } } function Test-FirewallProfile { try { $profiles = Get-NetFirewallProfile -ErrorAction Stop | Where-Object { $_.Enabled -eq $true } if (-not $profiles) { Record-Fail "firewall profile" "no firewall profile is enabled" return } $activeNames = ($profiles | ForEach-Object { $_.Name }) -join ", " $hasPublic = $profiles | Where-Object { $_.Name -eq "Public" } if ($hasPublic) { Record-Fail "firewall profile ($activeNames)" "Public profile is active - expected Domain or Private" } else { Record-Pass "firewall profile ($activeNames)" } } catch { Record-Fail "firewall profile" $_.Exception.Message } } function Test-FirewallRules { $checks = @( @{ Name = "RDP"; Pattern = "*Remote Desktop*" } @{ Name = "WinRM"; Pattern = "*Windows Remote Management*" } @{ Name = "ICMP"; Pattern = "*ICMPv4*" } ) $found = @(); $missing = @() foreach ($check in $checks) { try { $rule = Get-NetFirewallRule -Enabled True -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like $check.Pattern } | Select-Object -First 1 if ($rule) { $found += $check.Name } else { $missing += $check.Name } } catch { $missing += $check.Name } } if ($missing.Count -eq 0) { Record-Pass "firewall rules ($($found -join ', '))" } elseif ($found.Count -gt 0) { Record-Fail "firewall rules" "missing: $($missing -join ', '); found: $($found -join ', ')" } else { Record-Fail "firewall rules" "none of RDP, WinRM, ICMP rules found enabled" } } function Test-NtpSync { try { $status = w32tm /query /status 2>&1 if ($LASTEXITCODE -ne 0) { Record-Fail "NTP synchronization" "w32tm query failed - is the Windows Time service running?" return } $sourceLine = $status | Select-String "Source:" $source = if ($sourceLine) { ($sourceLine -replace "Source:\s*", "").Trim() } else { "unknown" } if ($source -match "Free-Running|Local CMOS|VM IC") { Record-Fail "NTP synchronization" "source is $source - not synced to a remote server" return } $lastSync = $status | Select-String "Last Successful Sync Time:" if ($lastSync) { Record-Pass "NTP synchronized ($source)" } else { Record-Fail "NTP synchronization" "no successful sync recorded" } } catch { Record-Fail "NTP synchronization" $_.Exception.Message } } function Test-TimeDrift { $ntp = $NtpServer if (-not $ntp) { try { $cfg = w32tm /query /status 2>&1 $sourceLine = $cfg | Select-String "Source:" if ($sourceLine) { $ntp = ($sourceLine -replace "Source:\s*", "").Trim() } } catch { } } if (-not $ntp -or $ntp -match "Free-Running|Local CMOS|VM IC") { Record-Skip "time drift" "no valid NTP source available" return } try { $strip = w32tm /stripchart /computer:$ntp /samples:1 /dataonly 2>&1 if ($LASTEXITCODE -ne 0) { Record-Fail "time drift" "w32tm stripchart failed for $ntp" return } $offsetLine = $strip | Select-String "[\+\-]?\d+\.\d+s" | Select-Object -Last 1 if ($offsetLine) { $match = [regex]::Match($offsetLine, "([\+\-]?\d+\.\d+)s") if ($match.Success) { $offset = [math]::Abs([double]$match.Groups[1].Value) if ($offset -le 1.0) { Record-Pass "time drift (${offset}s offset from $ntp)" } else { Record-Fail "time drift" "${offset}s offset exceeds 1.0s threshold" } } else { Record-Fail "time drift" "could not parse offset from w32tm output" } } else { Record-Fail "time drift" "no offset data in w32tm output" } } catch { Record-Fail "time drift" $_.Exception.Message } } function Test-LinkSpeed { try { $adapter = Get-NetAdapter | Where-Object { $_.Status -eq "Up" } | Select-Object -First 1 if (-not $adapter) { Record-Fail "link speed" "no active adapter found"; return } $linkSpeedStr = $adapter.LinkSpeed $speedMbps = 0 if ($linkSpeedStr -match "(\d+(\.\d+)?)\s*Gbps") { $speedMbps = [int]([double]$Matches[1] * 1000) } elseif ($linkSpeedStr -match "(\d+)\s*Mbps") { $speedMbps = [int]$Matches[1] } elseif ($linkSpeedStr -match "(\d+)\s*Kbps") { $speedMbps = [int]([double]$Matches[1] / 1000) } if ($speedMbps -ge $ExpectedLinkSpeedMbps) { Record-Pass "link speed ($speedMbps Mbps >= $ExpectedLinkSpeedMbps Mbps)" } else { Record-Fail "link speed" "$speedMbps Mbps is below expected $ExpectedLinkSpeedMbps Mbps" } } catch { Record-Fail "link speed" $_.Exception.Message } } function Test-VlanTagging { try { $adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" } if (-not $adapters) { Record-Skip "VLAN tagging" "no active adapter"; return } $tagged = @($adapters) | Where-Object { $_.VlanID -and $_.VlanID -ne 0 } if ($tagged) { $first = @($tagged)[0] Record-Pass "VLAN tagging ($($first.Name) VLAN $($first.VlanID))" } else { Record-Skip "VLAN tagging" "adapter not VLAN-tagged" } } catch { Record-Skip "VLAN tagging" "VlanID property not available" } } function Test-InternetConnectivity { try { $response = Invoke-WebRequest -Uri $TestUrl -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop if ($response.StatusCode -eq 200) { Record-Pass "internet connectivity (HTTP $($response.StatusCode))" } else { Record-Fail "internet connectivity" "HTTP $($response.StatusCode)" } } catch { Record-Fail "internet connectivity" $_.Exception.Message } } # ============================================================================ # MAIN # ============================================================================ if ($Help) { Show-Usage; exit 0 } $script:StartTime = Get-Date if ($OutputFormat -eq "tap") { Write-TapHeader } else { Write-Host "" Write-Color "Windows Network Smoke Tests" "White" Write-Host "Host: $($env:COMPUTERNAME)" Write-Host "Time: $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')" Write-Host "" } Test-NicStatus Test-IpConfiguration Test-DefaultGateway Test-DnsServers Test-DnsResolution Test-DomainController Test-FirewallProfile Test-FirewallRules Test-NtpSync Test-TimeDrift Test-LinkSpeed Test-VlanTagging Test-InternetConnectivity if ($OutputFormat -eq "tap") { Write-TapFooter } else { Write-Summary } if ($script:Fail -eq 0) { exit 0 } else { exit 1 }