From 1602fa17975a4f47a7739a55af7cfcaa4951c5cd Mon Sep 17 00:00:00 2001 From: Franz Haberfellner Date: Sat, 7 Nov 2026 16:39:59 +0100 Subject: [PATCH 1/2] fix(test): retry transparent gateway egress check in verify_gateway A single curl attempt against check.torproject.org over a fresh circuit flaked in the daily canary (curl exit 55) although the gateway was healthy; the manual re-run passed. Retry up to 4 times like the other canary egress checks, then fail. --- tests/integration/verify_gateway.sh | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/integration/verify_gateway.sh b/tests/integration/verify_gateway.sh index 014c4c7..711c6a7 100755 --- a/tests/integration/verify_gateway.sh +++ b/tests/integration/verify_gateway.sh @@ -56,10 +56,24 @@ fi echo "✅ No ESTABLISHED fail-closed bypass rule present." echo "=== 2. Transparent Tor Egress Verification ===" -CLIENT_OUTPUT=$(docker run --rm --network "container:${GATEWAY_CONTAINER}" curlimages/curl:latest -sS --connect-timeout 15 --max-time 30 https://check.torproject.org/api/ip) +# Retry like the other canary egress checks: a single attempt against +# check.torproject.org over a freshly built circuit is flaky and +# produced a false red in the daily canary (curl exit 55) although +# the gateway itself was healthy. +CLIENT_OUTPUT="" +IS_TOR="" +EXIT_IP="" +for attempt in 1 2 3 4; do + echo "Transparent egress check attempt $attempt/4..." + CLIENT_OUTPUT=$(docker run --rm --network "container:${GATEWAY_CONTAINER}" curlimages/curl:latest -sS --connect-timeout 15 --max-time 30 --retry 2 --retry-all-errors https://check.torproject.org/api/ip 2>/dev/null || true) + IS_TOR=$(echo "$CLIENT_OUTPUT" | jq -r '.IsTor // empty' 2>/dev/null || true) + EXIT_IP=$(echo "$CLIENT_OUTPUT" | jq -r '.IP // empty' 2>/dev/null || true) + if [ "$IS_TOR" = "true" ] && [ -n "$EXIT_IP" ]; then + break + fi + sleep 3 +done echo "Client Response: $CLIENT_OUTPUT" -IS_TOR=$(echo "$CLIENT_OUTPUT" | jq -r '.IsTor // empty') -EXIT_IP=$(echo "$CLIENT_OUTPUT" | jq -r '.IP // empty') if [ "$IS_TOR" != "true" ]; then echo "❌ CRITICAL: Outgoing traffic was NOT identified as Tor egress!" From 2b0c335bb03e19ae242c2712f219b9cab5e12863 Mon Sep 17 00:00:00 2001 From: Franz Haberfellner Date: Sat, 7 Nov 2026 16:46:40 +0100 Subject: [PATCH 2/2] fix(ci): wait for Tor bootstrap progress in gateway steps Gateway bootstrap polls waited for /status connected, which additionally requires the external check.torproject.org egress probe. Its intermittent unreachability from CI runners flaked the step (PR #121 smoke run) although Tor reached 100% in seconds. Poll /health tor.progress == 100 instead; egress itself is verified separately by verify_gateway.sh. --- .github/actions/live-verification/action.yml | 8 ++++++-- .github/workflows/pr-gate.yml | 10 ++++++++-- tests/unit/test_ci_workflows.py | 10 ++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/actions/live-verification/action.yml b/.github/actions/live-verification/action.yml index 50d530e..bfb9004 100644 --- a/.github/actions/live-verification/action.yml +++ b/.github/actions/live-verification/action.yml @@ -299,8 +299,12 @@ runs: BOOTSTRAPPED=false START=$SECONDS while [ $((SECONDS - START)) -lt 180 ]; do - STATUS=$(docker exec torproxy-canary-gateway curl -fsS --connect-timeout 2 --max-time 5 http://127.0.0.1:8080/status 2>/dev/null | jq -r '.status // empty' || true) - if [ "$STATUS" = "connected" ]; then + # Bootstrap progress, not /status "connected": the latter + # requires the external check.torproject.org egress probe, + # whose intermittent unreachability flaked this step + # although Tor itself bootstrapped in seconds (PR #121). + PROGRESS=$(docker exec torproxy-canary-gateway curl -fsS --connect-timeout 2 --max-time 5 http://127.0.0.1:8080/health 2>/dev/null | jq -r '.tor.progress // empty' || true) + if [ "$PROGRESS" = "100" ]; then echo "✅ Gateway Tor successfully connected!" BOOTSTRAPPED=true break diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index f22b24d..b0f2d58 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -295,8 +295,14 @@ jobs: BOOTSTRAPPED=false START=$SECONDS while [ $((SECONDS - START)) -lt 180 ]; do - STATUS=$(docker exec torproxy-gateway-smoke curl -fsS --connect-timeout 2 --max-time 5 http://127.0.0.1:8080/status 2>/dev/null | jq -r '.status // empty' || true) - if [ "$STATUS" = "connected" ]; then + # Wait for Tor bootstrap progress (100%), not /status + # "connected": the latter additionally requires the + # external check.torproject.org egress probe, whose + # intermittent unreachability from CI runners flaked + # this step although Tor itself bootstrapped in seconds + # (PR #121). Egress is verified separately below. + PROGRESS=$(docker exec torproxy-gateway-smoke curl -fsS --connect-timeout 2 --max-time 5 http://127.0.0.1:8080/health 2>/dev/null | jq -r '.tor.progress // empty' || true) + if [ "$PROGRESS" = "100" ]; then echo "✅ Gateway container bootstrapped!" BOOTSTRAPPED=true break diff --git a/tests/unit/test_ci_workflows.py b/tests/unit/test_ci_workflows.py index a1dfe32..659e48e 100644 --- a/tests/unit/test_ci_workflows.py +++ b/tests/unit/test_ci_workflows.py @@ -83,6 +83,16 @@ def test_gateway_bootstrap_is_polled_inside_the_container_in_live_action() -> No ).read_text(encoding="utf-8") assert "docker exec torproxy-canary-gateway curl" in live assert "http://localhost:8089/status" not in live + # Bootstrap waits must use Tor progress (100%), not /status + # "connected", which additionally depends on the external + # check.torproject.org egress probe (PR #121 flake). + assert ".tor.progress" in live + assert ( + "8080/status" + not in live.split("Verify Transparent Tor Gateway Mode")[1].split( + "Verify GeoIP Upstream Size Drift Guard" + )[0] + ) def test_pr_gate_runs_live_verification_before_merge() -> None: