mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-27 12:52:27 +02:00
feat: add macOS pf DNS interception
This commit is contained in:
committed by
Cuong Manh Le
parent
395335162f
commit
a99dcca288
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# diag-lo0-capture.sh — Capture DNS on lo0 to see where the pf chain breaks
|
||||
# Usage: sudo bash diag-lo0-capture.sh
|
||||
# Run while Windscribe + ctrld are both active, then dig from another terminal
|
||||
|
||||
set -u
|
||||
PCAP="/tmp/lo0-dns-$(date +%s).pcap"
|
||||
echo "=== lo0 DNS Packet Capture ==="
|
||||
echo "Capturing to: $PCAP"
|
||||
echo ""
|
||||
|
||||
# Show current rules (verify build)
|
||||
echo "--- ctrld anchor rdr rules ---"
|
||||
pfctl -a com.controld.ctrld -sn 2>/dev/null
|
||||
echo ""
|
||||
echo "--- ctrld anchor filter rules (lo0 only) ---"
|
||||
pfctl -a com.controld.ctrld -sr 2>/dev/null | grep lo0
|
||||
echo ""
|
||||
|
||||
# Check pf state table for port 53 before
|
||||
echo "--- port 53 states BEFORE dig ---"
|
||||
pfctl -ss 2>/dev/null | grep ':53' | head -10
|
||||
echo "(total: $(pfctl -ss 2>/dev/null | grep -c ':53'))"
|
||||
echo ""
|
||||
|
||||
# Start capture on lo0
|
||||
echo "Starting tcpdump on lo0 port 53..."
|
||||
echo ">>> In another terminal, run: dig example.com"
|
||||
echo ">>> Then press Ctrl-C here"
|
||||
echo ""
|
||||
tcpdump -i lo0 -n -v port 53 -w "$PCAP" 2>&1 &
|
||||
TCPDUMP_PID=$!
|
||||
|
||||
# Also show live output
|
||||
tcpdump -i lo0 -n port 53 2>&1 &
|
||||
LIVE_PID=$!
|
||||
|
||||
# Wait for Ctrl-C
|
||||
trap "kill $TCPDUMP_PID $LIVE_PID 2>/dev/null; echo ''; echo '--- port 53 states AFTER dig ---'; pfctl -ss 2>/dev/null | grep ':53' | head -20; echo '(total: '$(pfctl -ss 2>/dev/null | grep -c ':53')')'; echo ''; echo 'Capture saved to: $PCAP'; echo 'Read with: tcpdump -r $PCAP -n -v'; exit 0" INT
|
||||
wait
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# diag-pf-poll.sh — Polls pf rules, options, states, and DNS every 2s
|
||||
# Usage: sudo bash diag-pf-poll.sh | tee /tmp/pf-poll.log
|
||||
# Steps: 1) Run script 2) Connect Windscribe 3) Start ctrld 4) Ctrl-C when done
|
||||
|
||||
set -u
|
||||
LOG="/tmp/pf-poll-$(date +%s).log"
|
||||
echo "=== PF Poll Diagnostic — logging to $LOG ==="
|
||||
echo "Press Ctrl-C to stop"
|
||||
echo ""
|
||||
|
||||
poll() {
|
||||
local ts=$(date '+%H:%M:%S.%3N')
|
||||
echo "======== [$ts] POLL ========"
|
||||
|
||||
# 1. pf options — looking for "set skip on lo0"
|
||||
echo "--- pf options ---"
|
||||
pfctl -so 2>/dev/null | grep -i skip || echo "(no skip rules)"
|
||||
|
||||
# 2. Main ruleset anchors — where is ctrld relative to block drop all?
|
||||
echo "--- main filter rules (summary) ---"
|
||||
pfctl -sr 2>/dev/null | head -30
|
||||
|
||||
# 3. Main NAT/rdr rules
|
||||
echo "--- main nat/rdr rules (summary) ---"
|
||||
pfctl -sn 2>/dev/null | head -20
|
||||
|
||||
# 4. ctrld anchor content
|
||||
echo "--- ctrld anchor (filter) ---"
|
||||
pfctl -a com.apple.internet-sharing/ctrld -sr 2>/dev/null || echo "(no anchor)"
|
||||
echo "--- ctrld anchor (nat/rdr) ---"
|
||||
pfctl -a com.apple.internet-sharing/ctrld -sn 2>/dev/null || echo "(no anchor)"
|
||||
|
||||
# 5. State count for rdr target (10.255.255.3) and loopback
|
||||
echo "--- states summary ---"
|
||||
local total=$(pfctl -ss 2>/dev/null | wc -l | tr -d ' ')
|
||||
local rdr=$(pfctl -ss 2>/dev/null | grep -c '10\.255\.255\.3' || true)
|
||||
local lo0=$(pfctl -ss 2>/dev/null | grep -c 'lo0' || true)
|
||||
echo "total=$total rdr_target=$rdr lo0=$lo0"
|
||||
|
||||
# 6. Quick DNS test (1s timeout)
|
||||
echo "--- DNS tests ---"
|
||||
local direct=$(dig +short +time=1 +tries=1 example.com @127.0.0.1 2>&1 | head -1)
|
||||
local system=$(dig +short +time=1 +tries=1 example.com 2>&1 | head -1)
|
||||
echo "direct @127.0.0.1: $direct"
|
||||
echo "system DNS: $system"
|
||||
|
||||
# 7. Windscribe tunnel interface
|
||||
echo "--- tunnel interfaces ---"
|
||||
ifconfig -l | tr ' ' '\n' | grep -E '^utun' | while read iface; do
|
||||
echo -n "$iface: "
|
||||
ifconfig "$iface" 2>/dev/null | grep 'inet ' | awk '{print $2}' || echo "no ip"
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main loop
|
||||
while true; do
|
||||
poll 2>&1 | tee -a "$LOG"
|
||||
sleep 2
|
||||
done
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
# diag-windscribe-connect.sh — Diagnostic script for testing ctrld dns-intercept
|
||||
# during Windscribe VPN connection on macOS.
|
||||
#
|
||||
# Usage: sudo ./diag-windscribe-connect.sh
|
||||
#
|
||||
# Run this BEFORE connecting Windscribe. It polls every 0.5s and captures:
|
||||
# 1. pf anchor state (are ctrld anchors present?)
|
||||
# 2. pf state table entries (rdr interception working?)
|
||||
# 3. ctrld log events (watchdog, rebootstrap, errors)
|
||||
# 4. scutil DNS resolver state
|
||||
# 5. Active tunnel interfaces
|
||||
# 6. dig test query results
|
||||
#
|
||||
# Output goes to /tmp/diag-windscribe-<timestamp>/
|
||||
# Press Ctrl-C to stop. A summary is printed at the end.
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "ERROR: Must run as root (sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CTRLD_LOG="${CTRLD_LOG:-/tmp/dns.log}"
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
OUTDIR="/tmp/diag-windscribe-${TIMESTAMP}"
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
echo "=== Windscribe + ctrld DNS Intercept Diagnostic ==="
|
||||
echo "Output: $OUTDIR"
|
||||
echo "ctrld log: $CTRLD_LOG"
|
||||
echo ""
|
||||
echo "1. Start this script"
|
||||
echo "2. Connect Windscribe"
|
||||
echo "3. Wait ~30 seconds"
|
||||
echo "4. Try: dig popads.net / dig @127.0.0.1 popads.net"
|
||||
echo "5. Ctrl-C to stop and see summary"
|
||||
echo ""
|
||||
echo "Polling every 0.5s... Press Ctrl-C to stop."
|
||||
echo ""
|
||||
|
||||
# Track ctrld log position
|
||||
if [ -f "$CTRLD_LOG" ]; then
|
||||
LOG_START_LINE=$(wc -l < "$CTRLD_LOG")
|
||||
else
|
||||
LOG_START_LINE=0
|
||||
fi
|
||||
|
||||
ITER=0
|
||||
DIG_FAIL=0
|
||||
DIG_OK=0
|
||||
ANCHOR_MISSING=0
|
||||
ANCHOR_PRESENT=0
|
||||
PF_WIPE_COUNT=0
|
||||
FORCE_REBOOT_COUNT=0
|
||||
LAST_TUNNEL_IFACES=""
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "=== Stopping diagnostic ==="
|
||||
|
||||
# Capture final state
|
||||
echo "--- Final pf state ---" > "$OUTDIR/final-pfctl.txt"
|
||||
pfctl -sa 2>/dev/null >> "$OUTDIR/final-pfctl.txt" 2>&1 || true
|
||||
|
||||
echo "--- Final scutil ---" > "$OUTDIR/final-scutil.txt"
|
||||
scutil --dns >> "$OUTDIR/final-scutil.txt" 2>&1 || true
|
||||
|
||||
# Extract ctrld log events since start
|
||||
if [ -f "$CTRLD_LOG" ]; then
|
||||
tail -n +$((LOG_START_LINE + 1)) "$CTRLD_LOG" > "$OUTDIR/ctrld-events.log" 2>/dev/null || true
|
||||
|
||||
# Extract key events
|
||||
echo "--- Watchdog events ---" > "$OUTDIR/summary-watchdog.txt"
|
||||
grep -i "watchdog\|anchor.*missing\|anchor.*restored\|force-reset\|re-bootstrapping\|force re-bootstrapping" "$OUTDIR/ctrld-events.log" >> "$OUTDIR/summary-watchdog.txt" 2>/dev/null || true
|
||||
|
||||
echo "--- Errors ---" > "$OUTDIR/summary-errors.txt"
|
||||
grep '"level":"error"' "$OUTDIR/ctrld-events.log" >> "$OUTDIR/summary-errors.txt" 2>/dev/null || true
|
||||
|
||||
echo "--- Network changes ---" > "$OUTDIR/summary-network.txt"
|
||||
grep -i "Network change\|tunnel interface\|Ignoring interface" "$OUTDIR/ctrld-events.log" >> "$OUTDIR/summary-network.txt" 2>/dev/null || true
|
||||
|
||||
echo "--- Transport resets ---" > "$OUTDIR/summary-transport.txt"
|
||||
grep -i "re-bootstrap\|force.*bootstrap\|dialing to\|connected to" "$OUTDIR/ctrld-events.log" >> "$OUTDIR/summary-transport.txt" 2>/dev/null || true
|
||||
|
||||
# Count key events
|
||||
PF_WIPE_COUNT=$(grep -c "anchor.*missing\|restoring pf" "$OUTDIR/ctrld-events.log" 2>/dev/null || echo 0)
|
||||
FORCE_REBOOT_COUNT=$(grep -c "force re-bootstrapping\|force-reset" "$OUTDIR/ctrld-events.log" 2>/dev/null || echo 0)
|
||||
DEADLINE_COUNT=$(grep -c "context deadline exceeded" "$OUTDIR/ctrld-events.log" 2>/dev/null || echo 0)
|
||||
FALLBACK_COUNT=$(grep -c "OS resolver retry query successful" "$OUTDIR/ctrld-events.log" 2>/dev/null || echo 0)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " DIAGNOSTIC SUMMARY"
|
||||
echo "========================================="
|
||||
echo "Duration: $ITER iterations (~$((ITER / 2))s)"
|
||||
echo ""
|
||||
echo "pf Anchor Status:"
|
||||
echo " Present: $ANCHOR_PRESENT times"
|
||||
echo " Missing: $ANCHOR_MISSING times"
|
||||
echo ""
|
||||
echo "dig Tests (popads.net):"
|
||||
echo " Success: $DIG_OK"
|
||||
echo " Failed: $DIG_FAIL"
|
||||
echo ""
|
||||
echo "ctrld Log Events:"
|
||||
echo " pf wipes detected: $PF_WIPE_COUNT"
|
||||
echo " Force rebootstraps: $FORCE_REBOOT_COUNT"
|
||||
echo " Context deadline errors: ${DEADLINE_COUNT:-0}"
|
||||
echo " OS resolver fallbacks: ${FALLBACK_COUNT:-0}"
|
||||
echo ""
|
||||
echo "Last tunnel interfaces: ${LAST_TUNNEL_IFACES:-none}"
|
||||
echo ""
|
||||
echo "Files saved to: $OUTDIR/"
|
||||
echo " final-pfctl.txt — full pfctl -sa at exit"
|
||||
echo " final-scutil.txt — scutil --dns at exit"
|
||||
echo " ctrld-events.log — ctrld log during test"
|
||||
echo " summary-watchdog.txt — watchdog events"
|
||||
echo " summary-errors.txt — errors"
|
||||
echo " summary-transport.txt — transport reset events"
|
||||
echo " timeline.log — per-iteration state"
|
||||
echo "========================================="
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup INT TERM
|
||||
|
||||
while true; do
|
||||
ITER=$((ITER + 1))
|
||||
NOW=$(date '+%H:%M:%S.%3N' 2>/dev/null || date '+%H:%M:%S')
|
||||
|
||||
# 1. Check pf anchor presence
|
||||
ANCHOR_STATUS="MISSING"
|
||||
if pfctl -sr 2>/dev/null | grep -q "com.controld.ctrld"; then
|
||||
ANCHOR_STATUS="PRESENT"
|
||||
ANCHOR_PRESENT=$((ANCHOR_PRESENT + 1))
|
||||
else
|
||||
ANCHOR_MISSING=$((ANCHOR_MISSING + 1))
|
||||
fi
|
||||
|
||||
# 2. Check tunnel interfaces
|
||||
TUNNEL_IFACES=$(ifconfig -l 2>/dev/null | tr ' ' '\n' | grep -E '^(utun|ipsec|ppp|tap|tun)' | \
|
||||
while read iface; do
|
||||
# Only list interfaces that are UP and have an IP
|
||||
if ifconfig "$iface" 2>/dev/null | grep -q "inet "; then
|
||||
echo -n "$iface "
|
||||
fi
|
||||
done)
|
||||
TUNNEL_IFACES=$(echo "$TUNNEL_IFACES" | xargs) # trim
|
||||
if [ -n "$TUNNEL_IFACES" ]; then
|
||||
LAST_TUNNEL_IFACES="$TUNNEL_IFACES"
|
||||
fi
|
||||
|
||||
# 3. Count rdr states (three-part = intercepted)
|
||||
RDR_COUNT=$(pfctl -ss 2>/dev/null | grep -c "127.0.0.1:53 <-" || echo 0)
|
||||
|
||||
# 4. Quick dig test (0.5s timeout)
|
||||
DIG_RESULT="SKIP"
|
||||
if [ $((ITER % 4)) -eq 0 ]; then # every 2 seconds
|
||||
if dig +time=1 +tries=1 popads.net A @127.0.0.1 +short >/dev/null 2>&1; then
|
||||
DIG_RESULT="OK"
|
||||
DIG_OK=$((DIG_OK + 1))
|
||||
else
|
||||
DIG_RESULT="FAIL"
|
||||
DIG_FAIL=$((DIG_FAIL + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# 5. Check latest ctrld log for recent errors
|
||||
RECENT_ERR=""
|
||||
if [ -f "$CTRLD_LOG" ]; then
|
||||
RECENT_ERR=$(tail -5 "$CTRLD_LOG" 2>/dev/null | grep -o '"message":"[^"]*deadline[^"]*"' | tail -1 || true)
|
||||
fi
|
||||
|
||||
# Output timeline
|
||||
LINE="[$NOW] anchor=$ANCHOR_STATUS rdr_states=$RDR_COUNT tunnels=[$TUNNEL_IFACES] dig=$DIG_RESULT $RECENT_ERR"
|
||||
echo "$LINE"
|
||||
echo "$LINE" >> "$OUTDIR/timeline.log"
|
||||
|
||||
sleep 0.5
|
||||
done
|
||||
Reference in New Issue
Block a user