# 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 ==="
