Files
ctrld/test-scripts/windows/test-recovery-bypass.ps1
2026-03-10 17:17:45 +07:00

290 lines
12 KiB
PowerShell

# test-recovery-bypass.ps1 — Test DNS intercept recovery bypass (captive portal simulation)
#
# Simulates a captive portal by:
# 1. Discovering ctrld's upstream IPs from active connections
# 2. Blocking them via Windows Firewall rules
# 3. Disabling/re-enabling the wifi adapter to trigger network change
# 4. Verifying recovery bypass forwards to OS/DHCP resolver
# 5. Removing firewall rules and verifying normal operation resumes
#
# SAFE: Uses named firewall rules that are cleaned up on exit.
#
# Usage (run as Administrator):
# .\test-recovery-bypass.ps1 [-WifiAdapter "Wi-Fi"] [-CtrldLog "C:\temp\dns.log"]
#
# Prerequisites:
# - ctrld running with --dns-intercept and -v 1 --log C:\temp\dns.log
# - Run as Administrator
param(
[string]$WifiAdapter = "Wi-Fi",
[string]$CtrldLog = "C:\temp\dns.log",
[int]$BlockDurationSec = 60
)
$ErrorActionPreference = "Stop"
$FwRulePrefix = "ctrld-test-recovery-block"
$BlockedIPs = @()
function Log($msg) { Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $msg" -ForegroundColor Cyan }
function Pass($msg) { Write-Host "[PASS] $msg" -ForegroundColor Green }
function Fail($msg) { Write-Host "[FAIL] $msg" -ForegroundColor Red }
function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
# ── Safety: cleanup function ─────────────────────────────────────────────────
function Cleanup {
Log "═══ CLEANUP ═══"
# Ensure wifi is enabled
Log "Ensuring wifi adapter is enabled..."
try { Enable-NetAdapter -Name $WifiAdapter -Confirm:$false -ErrorAction SilentlyContinue } catch {}
# Remove all test firewall rules
Log "Removing test firewall rules..."
Get-NetFirewallRule -DisplayName "$FwRulePrefix*" -ErrorAction SilentlyContinue |
Remove-NetFirewallRule -ErrorAction SilentlyContinue
Log "Cleanup complete."
}
# Register cleanup on script exit
$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Cleanup } -ErrorAction SilentlyContinue
trap { Cleanup; break }
# ── Pre-checks ───────────────────────────────────────────────────────────────
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Fail "Run as Administrator!"
exit 1
}
if (-not (Test-Path $CtrldLog)) {
Fail "ctrld log not found at $CtrldLog"
Write-Host "Start ctrld with: ctrld run --dns-intercept --cd <uid> -v 1 --log $CtrldLog"
exit 1
}
# Check wifi adapter exists
$adapter = Get-NetAdapter -Name $WifiAdapter -ErrorAction SilentlyContinue
if (-not $adapter) {
Fail "Wifi adapter '$WifiAdapter' not found"
Write-Host "Available adapters:"
Get-NetAdapter | Format-Table Name, Status, InterfaceDescription
exit 1
}
Log "═══════════════════════════════════════════════════════════"
Log " Recovery Bypass Test (Captive Portal Simulation)"
Log "═══════════════════════════════════════════════════════════"
Log "Wifi adapter: $WifiAdapter"
Log "ctrld log: $CtrldLog"
Write-Host ""
# ── Phase 1: Discover upstream IPs ──────────────────────────────────────────
Log "Phase 1: Discovering ctrld upstream IPs from active connections"
$ctrldConns = Get-NetTCPConnection -OwningProcess (Get-Process ctrld* -ErrorAction SilentlyContinue).Id -ErrorAction SilentlyContinue |
Where-Object { $_.State -eq "Established" -and $_.RemotePort -eq 443 }
$upstreamIPs = @()
if ($ctrldConns) {
$upstreamIPs = $ctrldConns | Select-Object -ExpandProperty RemoteAddress -Unique |
Where-Object { $_ -notmatch "^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)" }
foreach ($conn in $ctrldConns) {
Log " $($conn.LocalAddress):$($conn.LocalPort) -> $($conn.RemoteAddress):$($conn.RemotePort)"
}
}
# Also resolve known Control D endpoints
foreach ($host_ in @("dns.controld.com", "freedns.controld.com")) {
try {
$resolved = Resolve-DnsName $host_ -Type A -ErrorAction SilentlyContinue
$resolved | ForEach-Object { if ($_.IPAddress) { $upstreamIPs += $_.IPAddress } }
} catch {}
}
$upstreamIPs = $upstreamIPs | Sort-Object -Unique
if ($upstreamIPs.Count -eq 0) {
Fail "Could not discover any upstream IPs!"
exit 1
}
Log "Found $($upstreamIPs.Count) upstream IP(s):"
foreach ($ip in $upstreamIPs) { Log " $ip" }
Write-Host ""
# ── Phase 2: Baseline ───────────────────────────────────────────────────────
Log "Phase 2: Baseline — verify DNS works normally"
$baseline = Resolve-DnsName example.com -Server 127.0.0.1 -Type A -ErrorAction SilentlyContinue
if ($baseline) {
Pass "Baseline: example.com -> $($baseline[0].IPAddress)"
} else {
Fail "DNS not working!"
exit 1
}
$logLinesBefore = (Get-Content $CtrldLog).Count
Log "Log position: line $logLinesBefore"
Write-Host ""
# ── Phase 3: Block upstream IPs via Windows Firewall ────────────────────────
Log "Phase 3: Blocking upstream IPs via Windows Firewall"
foreach ($ip in $upstreamIPs) {
$ruleName = "$FwRulePrefix-$ip"
# Remove existing rule if any
Remove-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
# Block outbound to this IP
New-NetFirewallRule -DisplayName $ruleName -Direction Outbound -Action Block `
-RemoteAddress $ip -Protocol TCP -RemotePort 443 `
-Description "Temporary test rule for ctrld recovery bypass test" | Out-Null
$BlockedIPs += $ip
Log " Blocked: $ip (outbound TCP 443)"
}
Pass "All $($upstreamIPs.Count) upstream IPs blocked"
Write-Host ""
# ── Phase 4: Cycle wifi ─────────────────────────────────────────────────────
Log "Phase 4: Cycling wifi to trigger network change event"
Log " Disabling $WifiAdapter..."
Disable-NetAdapter -Name $WifiAdapter -Confirm:$false
Start-Sleep -Seconds 3
Log " Enabling $WifiAdapter..."
Enable-NetAdapter -Name $WifiAdapter -Confirm:$false
Log " Waiting for wifi to reconnect (up to 20s)..."
$wifiUp = $false
for ($i = 0; $i -lt 20; $i++) {
$status = (Get-NetAdapter -Name $WifiAdapter).Status
if ($status -eq "Up") {
# Check for IP
$ipAddr = (Get-NetIPAddress -InterfaceAlias $WifiAdapter -AddressFamily IPv4 -ErrorAction SilentlyContinue).IPAddress
if ($ipAddr) {
$wifiUp = $true
Pass "Wifi reconnected: $WifiAdapter -> $ipAddr"
break
}
}
Start-Sleep -Seconds 1
}
if (-not $wifiUp) {
Fail "Wifi did not reconnect in 20s!"
Cleanup
exit 1
}
Log " Waiting 5s for ctrld network monitor..."
Start-Sleep -Seconds 5
Write-Host ""
# ── Phase 5: Query and watch for recovery ────────────────────────────────────
Log "Phase 5: Sending queries — upstream blocked, recovery should activate"
Write-Host ""
$recoveryDetected = $false
$bypassActive = $false
$dnsDuringBypass = $false
for ($q = 1; $q -le 30; $q++) {
$result = $null
try {
$result = Resolve-DnsName "example.com" -Server 127.0.0.1 -Type A -DnsOnly -ErrorAction SilentlyContinue
} catch {}
if ($result) {
Log " Query #$q`: example.com -> $($result[0].IPAddress)"
} else {
Log " Query #$q`: example.com -> FAIL ✗"
}
# Check ctrld log for recovery
$newLogs = Get-Content $CtrldLog | Select-Object -Skip $logLinesBefore
$logText = $newLogs -join "`n"
if (-not $recoveryDetected -and ($logText -match "enabling DHCP bypass|triggering recovery|No healthy")) {
Write-Host ""
Pass "🎯 Recovery flow triggered!"
$recoveryDetected = $true
}
if (-not $bypassActive -and ($logText -match "Recovery bypass active")) {
Pass "🔄 Recovery bypass forwarding to OS/DHCP resolver"
$bypassActive = $true
}
if ($recoveryDetected -and $result) {
Pass "✅ DNS resolves during recovery: example.com -> $($result[0].IPAddress)"
$dnsDuringBypass = $true
break
}
Start-Sleep -Seconds 2
}
# ── Phase 6: Show log entries ────────────────────────────────────────────────
Write-Host ""
Log "Phase 6: Recovery-related ctrld log entries"
Log "────────────────────────────────────────────"
$newLogs = Get-Content $CtrldLog | Select-Object -Skip $logLinesBefore
$relevant = $newLogs | Where-Object { $_ -match "recovery|bypass|DHCP|unhealthy|upstream.*fail|No healthy|network change|OS resolver" }
if ($relevant) {
$relevant | Select-Object -First 30 | ForEach-Object { Write-Host " $_" }
} else {
Warn "No recovery-related log entries found"
Get-Content $CtrldLog | Select-Object -Last 10 | ForEach-Object { Write-Host " $_" }
}
# ── Phase 7: Unblock and verify ─────────────────────────────────────────────
Write-Host ""
Log "Phase 7: Removing firewall blocks"
Get-NetFirewallRule -DisplayName "$FwRulePrefix*" -ErrorAction SilentlyContinue |
Remove-NetFirewallRule -ErrorAction SilentlyContinue
$BlockedIPs = @()
Pass "Firewall rules removed"
Log "Waiting for recovery (up to 30s)..."
$logLinesUnblock = (Get-Content $CtrldLog).Count
$recoveryComplete = $false
for ($i = 0; $i -lt 15; $i++) {
try { Resolve-DnsName example.com -Server 127.0.0.1 -Type A -DnsOnly -ErrorAction SilentlyContinue } catch {}
$postLogs = (Get-Content $CtrldLog | Select-Object -Skip $logLinesUnblock) -join "`n"
if ($postLogs -match "recovery complete|disabling DHCP bypass|Upstream.*recovered") {
$recoveryComplete = $true
Pass "ctrld recovered — normal operation resumed"
break
}
Start-Sleep -Seconds 2
}
if (-not $recoveryComplete) { Warn "Recovery completion not detected (may need more time)" }
# ── Phase 8: Final check ────────────────────────────────────────────────────
Write-Host ""
Log "Phase 8: Final DNS verification"
Start-Sleep -Seconds 2
$final = Resolve-DnsName example.com -Server 127.0.0.1 -Type A -ErrorAction SilentlyContinue
if ($final) {
Pass "DNS working: example.com -> $($final[0].IPAddress)"
} else {
Fail "DNS not resolving"
}
# ── Summary ──────────────────────────────────────────────────────────────────
Write-Host ""
Log "═══════════════════════════════════════════════════════════"
Log " Test Summary"
Log "═══════════════════════════════════════════════════════════"
if ($recoveryDetected) { Pass "Recovery bypass activated" } else { Fail "Recovery bypass NOT activated" }
if ($bypassActive) { Pass "Queries forwarded to OS/DHCP" } else { Warn "OS resolver forwarding not confirmed" }
if ($dnsDuringBypass) { Pass "DNS resolved during bypass" } else { Warn "DNS during bypass not confirmed" }
if ($recoveryComplete) { Pass "Normal operation resumed" } else { Warn "Recovery completion not confirmed" }
if ($final) { Pass "DNS functional at end of test" } else { Fail "DNS broken at end of test" }
Write-Host ""
Log "Full log: Get-Content $CtrldLog | Select-Object -Skip $logLinesBefore"
# Cleanup runs via trap
Cleanup