feat: add macOS pf DNS interception

This commit is contained in:
Codescribe
2026-03-05 04:50:12 -05:00
committed by Cuong Manh Le
parent 395335162f
commit a99dcca288
14 changed files with 4738 additions and 0 deletions
+544
View File
@@ -0,0 +1,544 @@
# =============================================================================
# DNS Intercept Mode Test Script — Windows (WFP)
# =============================================================================
# Run as Administrator: powershell -ExecutionPolicy Bypass -File test-dns-intercept-win.ps1
#
# Tests the dns-intercept feature end-to-end with validation at each step.
# Logs are read from C:\tmp\dns.log (ctrld log location on test machine).
#
# Manual steps marked with [MANUAL] require human interaction.
# =============================================================================
$ErrorActionPreference = "Continue"
$CtrldLog = "C:\tmp\dns.log"
$WfpSubLayerName = "ctrld DNS Intercept"
$Pass = 0
$Fail = 0
$Warn = 0
$Results = @()
# --- Helpers ---
function Header($text) { Write-Host "`n━━━ $text ━━━" -ForegroundColor Cyan }
function Info($text) { Write-Host " $text" }
function Manual($text) { Write-Host " [MANUAL] $text" -ForegroundColor Yellow }
function Separator() { Write-Host "─────────────────────────────────────────────────────" -ForegroundColor Cyan }
function Pass($text) {
Write-Host " ✅ PASS: $text" -ForegroundColor Green
$script:Pass++
$script:Results += "PASS: $text"
}
function Fail($text) {
Write-Host " ❌ FAIL: $text" -ForegroundColor Red
$script:Fail++
$script:Results += "FAIL: $text"
}
function Warn($text) {
Write-Host " ⚠️ WARN: $text" -ForegroundColor Yellow
$script:Warn++
$script:Results += "WARN: $text"
}
function WaitForKey {
Write-Host "`n Press Enter to continue..." -NoNewline
Read-Host
}
function LogGrep($pattern, $lines = 200) {
if (Test-Path $CtrldLog) {
Get-Content $CtrldLog -Tail $lines -ErrorAction SilentlyContinue |
Select-String -Pattern $pattern -ErrorAction SilentlyContinue
}
}
function LogGrepCount($pattern, $lines = 200) {
$matches = LogGrep $pattern $lines
if ($matches) { return @($matches).Count } else { return 0 }
}
# --- Check Admin ---
function Check-Admin {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host "This script must be run as Administrator." -ForegroundColor Red
exit 1
}
}
# =============================================================================
# TEST SECTIONS
# =============================================================================
function Test-Prereqs {
Header "0. Prerequisites"
if (Get-Command nslookup -ErrorAction SilentlyContinue) {
Pass "nslookup available"
} else {
Fail "nslookup not found"
}
if (Get-Command netsh -ErrorAction SilentlyContinue) {
Pass "netsh available"
} else {
Fail "netsh not found"
}
if (Test-Path $CtrldLog) {
Pass "ctrld log exists at $CtrldLog"
} else {
Warn "ctrld log not found at $CtrldLog — log checks will be skipped"
}
# Show current DNS config
Info "Current DNS servers:"
Get-DnsClientServerAddress -AddressFamily IPv4 |
Where-Object { $_.ServerAddresses.Count -gt 0 } |
Format-Table InterfaceAlias, ServerAddresses -AutoSize |
Out-String | ForEach-Object { $_.Trim() } | Write-Host
}
function Test-WfpState {
Header "1. WFP State Validation"
# Export WFP filters and check for ctrld's sublayer/filters
$wfpExport = "$env:TEMP\wfp_filters.xml"
Info "Exporting WFP filters (this may take a few seconds)..."
try {
netsh wfp show filters file=$wfpExport 2>$null | Out-Null
if (Test-Path $wfpExport) {
$wfpContent = Get-Content $wfpExport -Raw -ErrorAction SilentlyContinue
# Check for ctrld sublayer
if ($wfpContent -match "ctrld") {
Pass "WFP filters contain 'ctrld' references"
# Count filters
$filterMatches = ([regex]::Matches($wfpContent, "ctrld")).Count
Info "Found $filterMatches 'ctrld' references in WFP export"
} else {
Fail "No 'ctrld' references found in WFP filters"
}
# Check for DNS port 53 filters
if ($wfpContent -match "port.*53" -or $wfpContent -match "0x0035") {
Pass "Port 53 filter conditions found in WFP"
} else {
Warn "Could not confirm port 53 filters in WFP export"
}
Remove-Item $wfpExport -ErrorAction SilentlyContinue
} else {
Warn "WFP export file not created"
}
} catch {
Warn "Could not export WFP filters: $_"
}
Separator
# Alternative: Check via PowerShell WFP cmdlets if available
Info "Checking WFP via netsh wfp show state..."
$wfpState = netsh wfp show state 2>$null
if ($wfpState) {
Info "WFP state export completed (check $env:TEMP for details)"
}
# Check Windows Firewall service is running
$fwService = Get-Service -Name "mpssvc" -ErrorAction SilentlyContinue
if ($fwService -and $fwService.Status -eq "Running") {
Pass "Windows Firewall service (BFE/WFP) is running"
} else {
Fail "Windows Firewall service not running — WFP won't work"
}
# Check BFE (Base Filtering Engine)
$bfeService = Get-Service -Name "BFE" -ErrorAction SilentlyContinue
if ($bfeService -and $bfeService.Status -eq "Running") {
Pass "Base Filtering Engine (BFE) is running"
} else {
Fail "BFE not running — WFP requires this service"
}
}
function Test-DnsInterception {
Header "2. DNS Interception Tests"
# Mark log position
$logLinesBefore = 0
if (Test-Path $CtrldLog) {
$logLinesBefore = @(Get-Content $CtrldLog -ErrorAction SilentlyContinue).Count
}
# Test 1: Query to external resolver should be intercepted
Info "Test: nslookup example.com 8.8.8.8 (should be intercepted by ctrld)"
$result = $null
try {
$result = nslookup example.com 8.8.8.8 2>&1 | Out-String
} catch { }
if ($result -and $result -match "\d+\.\d+\.\d+\.\d+") {
Pass "nslookup @8.8.8.8 returned a result"
# Check which server answered
if ($result -match "Server:\s+(\S+)") {
$server = $Matches[1]
Info "Answered by server: $server"
if ($server -match "127\.0\.0\.1|localhost") {
Pass "Response came from localhost (ctrld intercepted)"
} elseif ($server -match "8\.8\.8\.8") {
Fail "Response came from 8.8.8.8 directly — NOT intercepted"
}
}
} else {
Fail "nslookup @8.8.8.8 failed or returned no address"
}
# Check ctrld logged it
Start-Sleep -Seconds 1
if (Test-Path $CtrldLog) {
$newLines = Get-Content $CtrldLog -ErrorAction SilentlyContinue |
Select-Object -Skip $logLinesBefore
$intercepted = $newLines | Select-String "example.com" -ErrorAction SilentlyContinue
if ($intercepted) {
Pass "ctrld logged the intercepted query for example.com"
} else {
Fail "ctrld did NOT log query for example.com"
}
}
Separator
# Test 2: Another external resolver
Info "Test: nslookup cloudflare.com 1.1.1.1 (should also be intercepted)"
try {
$result2 = nslookup cloudflare.com 1.1.1.1 2>&1 | Out-String
if ($result2 -match "\d+\.\d+\.\d+\.\d+") {
Pass "nslookup @1.1.1.1 returned result"
} else {
Fail "nslookup @1.1.1.1 failed"
}
} catch {
Fail "nslookup @1.1.1.1 threw exception"
}
Separator
# Test 3: Query to localhost should work (no loop)
Info "Test: nslookup example.org 127.0.0.1 (direct to ctrld, no loop)"
try {
$result3 = nslookup example.org 127.0.0.1 2>&1 | Out-String
if ($result3 -match "\d+\.\d+\.\d+\.\d+") {
Pass "nslookup @127.0.0.1 works (no loop)"
} else {
Fail "nslookup @127.0.0.1 failed — possible loop"
}
} catch {
Fail "nslookup @127.0.0.1 exception — possible loop"
}
Separator
# Test 4: System DNS via Resolve-DnsName
Info "Test: Resolve-DnsName example.net (system resolver)"
try {
$result4 = Resolve-DnsName example.net -Type A -ErrorAction Stop
if ($result4) {
Pass "System DNS resolution works (Resolve-DnsName)"
}
} catch {
Fail "System DNS resolution failed: $_"
}
Separator
# Test 5: TCP DNS
Info "Test: nslookup -vc example.com 9.9.9.9 (TCP DNS)"
try {
$result5 = nslookup -vc example.com 9.9.9.9 2>&1 | Out-String
if ($result5 -match "\d+\.\d+\.\d+\.\d+") {
Pass "TCP DNS query intercepted and resolved"
} else {
Warn "TCP DNS query may not have been intercepted"
}
} catch {
Warn "TCP DNS test inconclusive"
}
}
function Test-NonDnsUnaffected {
Header "3. Non-DNS Traffic Unaffected"
# HTTPS
Info "Test: Invoke-WebRequest https://example.com (HTTPS should NOT be affected)"
try {
$web = Invoke-WebRequest -Uri "https://example.com" -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
if ($web.StatusCode -eq 200) {
Pass "HTTPS works (HTTP 200)"
} else {
Pass "HTTPS returned HTTP $($web.StatusCode)"
}
} catch {
Fail "HTTPS failed: $_"
}
# Test non-53 port connectivity
Info "Test: Test-NetConnection to github.com:443 (non-DNS port)"
try {
$nc = Test-NetConnection -ComputerName "github.com" -Port 443 -WarningAction SilentlyContinue
if ($nc.TcpTestSucceeded) {
Pass "Port 443 reachable (non-DNS traffic unaffected)"
} else {
Warn "Port 443 unreachable (may be firewall)"
}
} catch {
Warn "Test-NetConnection failed: $_"
}
}
function Test-CtrldLogHealth {
Header "4. ctrld Log Health Check"
if (-not (Test-Path $CtrldLog)) {
Warn "Skipping log checks — $CtrldLog not found"
return
}
# Check for WFP initialization
if (LogGrepCount "initializing Windows Filtering Platform" 500) {
Pass "WFP initialization logged"
} else {
Fail "No WFP initialization in recent logs"
}
# Check for successful WFP engine open
if (LogGrepCount "WFP engine opened" 500) {
Pass "WFP engine opened successfully"
} else {
Fail "WFP engine open not found in logs"
}
# Check for sublayer creation
if (LogGrepCount "WFP sublayer created" 500) {
Pass "WFP sublayer created"
} else {
Fail "WFP sublayer creation not logged"
}
# Check for filter creation
$filterCount = LogGrepCount "added WFP.*filter" 500
if ($filterCount -gt 0) {
Pass "WFP filters added ($filterCount filter log entries)"
} else {
Fail "No WFP filter creation logged"
}
# Check for permit-localhost filters
if (LogGrepCount "permit.*localhost\|permit.*127\.0\.0\.1" 500) {
Pass "Localhost permit filters logged"
} else {
Warn "Localhost permit filters not explicitly logged"
}
Separator
# Check for errors
Info "Recent errors in ctrld log:"
$errors = LogGrep '"level":"error"' 500
if ($errors) {
$errors | Select-Object -Last 5 | ForEach-Object { Write-Host " $_" }
Warn "Errors found in recent logs"
} else {
Pass "No errors in recent logs"
}
# Warnings (excluding expected ones)
$warnings = LogGrep '"level":"warn"' 500 | Where-Object {
$_ -notmatch "skipping self-upgrade"
}
if ($warnings) {
Info "Warnings:"
$warnings | Select-Object -Last 5 | ForEach-Object { Write-Host " $_" }
}
# VPN DNS detection
$vpnLogs = LogGrep "VPN DNS" 500
if ($vpnLogs) {
Info "VPN DNS activity:"
$vpnLogs | Select-Object -Last 5 | ForEach-Object { Write-Host " $_" }
} else {
Info "No VPN DNS activity (expected if no VPN connected)"
}
}
function Test-CleanupOnStop {
Header "5. Cleanup Validation (After ctrld Stop)"
Manual "Stop ctrld now (ctrld stop or Ctrl+C), then press Enter"
WaitForKey
Start-Sleep -Seconds 2
# Check WFP filters are removed
$wfpExport = "$env:TEMP\wfp_after_stop.xml"
try {
netsh wfp show filters file=$wfpExport 2>$null | Out-Null
if (Test-Path $wfpExport) {
$content = Get-Content $wfpExport -Raw -ErrorAction SilentlyContinue
if ($content -match "ctrld") {
Fail "WFP still contains 'ctrld' filters after stop"
} else {
Pass "WFP filters cleaned up after stop"
}
Remove-Item $wfpExport -ErrorAction SilentlyContinue
}
} catch {
Warn "Could not verify WFP cleanup"
}
# DNS should work normally
Info "Test: nslookup example.com (should work via system DNS)"
try {
$result = nslookup example.com 2>&1 | Out-String
if ($result -match "\d+\.\d+\.\d+\.\d+") {
Pass "DNS works after ctrld stop"
} else {
Fail "DNS broken after ctrld stop"
}
} catch {
Fail "DNS exception after ctrld stop"
}
}
function Test-RestartResilience {
Header "6. Restart Resilience"
Manual "Start ctrld again with --dns-intercept, then press Enter"
WaitForKey
Start-Sleep -Seconds 3
# Quick interception test
Info "Test: nslookup example.com 8.8.8.8 (should be intercepted after restart)"
try {
$result = nslookup example.com 8.8.8.8 2>&1 | Out-String
if ($result -match "\d+\.\d+\.\d+\.\d+") {
Pass "DNS interception works after restart"
} else {
Fail "DNS interception broken after restart"
}
} catch {
Fail "DNS test failed after restart"
}
# Check WFP filters restored
if (LogGrepCount "WFP engine opened" 100) {
Pass "WFP re-initialized after restart"
}
}
function Test-NetworkChange {
Header "7. Network Change Recovery"
Info "This test verifies recovery after network changes."
Manual "Switch Wi-Fi networks, or disable/re-enable network adapter, then press Enter"
WaitForKey
Start-Sleep -Seconds 5
# Test interception still works
Info "Test: nslookup example.com 8.8.8.8 (should still be intercepted)"
try {
$result = nslookup example.com 8.8.8.8 2>&1 | Out-String
if ($result -match "\d+\.\d+\.\d+\.\d+") {
Pass "DNS interception works after network change"
} else {
Fail "DNS interception broken after network change"
}
} catch {
Fail "DNS test failed after network change"
}
# Check logs for recovery/network events
if (Test-Path $CtrldLog) {
$recoveryLogs = LogGrep "recovery|network change|network monitor" 100
if ($recoveryLogs) {
Info "Recovery/network log entries:"
$recoveryLogs | Select-Object -Last 5 | ForEach-Object { Write-Host " $_" }
}
}
}
# =============================================================================
# SUMMARY
# =============================================================================
function Print-Summary {
Header "TEST SUMMARY"
Write-Host ""
foreach ($r in $Results) {
if ($r.StartsWith("PASS")) {
Write-Host "$($r.Substring(6))" -ForegroundColor Green
} elseif ($r.StartsWith("FAIL")) {
Write-Host "$($r.Substring(6))" -ForegroundColor Red
} elseif ($r.StartsWith("WARN")) {
Write-Host " ⚠️ $($r.Substring(6))" -ForegroundColor Yellow
}
}
Write-Host ""
Separator
Write-Host " Passed: $Pass | Failed: $Fail | Warnings: $Warn"
Separator
if ($Fail -gt 0) {
Write-Host "`n Some tests failed. Debug commands:" -ForegroundColor Red
Write-Host " netsh wfp show filters # dump all WFP filters"
Write-Host " Get-Content $CtrldLog -Tail 100 # recent ctrld logs"
Write-Host " Get-DnsClientServerAddress # current DNS config"
Write-Host " netsh wfp show state # WFP state dump"
} else {
Write-Host "`n All tests passed!" -ForegroundColor Green
}
}
# =============================================================================
# MAIN
# =============================================================================
Write-Host "╔═══════════════════════════════════════════════════════╗" -ForegroundColor White
Write-Host "║ ctrld DNS Intercept Mode — Windows Test Suite ║" -ForegroundColor White
Write-Host "║ Tests WFP-based DNS interception ║" -ForegroundColor White
Write-Host "╚═══════════════════════════════════════════════════════╝" -ForegroundColor White
Check-Admin
Write-Host ""
Write-Host "Make sure ctrld is running with --dns-intercept before starting."
Write-Host "Log location: $CtrldLog"
WaitForKey
Test-Prereqs
Test-WfpState
Test-DnsInterception
Test-NonDnsUnaffected
Test-CtrldLogHealth
Separator
Write-Host ""
Write-Host "The next tests require manual steps (stop/start ctrld, network changes)."
Write-Host "Press Enter to continue, or Ctrl+C to skip and see results so far."
WaitForKey
Test-CleanupOnStop
Test-RestartResilience
Test-NetworkChange
Print-Summary