probe.ps1 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. # net-ops :: windows/probe.ps1
  2. # Full layered diagnostic ladder for Windows network troubleshooting.
  3. # Designed to be invoked over SSH via -EncodedCommand. Outputs structured
  4. # sections so a human or LLM can scan for the first FAIL and drill in.
  5. param(
  6. [string]$TestHost = "google.com",
  7. [string[]]$TestIPs = @("1.1.1.1","8.8.8.8"),
  8. [int]$Timeout = 5,
  9. [switch]$Redact,
  10. [switch]$JsonOutput
  11. )
  12. # If -Redact, self-reinvoke without the switch and pipe output through a
  13. # regex-driven redactor. Preserves Tailscale's well-known 100.100.100.100
  14. # anchor and public DoH IPs as diagnostic landmarks.
  15. if ($Redact) {
  16. $cleanArgs = @(
  17. '-TestHost', $TestHost,
  18. '-TestIPs', ($TestIPs -join ',')
  19. '-Timeout', $Timeout
  20. )
  21. if ($JsonOutput) { $cleanArgs += '-JsonOutput' }
  22. & powershell -NoProfile -File $PSCommandPath @cleanArgs |
  23. ForEach-Object {
  24. $line = $_
  25. $line = $line -replace '100\.100\.100\.100','__TS_MAGIC__'
  26. $line = $line -replace '\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b','10.X.X.X'
  27. $line = $line -replace '\b172\.(1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}\b','172.X.X.X'
  28. $line = $line -replace '\b192\.168\.\d{1,3}\.\d{1,3}\b','192.168.X.X'
  29. $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'
  30. $line = $line -replace '\b169\.254\.\d{1,3}\.\d{1,3}\b','169.254.X.X'
  31. $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'
  32. $line = $line -replace '\b[a-z0-9-]+\.ts\.net\b','REDACTED.ts.net'
  33. $line = $line -replace '__TS_MAGIC__','100.100.100.100'
  34. Write-Output $line
  35. }
  36. exit $LASTEXITCODE
  37. }
  38. $script:PASS_COUNT = 0
  39. $script:FAIL_COUNT = 0
  40. $script:FIRST_FAIL = ""
  41. $script:CURRENT_SECTION = ""
  42. function Section($name) {
  43. $script:CURRENT_SECTION = $name
  44. Write-Output ""
  45. Write-Output ("=== " + $name + " ===")
  46. }
  47. function Result($label, $ok, $detail = "") {
  48. if ($ok) {
  49. $script:PASS_COUNT++
  50. $tag = "PASS"
  51. } else {
  52. $script:FAIL_COUNT++
  53. if (-not $script:FIRST_FAIL) {
  54. $script:FIRST_FAIL = "[" + $script:CURRENT_SECTION + "] " + $label
  55. }
  56. $tag = "FAIL"
  57. }
  58. Write-Output ("[" + $tag + "] " + $label + $(if ($detail) { " :: " + $detail } else { "" }))
  59. }
  60. # ---------------------------------------------------------------------------
  61. Section "1. LINK LAYER"
  62. # ---------------------------------------------------------------------------
  63. $adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" }
  64. if (!$adapters) {
  65. Result "Any interface up" $false "No interfaces in Up state"
  66. } else {
  67. $adapters | ForEach-Object {
  68. Result ("Interface " + $_.Name) $true ($_.LinkSpeed + ", MAC " + $_.MacAddress)
  69. }
  70. }
  71. $cfg = Get-NetIPConfiguration | Where-Object { $_.NetAdapter.Status -eq "Up" }
  72. $cfg | Format-Table InterfaceAlias, IPv4Address, IPv4DefaultGateway -AutoSize | Out-String | Write-Output
  73. # ---------------------------------------------------------------------------
  74. Section "2. IP / ICMP REACHABILITY"
  75. # ---------------------------------------------------------------------------
  76. $gateway = ($cfg | Where-Object { $_.IPv4DefaultGateway } | Select-Object -First 1).IPv4DefaultGateway.NextHop
  77. if ($gateway) {
  78. $r = Test-Connection $gateway -Count 2 -Quiet -ErrorAction SilentlyContinue
  79. Result ("Ping gateway $gateway") $r
  80. }
  81. foreach ($ip in $TestIPs) {
  82. $r = Test-Connection $ip -Count 2 -Quiet -ErrorAction SilentlyContinue
  83. Result ("Ping $ip") $r
  84. }
  85. # ---------------------------------------------------------------------------
  86. Section "3. TCP/UDP SOCKET REACHABILITY"
  87. # ---------------------------------------------------------------------------
  88. foreach ($ip in $TestIPs) {
  89. $tcp53 = Test-NetConnection $ip -Port 53 -InformationLevel Quiet -WarningAction SilentlyContinue
  90. $tcp443 = Test-NetConnection $ip -Port 443 -InformationLevel Quiet -WarningAction SilentlyContinue
  91. Result ("TCP/53 -> $ip") $tcp53
  92. Result ("TCP/443 -> $ip") $tcp443
  93. }
  94. # Raw UDP/53 — bypasses DNS Client API, proves whether DNS protocol itself works.
  95. foreach ($ip in $TestIPs) {
  96. try {
  97. $u = New-Object System.Net.Sockets.UdpClient
  98. $u.Client.ReceiveTimeout = ($Timeout * 1000)
  99. $u.Client.SendTimeout = ($Timeout * 1000)
  100. $u.Connect($ip, 53)
  101. # Minimal DNS query for google.com A record
  102. $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)
  103. [void]$u.Send($q, $q.Length)
  104. $ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0)
  105. $resp = $u.Receive([ref]$ep)
  106. Result ("Raw UDP/53 -> $ip") $true ($resp.Length.ToString() + " bytes")
  107. $u.Close()
  108. } catch {
  109. Result ("Raw UDP/53 -> $ip") $false $_.Exception.Message
  110. }
  111. }
  112. # ---------------------------------------------------------------------------
  113. Section "4. DNS INFRASTRUCTURE (bypass tools)"
  114. # ---------------------------------------------------------------------------
  115. foreach ($srv in @("default") + $TestIPs) {
  116. $cmd = if ($srv -eq "default") { "nslookup $TestHost" } else { "nslookup $TestHost $srv" }
  117. $out = Invoke-Expression $cmd 2>&1 | Out-String
  118. $resolved = $out -match "Addresses?:\s+(\d+\.\d+\.\d+\.\d+|[0-9a-f:]+)"
  119. Result ("nslookup via $srv") $resolved
  120. if (!$resolved) { Write-Output " --- output ---"; $out | Select-String -Pattern "." | Select-Object -First 6 | ForEach-Object { Write-Output (" " + $_) } }
  121. }
  122. # ---------------------------------------------------------------------------
  123. Section "5. WINDOWS DNS CLIENT API (the hook layer)"
  124. # ---------------------------------------------------------------------------
  125. try {
  126. $r = Resolve-DnsName $TestHost -Type A -QuickTimeout -ErrorAction Stop
  127. $ips = ($r | Where-Object { $_.Type -eq "A" } | Select-Object -ExpandProperty IPAddress) -join ", "
  128. Result "Resolve-DnsName (system API)" $true $ips
  129. } catch {
  130. Result "Resolve-DnsName (system API)" $false $_.Exception.Message
  131. }
  132. # If layer 4 passed but layer 5 failed, dump the NRPT — that's the prime suspect.
  133. $nrptRules = Get-DnsClientNrptRule -ErrorAction SilentlyContinue
  134. $catchAll = $nrptRules | Where-Object { $_.Namespace -eq "." }
  135. if ($catchAll) {
  136. Write-Output " !! Catch-all NRPT rule(s) detected (likely culprit):"
  137. $catchAll | Format-Table Name, NameServers, Comment -AutoSize | Out-String | Write-Output
  138. }
  139. # DNS Client service status
  140. $dnsClient = Get-Service Dnscache -ErrorAction SilentlyContinue
  141. Result "DNS Client (Dnscache) service running" ($dnsClient.Status -eq "Running")
  142. # Port 53 listeners on the box itself
  143. $listeners = Get-NetUDPEndpoint -LocalPort 53 -ErrorAction SilentlyContinue
  144. if ($listeners) {
  145. Write-Output " Port 53 listeners on localhost:"
  146. $listeners | ForEach-Object {
  147. $p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
  148. $svc = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -eq $_.OwningProcess } | Select-Object -First 1
  149. Write-Output (" " + $_.LocalAddress + ":53 PID=" + $_.OwningProcess + " " + $p.ProcessName + $(if ($svc) { " (" + $svc.Name + ")" } else { "" }))
  150. }
  151. }
  152. # ---------------------------------------------------------------------------
  153. Section "6. APPLICATION LAYER (real HTTP request)"
  154. # ---------------------------------------------------------------------------
  155. foreach ($url in @("https://www.google.com","https://github.com")) {
  156. try {
  157. $r = Invoke-WebRequest -Uri $url -TimeoutSec $Timeout -UseBasicParsing -ErrorAction Stop
  158. Result ("GET $url") $true ("HTTP " + $r.StatusCode + ", " + $r.RawContentLength + " bytes")
  159. } catch {
  160. Result ("GET $url") $false $_.Exception.Message
  161. }
  162. }
  163. # ---------------------------------------------------------------------------
  164. Section "7. KNOWN VPN / DNS CLIENT FOOTPRINT"
  165. # ---------------------------------------------------------------------------
  166. # AV products (drives "Encrypted DNS Detection" type blocks)
  167. $av = Get-CimInstance -Namespace "root/SecurityCenter2" -ClassName AntiVirusProduct -ErrorAction SilentlyContinue
  168. if ($av) { $av | Select-Object displayName, productState | Format-Table -AutoSize | Out-String | Write-Output }
  169. # WFP-callout drivers (third-party kernel hooks on network stack)
  170. $wfp = Get-CimInstance Win32_SystemDriver | Where-Object {
  171. $_.State -eq "Running" -and ($_.Name -match "epfwwfp|wfpcap|netbtsmb|pctcore|symefa|mfewfpk|kvfwwfp|bdfwfpf|cbfsfilter")
  172. }
  173. if ($wfp) {
  174. Write-Output " Third-party WFP/network drivers active:"
  175. $wfp | Format-Table Name, State, PathName -AutoSize | Out-String | Write-Output
  176. }
  177. # Known VPN clients (common NRPT rule creators)
  178. $vpnPaths = @(
  179. "C:\Program Files\Proton\VPN",
  180. "C:\Program Files\Mullvad VPN",
  181. "C:\Program Files (x86)\OpenVPN",
  182. "C:\Program Files\WireGuard",
  183. "C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client",
  184. "C:\Program Files\NordVPN",
  185. "C:\Program Files (x86)\NextDNS"
  186. )
  187. $found = $vpnPaths | Where-Object { Test-Path $_ }
  188. if ($found) {
  189. Write-Output " VPN / DNS clients installed:"
  190. $found | ForEach-Object { Write-Output (" " + $_) }
  191. }
  192. Write-Output ""
  193. Write-Output "=== SUMMARY ==="
  194. Write-Output (" PASS: " + $script:PASS_COUNT + " FAIL: " + $script:FAIL_COUNT)
  195. if ($script:FIRST_FAIL) {
  196. Write-Output (" First failure: " + $script:FIRST_FAIL)
  197. $next = switch -Wildcard ($script:FIRST_FAIL) {
  198. "*LINK LAYER*" { "check Get-NetAdapter, Get-NetIPConfiguration, DHCP state" }
  199. "*SOCKET*" { "check Windows Firewall outbound rules; AV protocol filtering; consumer router DoH IP blocking" }
  200. "*ICMP*" { "check Get-NetRoute, ISP/upstream connectivity" }
  201. "*DNS INFRASTRUCTURE*" { "check UDP/53 outbound, router DNS forwarder" }
  202. "*DNS CLIENT API*" { "scripts\\windows\\nrpt-audit.ps1 # drill rung 5 (the hook layer)" }
  203. "*RESOLVER PATH*" { "scripts\\windows\\nrpt-audit.ps1 # drill rung 5 (the hook layer)" }
  204. "*APPLICATION*" { "check netsh winhttp show proxy, cert store, IPv6 preference" }
  205. default { "re-run with -Verbose; check references/common-culprits.md" }
  206. }
  207. Write-Output (" Next: " + $next)
  208. } else {
  209. Write-Output " No failures. If user still reports issues, see rung 7 footprint and time-based notes in references/diagnostic-ladder.md."
  210. }
  211. Write-Output ""
  212. Write-Output "=== END PROBE ==="