mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-13 10:26:06 +00:00
557 lines
18 KiB
Bash
Executable File
557 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
||
# =============================================================================
|
||
# DNS Intercept Mode Test Script — macOS (pf)
|
||
# =============================================================================
|
||
# Run as root: sudo bash test-dns-intercept-mac.sh
|
||
#
|
||
# Tests the dns-intercept feature end-to-end with validation at each step.
|
||
# Logs are read from /tmp/dns.log (ctrld log location on test machine).
|
||
#
|
||
# Manual steps marked with [MANUAL] require human interaction.
|
||
# =============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
CTRLD_LOG="/tmp/dns.log"
|
||
PF_ANCHOR="com.controld.ctrld"
|
||
PASS=0
|
||
FAIL=0
|
||
WARN=0
|
||
RESULTS=()
|
||
|
||
# Colors
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
header() { echo -e "\n${CYAN}${BOLD}━━━ $1 ━━━${NC}"; }
|
||
info() { echo -e " ${BOLD}ℹ${NC} $1"; }
|
||
pass() { echo -e " ${GREEN}✅ PASS${NC}: $1"; PASS=$((PASS+1)); RESULTS+=("PASS: $1"); }
|
||
fail() { echo -e " ${RED}❌ FAIL${NC}: $1"; FAIL=$((FAIL+1)); RESULTS+=("FAIL: $1"); }
|
||
warn() { echo -e " ${YELLOW}⚠️ WARN${NC}: $1"; WARN=$((WARN+1)); RESULTS+=("WARN: $1"); }
|
||
manual() { echo -e " ${YELLOW}[MANUAL]${NC} $1"; }
|
||
separator() { echo -e "${CYAN}─────────────────────────────────────────────────────${NC}"; }
|
||
|
||
check_root() {
|
||
if [[ $EUID -ne 0 ]]; then
|
||
echo -e "${RED}This script must be run as root (sudo).${NC}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
wait_for_key() {
|
||
echo -e "\n Press ${BOLD}Enter${NC} to continue..."
|
||
read -r
|
||
}
|
||
|
||
# Grep recent log entries (last N lines)
|
||
log_grep() {
|
||
local pattern="$1"
|
||
local lines="${2:-200}"
|
||
tail -n "$lines" "$CTRLD_LOG" 2>/dev/null | grep -i "$pattern" 2>/dev/null || true
|
||
}
|
||
|
||
log_grep_count() {
|
||
local pattern="$1"
|
||
local lines="${2:-200}"
|
||
tail -n "$lines" "$CTRLD_LOG" 2>/dev/null | grep -ci "$pattern" 2>/dev/null || echo "0"
|
||
}
|
||
|
||
# =============================================================================
|
||
# TEST SECTIONS
|
||
# =============================================================================
|
||
|
||
test_prereqs() {
|
||
header "0. Prerequisites"
|
||
|
||
if command -v pfctl &>/dev/null; then
|
||
pass "pfctl available"
|
||
else
|
||
fail "pfctl not found"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -f "$CTRLD_LOG" ]]; then
|
||
pass "ctrld log exists at $CTRLD_LOG"
|
||
else
|
||
warn "ctrld log not found at $CTRLD_LOG — log checks will be skipped"
|
||
fi
|
||
|
||
if command -v dig &>/dev/null; then
|
||
pass "dig available"
|
||
else
|
||
fail "dig not found — install bind tools"
|
||
exit 1
|
||
fi
|
||
|
||
info "Default route interface: $(route -n get default 2>/dev/null | grep interface | awk '{print $2}' || echo 'unknown')"
|
||
info "Current DNS servers:"
|
||
scutil --dns | grep "nameserver\[" | head -5 | sed 's/^/ /'
|
||
}
|
||
|
||
test_pf_state() {
|
||
header "1. PF State Validation"
|
||
|
||
# Is pf enabled?
|
||
local pf_status
|
||
pf_status=$(pfctl -si 2>&1 | grep "Status:" || true)
|
||
if echo "$pf_status" | grep -q "Enabled"; then
|
||
pass "pf is enabled"
|
||
else
|
||
fail "pf is NOT enabled (status: $pf_status)"
|
||
fi
|
||
|
||
# Is our anchor referenced in the running ruleset?
|
||
local sr_match sn_match
|
||
sr_match=$(pfctl -sr 2>&1 | grep "$PF_ANCHOR" || true)
|
||
sn_match=$(pfctl -sn 2>&1 | grep "$PF_ANCHOR" || true)
|
||
|
||
if [[ -n "$sr_match" ]]; then
|
||
pass "anchor '$PF_ANCHOR' found in filter rules (pfctl -sr)"
|
||
info " $sr_match"
|
||
else
|
||
fail "anchor '$PF_ANCHOR' NOT in filter rules — main ruleset doesn't reference it"
|
||
fi
|
||
|
||
if [[ -n "$sn_match" ]]; then
|
||
pass "rdr-anchor '$PF_ANCHOR' found in NAT rules (pfctl -sn)"
|
||
info " $sn_match"
|
||
else
|
||
fail "rdr-anchor '$PF_ANCHOR' NOT in NAT rules — redirect won't work"
|
||
fi
|
||
|
||
# Check anchor rules
|
||
separator
|
||
info "Anchor filter rules (pfctl -a '$PF_ANCHOR' -sr):"
|
||
local anchor_sr
|
||
anchor_sr=$(pfctl -a "$PF_ANCHOR" -sr 2>&1 | grep -v "ALTQ" || true)
|
||
if [[ -n "$anchor_sr" ]]; then
|
||
echo "$anchor_sr" | sed 's/^/ /'
|
||
# Check for route-to rules
|
||
if echo "$anchor_sr" | grep -q "route-to"; then
|
||
pass "route-to lo0 rules present (needed for local traffic interception)"
|
||
else
|
||
warn "No route-to rules found — local DNS may not be intercepted"
|
||
fi
|
||
else
|
||
fail "No filter rules in anchor"
|
||
fi
|
||
|
||
info "Anchor redirect rules (pfctl -a '$PF_ANCHOR' -sn):"
|
||
local anchor_sn
|
||
anchor_sn=$(pfctl -a "$PF_ANCHOR" -sn 2>&1 | grep -v "ALTQ" || true)
|
||
if [[ -n "$anchor_sn" ]]; then
|
||
echo "$anchor_sn" | sed 's/^/ /'
|
||
if echo "$anchor_sn" | grep -q "rdr.*lo0.*port = 53"; then
|
||
pass "rdr rules on lo0 present (redirect DNS to ctrld)"
|
||
else
|
||
warn "rdr rules don't match expected pattern"
|
||
fi
|
||
else
|
||
fail "No redirect rules in anchor"
|
||
fi
|
||
|
||
# Check anchor file exists
|
||
if [[ -f "/etc/pf.anchors/$PF_ANCHOR" ]]; then
|
||
pass "Anchor file exists: /etc/pf.anchors/$PF_ANCHOR"
|
||
else
|
||
fail "Anchor file missing: /etc/pf.anchors/$PF_ANCHOR"
|
||
fi
|
||
|
||
# Check pf.conf was NOT modified
|
||
if grep -q "$PF_ANCHOR" /etc/pf.conf 2>/dev/null; then
|
||
warn "pf.conf contains '$PF_ANCHOR' reference — should NOT be modified on disk"
|
||
else
|
||
pass "pf.conf NOT modified on disk (anchor injected at runtime only)"
|
||
fi
|
||
}
|
||
|
||
test_dns_interception() {
|
||
header "2. DNS Interception Tests"
|
||
|
||
# Mark position in log
|
||
local log_lines_before=0
|
||
if [[ -f "$CTRLD_LOG" ]]; then
|
||
log_lines_before=$(wc -l < "$CTRLD_LOG")
|
||
fi
|
||
|
||
# Test 1: Query to external resolver should be intercepted
|
||
info "Test: dig @8.8.8.8 example.com (should be intercepted by ctrld)"
|
||
local dig_result
|
||
dig_result=$(dig @8.8.8.8 example.com +short +timeout=5 2>&1 || true)
|
||
|
||
if [[ -n "$dig_result" ]] && ! echo "$dig_result" | grep -q "timed out"; then
|
||
pass "dig @8.8.8.8 returned result: $dig_result"
|
||
else
|
||
fail "dig @8.8.8.8 failed or timed out"
|
||
fi
|
||
|
||
# Check if ctrld logged the query
|
||
sleep 1
|
||
if [[ -f "$CTRLD_LOG" ]]; then
|
||
local intercepted
|
||
intercepted=$(tail -n +$((log_lines_before+1)) "$CTRLD_LOG" | grep -c "example.com" || echo "0")
|
||
if [[ "$intercepted" -gt 0 ]]; then
|
||
pass "ctrld logged the intercepted query for example.com"
|
||
else
|
||
fail "ctrld did NOT log query for example.com — interception may not be working"
|
||
fi
|
||
fi
|
||
|
||
# Check dig reports ctrld answered (not 8.8.8.8)
|
||
local full_dig
|
||
full_dig=$(dig @8.8.8.8 example.com +timeout=5 2>&1 || true)
|
||
local server_line
|
||
server_line=$(echo "$full_dig" | grep "SERVER:" || true)
|
||
info "dig SERVER line: $server_line"
|
||
if echo "$server_line" | grep -q "127.0.0.1"; then
|
||
pass "Response came from 127.0.0.1 (ctrld intercepted)"
|
||
elif echo "$server_line" | grep -q "8.8.8.8"; then
|
||
fail "Response came from 8.8.8.8 directly — NOT intercepted"
|
||
else
|
||
warn "Could not determine response server from dig output"
|
||
fi
|
||
|
||
separator
|
||
|
||
# Test 2: Query to another external resolver
|
||
info "Test: dig @1.1.1.1 cloudflare.com (should also be intercepted)"
|
||
local dig2
|
||
dig2=$(dig @1.1.1.1 cloudflare.com +short +timeout=5 2>&1 || true)
|
||
if [[ -n "$dig2" ]] && ! echo "$dig2" | grep -q "timed out"; then
|
||
pass "dig @1.1.1.1 returned result"
|
||
else
|
||
fail "dig @1.1.1.1 failed or timed out"
|
||
fi
|
||
|
||
separator
|
||
|
||
# Test 3: Query to localhost should work (not double-redirected)
|
||
info "Test: dig @127.0.0.1 example.org (direct to ctrld, should NOT be redirected)"
|
||
local dig3
|
||
dig3=$(dig @127.0.0.1 example.org +short +timeout=5 2>&1 || true)
|
||
if [[ -n "$dig3" ]] && ! echo "$dig3" | grep -q "timed out"; then
|
||
pass "dig @127.0.0.1 works (no loop)"
|
||
else
|
||
fail "dig @127.0.0.1 failed — possible redirect loop"
|
||
fi
|
||
|
||
separator
|
||
|
||
# Test 4: System DNS resolution
|
||
info "Test: host example.net (system resolver, should go through ctrld)"
|
||
local host_result
|
||
host_result=$(host example.net 2>&1 || true)
|
||
if echo "$host_result" | grep -q "has address"; then
|
||
pass "System DNS resolution works via host command"
|
||
else
|
||
fail "System DNS resolution failed"
|
||
fi
|
||
|
||
separator
|
||
|
||
# Test 5: TCP DNS query
|
||
info "Test: dig @9.9.9.9 example.com +tcp (TCP DNS should also be intercepted)"
|
||
local dig_tcp
|
||
dig_tcp=$(dig @9.9.9.9 example.com +tcp +short +timeout=5 2>&1 || true)
|
||
if [[ -n "$dig_tcp" ]] && ! echo "$dig_tcp" | grep -q "timed out"; then
|
||
pass "TCP DNS query intercepted and resolved"
|
||
else
|
||
warn "TCP DNS query failed (may not be critical if UDP works)"
|
||
fi
|
||
}
|
||
|
||
test_non_dns_unaffected() {
|
||
header "3. Non-DNS Traffic Unaffected"
|
||
|
||
# HTTPS should work fine
|
||
info "Test: curl https://example.com (HTTPS port 443 should NOT be affected)"
|
||
local curl_result
|
||
curl_result=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://example.com 2>&1 || echo "000")
|
||
if [[ "$curl_result" == "200" ]] || [[ "$curl_result" == "301" ]] || [[ "$curl_result" == "302" ]]; then
|
||
pass "HTTPS works (HTTP $curl_result)"
|
||
else
|
||
fail "HTTPS failed (HTTP $curl_result) — pf may be affecting non-DNS traffic"
|
||
fi
|
||
|
||
# SSH-style connection test (port 22 should be unaffected)
|
||
info "Test: nc -z -w5 github.com 22 (SSH port should NOT be affected)"
|
||
if nc -z -w5 github.com 22 2>/dev/null; then
|
||
pass "SSH port reachable (non-DNS traffic unaffected)"
|
||
else
|
||
warn "SSH port unreachable (may be firewall, not necessarily our fault)"
|
||
fi
|
||
}
|
||
|
||
test_ctrld_log_health() {
|
||
header "4. ctrld Log Health Check"
|
||
|
||
if [[ ! -f "$CTRLD_LOG" ]]; then
|
||
warn "Skipping log checks — $CTRLD_LOG not found"
|
||
return
|
||
fi
|
||
|
||
# Check for intercept initialization
|
||
if log_grep "DNS intercept.*initializing" 500 | grep -q "."; then
|
||
pass "DNS intercept initialization logged"
|
||
else
|
||
fail "No DNS intercept initialization in recent logs"
|
||
fi
|
||
|
||
# Check for successful anchor load
|
||
if log_grep "pf anchor.*active" 500 | grep -q "."; then
|
||
pass "PF anchor reported as active"
|
||
else
|
||
fail "PF anchor not reported as active"
|
||
fi
|
||
|
||
# Check for anchor reference injection
|
||
if log_grep "anchor reference active" 500 | grep -q "."; then
|
||
pass "Anchor reference injected into running ruleset"
|
||
else
|
||
fail "Anchor reference NOT injected — this is the critical step"
|
||
fi
|
||
|
||
# Check for errors
|
||
separator
|
||
info "Recent errors/warnings in ctrld log:"
|
||
local errors
|
||
errors=$(log_grep '"level":"error"' 500)
|
||
if [[ -n "$errors" ]]; then
|
||
echo "$errors" | tail -5 | sed 's/^/ /'
|
||
warn "Errors found in recent logs (see above)"
|
||
else
|
||
pass "No errors in recent logs"
|
||
fi
|
||
|
||
local warnings
|
||
warnings=$(log_grep '"level":"warn"' 500 | grep -v "skipping self-upgrade" || true)
|
||
if [[ -n "$warnings" ]]; then
|
||
echo "$warnings" | tail -5 | sed 's/^/ /'
|
||
info "(warnings above may be expected)"
|
||
fi
|
||
|
||
# Check for recovery bypass state
|
||
if log_grep "recoveryBypass\|recovery bypass\|prepareForRecovery" 500 | grep -q "."; then
|
||
info "Recovery bypass activity detected in logs"
|
||
log_grep "recovery" 500 | tail -3 | sed 's/^/ /'
|
||
fi
|
||
|
||
# Check for VPN DNS detection
|
||
if log_grep "VPN DNS" 500 | grep -q "."; then
|
||
info "VPN DNS activity in logs:"
|
||
log_grep "VPN DNS" 500 | tail -5 | sed 's/^/ /'
|
||
else
|
||
info "No VPN DNS activity (expected if no VPN is connected)"
|
||
fi
|
||
}
|
||
|
||
test_pf_counters() {
|
||
header "5. PF Statistics & Counters"
|
||
|
||
info "PF info (pfctl -si):"
|
||
pfctl -si 2>&1 | grep -v "ALTQ" | head -15 | sed 's/^/ /'
|
||
|
||
info "PF state table entries:"
|
||
pfctl -ss 2>&1 | grep -c "." | sed 's/^/ States: /'
|
||
|
||
# Count evaluations of our anchor
|
||
info "Anchor-specific stats (if available):"
|
||
local anchor_info
|
||
anchor_info=$(pfctl -a "$PF_ANCHOR" -si 2>&1 | grep -v "ALTQ" || true)
|
||
if [[ -n "$anchor_info" ]]; then
|
||
echo "$anchor_info" | head -10 | sed 's/^/ /'
|
||
else
|
||
info " (no per-anchor stats available)"
|
||
fi
|
||
}
|
||
|
||
test_cleanup_on_stop() {
|
||
header "6. Cleanup Validation (After ctrld Stop)"
|
||
|
||
manual "Stop ctrld now (Ctrl+C or 'ctrld stop'), then press Enter"
|
||
wait_for_key
|
||
|
||
# Check anchor is flushed
|
||
local anchor_rules_after
|
||
anchor_rules_after=$(pfctl -a "$PF_ANCHOR" -sr 2>&1 | grep -v "ALTQ" | grep -v "^$" || true)
|
||
if [[ -z "$anchor_rules_after" ]]; then
|
||
pass "Anchor filter rules flushed after stop"
|
||
else
|
||
fail "Anchor filter rules still present after stop"
|
||
echo "$anchor_rules_after" | sed 's/^/ /'
|
||
fi
|
||
|
||
local anchor_rdr_after
|
||
anchor_rdr_after=$(pfctl -a "$PF_ANCHOR" -sn 2>&1 | grep -v "ALTQ" | grep -v "^$" || true)
|
||
if [[ -z "$anchor_rdr_after" ]]; then
|
||
pass "Anchor redirect rules flushed after stop"
|
||
else
|
||
fail "Anchor redirect rules still present after stop"
|
||
fi
|
||
|
||
# Check anchor file removed
|
||
if [[ ! -f "/etc/pf.anchors/$PF_ANCHOR" ]]; then
|
||
pass "Anchor file removed after stop"
|
||
else
|
||
fail "Anchor file still exists: /etc/pf.anchors/$PF_ANCHOR"
|
||
fi
|
||
|
||
# Check pf.conf is clean
|
||
if ! grep -q "$PF_ANCHOR" /etc/pf.conf 2>/dev/null; then
|
||
pass "pf.conf is clean (no ctrld references)"
|
||
else
|
||
fail "pf.conf still has ctrld references after stop"
|
||
fi
|
||
|
||
# DNS should work normally without ctrld
|
||
info "Test: dig example.com (should resolve via system DNS)"
|
||
local dig_after
|
||
dig_after=$(dig example.com +short +timeout=5 2>&1 || true)
|
||
if [[ -n "$dig_after" ]] && ! echo "$dig_after" | grep -q "timed out"; then
|
||
pass "DNS works after ctrld stop"
|
||
else
|
||
fail "DNS broken after ctrld stop — cleanup may have failed"
|
||
fi
|
||
}
|
||
|
||
test_restart_resilience() {
|
||
header "7. Restart Resilience"
|
||
|
||
manual "Start ctrld again with --dns-intercept, then press Enter"
|
||
wait_for_key
|
||
|
||
sleep 3
|
||
|
||
# Re-run pf state checks
|
||
local sr_match sn_match
|
||
sr_match=$(pfctl -sr 2>&1 | grep "$PF_ANCHOR" || true)
|
||
sn_match=$(pfctl -sn 2>&1 | grep "$PF_ANCHOR" || true)
|
||
|
||
if [[ -n "$sr_match" ]] && [[ -n "$sn_match" ]]; then
|
||
pass "Anchor references restored after restart"
|
||
else
|
||
fail "Anchor references NOT restored after restart"
|
||
fi
|
||
|
||
# Quick interception test
|
||
local dig_after_restart
|
||
dig_after_restart=$(dig @8.8.8.8 example.com +short +timeout=5 2>&1 || true)
|
||
if [[ -n "$dig_after_restart" ]] && ! echo "$dig_after_restart" | grep -q "timed out"; then
|
||
pass "DNS interception works after restart"
|
||
else
|
||
fail "DNS interception broken after restart"
|
||
fi
|
||
}
|
||
|
||
test_network_change() {
|
||
header "8. Network Change Recovery"
|
||
|
||
info "This test verifies recovery after network changes."
|
||
manual "Switch Wi-Fi networks (or disconnect/reconnect Ethernet), then press Enter"
|
||
wait_for_key
|
||
|
||
sleep 5
|
||
|
||
# Check pf rules still active
|
||
local sr_after sn_after
|
||
sr_after=$(pfctl -sr 2>&1 | grep "$PF_ANCHOR" || true)
|
||
sn_after=$(pfctl -sn 2>&1 | grep "$PF_ANCHOR" || true)
|
||
|
||
if [[ -n "$sr_after" ]] && [[ -n "$sn_after" ]]; then
|
||
pass "Anchor references survived network change"
|
||
else
|
||
fail "Anchor references lost after network change"
|
||
fi
|
||
|
||
# Check interception still works
|
||
local dig_after_net
|
||
dig_after_net=$(dig @8.8.8.8 example.com +short +timeout=10 2>&1 || true)
|
||
if [[ -n "$dig_after_net" ]] && ! echo "$dig_after_net" | grep -q "timed out"; then
|
||
pass "DNS interception works after network change"
|
||
else
|
||
fail "DNS interception broken after network change"
|
||
fi
|
||
|
||
# Check logs for recovery bypass activity
|
||
if [[ -f "$CTRLD_LOG" ]]; then
|
||
local recovery_logs
|
||
recovery_logs=$(log_grep "recovery\|network change\|network monitor" 100)
|
||
if [[ -n "$recovery_logs" ]]; then
|
||
info "Recovery/network change log entries:"
|
||
echo "$recovery_logs" | tail -5 | sed 's/^/ /'
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# SUMMARY
|
||
# =============================================================================
|
||
|
||
print_summary() {
|
||
header "TEST SUMMARY"
|
||
echo ""
|
||
for r in "${RESULTS[@]}"; do
|
||
if [[ "$r" == PASS* ]]; then
|
||
echo -e " ${GREEN}✅${NC} ${r#PASS: }"
|
||
elif [[ "$r" == FAIL* ]]; then
|
||
echo -e " ${RED}❌${NC} ${r#FAIL: }"
|
||
elif [[ "$r" == WARN* ]]; then
|
||
echo -e " ${YELLOW}⚠️${NC} ${r#WARN: }"
|
||
fi
|
||
done
|
||
echo ""
|
||
separator
|
||
echo -e " ${GREEN}Passed: $PASS${NC} | ${RED}Failed: $FAIL${NC} | ${YELLOW}Warnings: $WARN${NC}"
|
||
separator
|
||
|
||
if [[ $FAIL -gt 0 ]]; then
|
||
echo -e "\n ${RED}${BOLD}Some tests failed.${NC} Check output above for details."
|
||
echo -e " Useful debug commands:"
|
||
echo -e " pfctl -a '$PF_ANCHOR' -sr # anchor filter rules"
|
||
echo -e " pfctl -a '$PF_ANCHOR' -sn # anchor redirect rules"
|
||
echo -e " pfctl -sr | grep controld # main ruleset references"
|
||
echo -e " tail -100 $CTRLD_LOG # recent ctrld logs"
|
||
else
|
||
echo -e "\n ${GREEN}${BOLD}All tests passed!${NC}"
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# MAIN
|
||
# =============================================================================
|
||
|
||
echo -e "${BOLD}╔═══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${BOLD}║ ctrld DNS Intercept Mode — macOS Test Suite ║${NC}"
|
||
echo -e "${BOLD}║ Tests pf-based DNS interception (route-to + rdr) ║${NC}"
|
||
echo -e "${BOLD}╚═══════════════════════════════════════════════════════╝${NC}"
|
||
|
||
check_root
|
||
|
||
echo ""
|
||
echo "Make sure ctrld is running with --dns-intercept before starting."
|
||
echo "Log location: $CTRLD_LOG"
|
||
wait_for_key
|
||
|
||
test_prereqs
|
||
test_pf_state
|
||
test_dns_interception
|
||
test_non_dns_unaffected
|
||
test_ctrld_log_health
|
||
test_pf_counters
|
||
|
||
separator
|
||
echo ""
|
||
echo "The next tests require manual steps (stop/start ctrld, network changes)."
|
||
echo "Press Enter to continue, or Ctrl+C to skip and see results so far."
|
||
wait_for_key
|
||
|
||
test_cleanup_on_stop
|
||
test_restart_resilience
|
||
test_network_change
|
||
|
||
print_summary
|