mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:25:19 +00:00
Runtime-verified whether Apple's URLSession honors connectionProxyDictionary SOCKS settings for Nostr relay traffic (plain HTTPS + URLSessionWebSocketTask), since the app routes all Nostr traffic through Arti's local SOCKS5 proxy solely via those keys and Apple does not officially support SOCKS for URLSession on iOS. Verification harness (scripts/tor-egress-verification/): a minimal SOCKS5 CONNECT proxy that logs arriving connections, plus a Swift probe that runs an HTTPS GET and a WebSocket ping through a URLSession configured with the proxy dict. Result: on macOS (kCFNetworkProxiesSOCKS* constants) and the iOS simulator (raw "SOCKSProxy"/"SOCKSPort" string keys, the exact app path) BOTH HTTP and WebSocket are proxied, do remote DNS through the proxy, and the proxied session is fail-closed (every request errors when the SOCKS proxy is down). The feared direct-egress leak did not reproduce on the tested platforms. Defense-in-depth for the platform Apple does not guarantee (physical iOS): add TorEgressVerifier, which performs a canary request through the proxied session and asserts the exit is a Tor node (check.torproject.org IsTor==true), positively detecting a direct egress even if the OS silently ignored the proxy. Policy: verifiedTor allows (cached for a TTL); notTor refuses (leak detected, never open relays); unreachable allows-with-warning (session stays fail-closed by construction). Wired into TorManager.awaitEgressReady() and required by both NostrRelayManager and GeoRelayDirectory before opening connections. Cache is invalidated on Tor restart/dormant/shutdown. Never falls back to a direct connection. Probe is injected so the policy/caching is unit-tested offline; the live-network harness lives under scripts/ and is not run by CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.8 KiB
Bash
Executable File
74 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Orchestrates the Tor-egress proxy-honoring verification on macOS.
|
|
#
|
|
# For each (request-type x key-style) it runs two experiments:
|
|
# A) proxy UP — did a connection arrive at the SOCKS proxy? (log grows)
|
|
# B) proxy DOWN — pointed at a dead port; does the request still SUCCEED?
|
|
# If it succeeds with no proxy, egress went DIRECT (proxy ignored).
|
|
# If it fails, the proxy setting is being enforced (fail-closed).
|
|
#
|
|
# Discriminator: PROXIED = connection observed at proxy AND fails when proxy down
|
|
# DIRECT = no connection at proxy OR succeeds when proxy down
|
|
set -u
|
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PORT=19999
|
|
DEADPORT=19998 # nothing listens here
|
|
LOG="$(mktemp -t sockslog)"
|
|
BIN="$(mktemp -t proxyprobe)"
|
|
|
|
echo "== building swift probe =="
|
|
swiftc -O "$DIR/proxy_probe.swift" -o "$BIN" || { echo "swiftc failed"; exit 1; }
|
|
|
|
echo "== starting SOCKS proxy on $PORT =="
|
|
: > "$LOG"
|
|
python3 "$DIR/socks5_probe_proxy.py" "$PORT" "$LOG" >/tmp/socksproxy.out 2>&1 &
|
|
PROXY_PID=$!
|
|
trap 'kill $PROXY_PID 2>/dev/null' EXIT
|
|
# wait for READY
|
|
for _ in $(seq 1 50); do
|
|
grep -q READY /tmp/socksproxy.out 2>/dev/null && break
|
|
sleep 0.1
|
|
done
|
|
|
|
run_case() {
|
|
local mode="$1" key="$2"
|
|
# Experiment A: proxy up, watch log
|
|
local before after target
|
|
before=$(wc -l < "$LOG" | tr -d ' ')
|
|
local outA
|
|
outA=$("$BIN" "$mode" "$key" "$PORT" 2>/dev/null | grep '^RESULT' || echo "RESULT $mode $key ERROR no-output")
|
|
sleep 0.3
|
|
after=$(wc -l < "$LOG" | tr -d ' ')
|
|
local proxied="NO"
|
|
if [ "$after" -gt "$before" ]; then proxied="YES"; fi
|
|
local newlines
|
|
newlines=$(tail -n +"$((before+1))" "$LOG" | tr '\t' ' ' | tr '\n' '|')
|
|
|
|
# Experiment B: proxy down (dead port), same request
|
|
local outB
|
|
outB=$("$BIN" "$mode" "$key" "$DEADPORT" 2>/dev/null | grep '^RESULT' || echo "RESULT $mode $key ERROR no-output")
|
|
|
|
echo "----------------------------------------"
|
|
echo "CASE mode=$mode key=$key"
|
|
echo " A(proxy up): $outA | connection_at_proxy=$proxied [$newlines]"
|
|
echo " B(proxy down): $outB"
|
|
# verdict
|
|
local a_ok b_ok
|
|
a_ok=$(echo "$outA" | awk '{print $4}')
|
|
b_ok=$(echo "$outB" | awk '{print $4}')
|
|
local verdict="UNKNOWN"
|
|
if [ "$proxied" = "YES" ] && [ "$b_ok" = "ERROR" ]; then verdict="PROXIED (enforced)"; fi
|
|
if [ "$proxied" = "NO" ] && [ "$b_ok" = "OK" ]; then verdict="DIRECT (proxy ignored)"; fi
|
|
if [ "$proxied" = "YES" ] && [ "$b_ok" = "OK" ]; then verdict="AMBIGUOUS (uses proxy if up, but egresses direct if down)"; fi
|
|
if [ "$proxied" = "NO" ] && [ "$b_ok" = "ERROR" ]; then verdict="BLOCKED both (network/target issue?)"; fi
|
|
echo " VERDICT: $verdict"
|
|
}
|
|
|
|
for mode in http ws; do
|
|
for key in cf raw; do
|
|
run_case "$mode" "$key"
|
|
done
|
|
done
|
|
echo "========================================"
|
|
echo "raw proxy log:"; cat "$LOG" | tr '\t' ' '
|