| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- # net-ops :: windows/probe.ps1
- # Full layered diagnostic ladder for Windows network troubleshooting.
- # Designed to be invoked over SSH via -EncodedCommand. Outputs structured
- # sections so a human or LLM can scan for the first FAIL and drill in.
- param(
- [string]$TestHost = "google.com",
- [string[]]$TestIPs = @("1.1.1.1","8.8.8.8"),
- [int]$Timeout = 5,
- [switch]$Redact,
- [switch]$JsonOutput
- )
- # If -Redact, self-reinvoke without the switch and pipe output through a
- # regex-driven redactor. Preserves Tailscale's well-known 100.100.100.100
- # anchor and public DoH IPs as diagnostic landmarks.
- if ($Redact) {
- $cleanArgs = @(
- '-TestHost', $TestHost,
- '-TestIPs', ($TestIPs -join ',')
- '-Timeout', $Timeout
- )
- if ($JsonOutput) { $cleanArgs += '-JsonOutput' }
- & powershell -NoProfile -File $PSCommandPath @cleanArgs |
- ForEach-Object {
- $line = $_
- $line = $line -replace '100\.100\.100\.100','__TS_MAGIC__'
- $line = $line -replace '\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b','10.X.X.X'
- $line = $line -replace '\b172\.(1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}\b','172.X.X.X'
- $line = $line -replace '\b192\.168\.\d{1,3}\.\d{1,3}\b','192.168.X.X'
- $line = $line -replace '\b100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3}\b','100.X.X.X'
- $line = $line -replace '\b169\.254\.\d{1,3}\.\d{1,3}\b','169.254.X.X'
- $line = $line -replace '\b[0-9a-fA-F]{2}([:-])[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\1[0-9a-fA-F]{2}\b','XX:XX:XX:XX:XX:XX'
- $line = $line -replace '\b[a-z0-9-]+\.ts\.net\b','REDACTED.ts.net'
- $line = $line -replace '__TS_MAGIC__','100.100.100.100'
- Write-Output $line
- }
- exit $LASTEXITCODE
- }
- $script:PASS_COUNT = 0
- $script:FAIL_COUNT = 0
- $script:FIRST_FAIL = ""
- $script:CURRENT_SECTION = ""
- function Section($name) {
- $script:CURRENT_SECTION = $name
- Write-Output ""
- Write-Output ("=== " + $name + " ===")
- }
- function Result($label, $ok, $detail = "") {
- if ($ok) {
- $script:PASS_COUNT++
- $tag = "PASS"
- } else {
- $script:FAIL_COUNT++
- if (-not $script:FIRST_FAIL) {
- $script:FIRST_FAIL = "[" + $script:CURRENT_SECTION + "] " + $label
- }
- $tag = "FAIL"
- }
- Write-Output ("[" + $tag + "] " + $label + $(if ($detail) { " :: " + $detail } else { "" }))
- }
- # ---------------------------------------------------------------------------
- Section "1. LINK LAYER"
- # ---------------------------------------------------------------------------
- $adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" }
- if (!$adapters) {
- Result "Any interface up" $false "No interfaces in Up state"
- } else {
- $adapters | ForEach-Object {
- Result ("Interface " + $_.Name) $true ($_.LinkSpeed + ", MAC " + $_.MacAddress)
- }
- }
- $cfg = Get-NetIPConfiguration | Where-Object { $_.NetAdapter.Status -eq "Up" }
- $cfg | Format-Table InterfaceAlias, IPv4Address, IPv4DefaultGateway -AutoSize | Out-String | Write-Output
- # ---------------------------------------------------------------------------
- Section "2. IP / ICMP REACHABILITY"
- # ---------------------------------------------------------------------------
- $gateway = ($cfg | Where-Object { $_.IPv4DefaultGateway } | Select-Object -First 1).IPv4DefaultGateway.NextHop
- if ($gateway) {
- $r = Test-Connection $gateway -Count 2 -Quiet -ErrorAction SilentlyContinue
- Result ("Ping gateway $gateway") $r
- }
- foreach ($ip in $TestIPs) {
- $r = Test-Connection $ip -Count 2 -Quiet -ErrorAction SilentlyContinue
- Result ("Ping $ip") $r
- }
- # ---------------------------------------------------------------------------
- Section "3. TCP/UDP SOCKET REACHABILITY"
- # ---------------------------------------------------------------------------
- foreach ($ip in $TestIPs) {
- $tcp53 = Test-NetConnection $ip -Port 53 -InformationLevel Quiet -WarningAction SilentlyContinue
- $tcp443 = Test-NetConnection $ip -Port 443 -InformationLevel Quiet -WarningAction SilentlyContinue
- Result ("TCP/53 -> $ip") $tcp53
- Result ("TCP/443 -> $ip") $tcp443
- }
- # Raw UDP/53 — bypasses DNS Client API, proves whether DNS protocol itself works.
- foreach ($ip in $TestIPs) {
- try {
- $u = New-Object System.Net.Sockets.UdpClient
- $u.Client.ReceiveTimeout = ($Timeout * 1000)
- $u.Client.SendTimeout = ($Timeout * 1000)
- $u.Connect($ip, 53)
- # Minimal DNS query for google.com A record
- $q = [byte[]](0x12,0x34,0x01,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x67,0x6f,0x6f,0x67,0x6c,0x65,0x03,0x63,0x6f,0x6d,0x00,0x00,0x01,0x00,0x01)
- [void]$u.Send($q, $q.Length)
- $ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0)
- $resp = $u.Receive([ref]$ep)
- Result ("Raw UDP/53 -> $ip") $true ($resp.Length.ToString() + " bytes")
- $u.Close()
- } catch {
- Result ("Raw UDP/53 -> $ip") $false $_.Exception.Message
- }
- }
- # ---------------------------------------------------------------------------
- Section "4. DNS INFRASTRUCTURE (bypass tools)"
- # ---------------------------------------------------------------------------
- foreach ($srv in @("default") + $TestIPs) {
- $cmd = if ($srv -eq "default") { "nslookup $TestHost" } else { "nslookup $TestHost $srv" }
- $out = Invoke-Expression $cmd 2>&1 | Out-String
- $resolved = $out -match "Addresses?:\s+(\d+\.\d+\.\d+\.\d+|[0-9a-f:]+)"
- Result ("nslookup via $srv") $resolved
- if (!$resolved) { Write-Output " --- output ---"; $out | Select-String -Pattern "." | Select-Object -First 6 | ForEach-Object { Write-Output (" " + $_) } }
- }
- # ---------------------------------------------------------------------------
- Section "5. WINDOWS DNS CLIENT API (the hook layer)"
- # ---------------------------------------------------------------------------
- try {
- $r = Resolve-DnsName $TestHost -Type A -QuickTimeout -ErrorAction Stop
- $ips = ($r | Where-Object { $_.Type -eq "A" } | Select-Object -ExpandProperty IPAddress) -join ", "
- Result "Resolve-DnsName (system API)" $true $ips
- } catch {
- Result "Resolve-DnsName (system API)" $false $_.Exception.Message
- }
- # If layer 4 passed but layer 5 failed, dump the NRPT — that's the prime suspect.
- $nrptRules = Get-DnsClientNrptRule -ErrorAction SilentlyContinue
- $catchAll = $nrptRules | Where-Object { $_.Namespace -eq "." }
- if ($catchAll) {
- Write-Output " !! Catch-all NRPT rule(s) detected (likely culprit):"
- $catchAll | Format-Table Name, NameServers, Comment -AutoSize | Out-String | Write-Output
- }
- # DNS Client service status
- $dnsClient = Get-Service Dnscache -ErrorAction SilentlyContinue
- Result "DNS Client (Dnscache) service running" ($dnsClient.Status -eq "Running")
- # Port 53 listeners on the box itself
- $listeners = Get-NetUDPEndpoint -LocalPort 53 -ErrorAction SilentlyContinue
- if ($listeners) {
- Write-Output " Port 53 listeners on localhost:"
- $listeners | ForEach-Object {
- $p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
- $svc = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -eq $_.OwningProcess } | Select-Object -First 1
- Write-Output (" " + $_.LocalAddress + ":53 PID=" + $_.OwningProcess + " " + $p.ProcessName + $(if ($svc) { " (" + $svc.Name + ")" } else { "" }))
- }
- }
- # ---------------------------------------------------------------------------
- Section "6. APPLICATION LAYER (real HTTP request)"
- # ---------------------------------------------------------------------------
- foreach ($url in @("https://www.google.com","https://github.com")) {
- try {
- $r = Invoke-WebRequest -Uri $url -TimeoutSec $Timeout -UseBasicParsing -ErrorAction Stop
- Result ("GET $url") $true ("HTTP " + $r.StatusCode + ", " + $r.RawContentLength + " bytes")
- } catch {
- Result ("GET $url") $false $_.Exception.Message
- }
- }
- # ---------------------------------------------------------------------------
- Section "7. KNOWN VPN / DNS CLIENT FOOTPRINT"
- # ---------------------------------------------------------------------------
- # AV products (drives "Encrypted DNS Detection" type blocks)
- $av = Get-CimInstance -Namespace "root/SecurityCenter2" -ClassName AntiVirusProduct -ErrorAction SilentlyContinue
- if ($av) { $av | Select-Object displayName, productState | Format-Table -AutoSize | Out-String | Write-Output }
- # WFP-callout drivers (third-party kernel hooks on network stack)
- $wfp = Get-CimInstance Win32_SystemDriver | Where-Object {
- $_.State -eq "Running" -and ($_.Name -match "epfwwfp|wfpcap|netbtsmb|pctcore|symefa|mfewfpk|kvfwwfp|bdfwfpf|cbfsfilter")
- }
- if ($wfp) {
- Write-Output " Third-party WFP/network drivers active:"
- $wfp | Format-Table Name, State, PathName -AutoSize | Out-String | Write-Output
- }
- # Known VPN clients (common NRPT rule creators)
- $vpnPaths = @(
- "C:\Program Files\Proton\VPN",
- "C:\Program Files\Mullvad VPN",
- "C:\Program Files (x86)\OpenVPN",
- "C:\Program Files\WireGuard",
- "C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client",
- "C:\Program Files\NordVPN",
- "C:\Program Files (x86)\NextDNS"
- )
- $found = $vpnPaths | Where-Object { Test-Path $_ }
- if ($found) {
- Write-Output " VPN / DNS clients installed:"
- $found | ForEach-Object { Write-Output (" " + $_) }
- }
- Write-Output ""
- Write-Output "=== SUMMARY ==="
- Write-Output (" PASS: " + $script:PASS_COUNT + " FAIL: " + $script:FAIL_COUNT)
- if ($script:FIRST_FAIL) {
- Write-Output (" First failure: " + $script:FIRST_FAIL)
- $next = switch -Wildcard ($script:FIRST_FAIL) {
- "*LINK LAYER*" { "check Get-NetAdapter, Get-NetIPConfiguration, DHCP state" }
- "*SOCKET*" { "check Windows Firewall outbound rules; AV protocol filtering; consumer router DoH IP blocking" }
- "*ICMP*" { "check Get-NetRoute, ISP/upstream connectivity" }
- "*DNS INFRASTRUCTURE*" { "check UDP/53 outbound, router DNS forwarder" }
- "*DNS CLIENT API*" { "scripts\\windows\\nrpt-audit.ps1 # drill rung 5 (the hook layer)" }
- "*RESOLVER PATH*" { "scripts\\windows\\nrpt-audit.ps1 # drill rung 5 (the hook layer)" }
- "*APPLICATION*" { "check netsh winhttp show proxy, cert store, IPv6 preference" }
- default { "re-run with -Verbose; check references/common-culprits.md" }
- }
- Write-Output (" Next: " + $next)
- } else {
- Write-Output " No failures. If user still reports issues, see rung 7 footprint and time-based notes in references/diagnostic-ladder.md."
- }
- Write-Output ""
- Write-Output "=== END PROBE ==="
|