#!/usr/bin/env bash # scripts/killswitch-test.sh — verify gluetun blocks traffic when VPN drops. # # Test plan (per docs/architecture.md §b): # # 1. With gluetun UP, qbt MUST resolve api.ipify.org and return an IP # that is NOT nullstone's WAN IP (i.e. Proton exit). # 2. Stop gluetun. qbt's container MUST NOT be able to reach the internet # (curl hangs / fails fast). If it succeeds → kill-switch leak → ABORT. # 3. Restart gluetun, re-verify step 1. # # Run from anywhere with `docker` access on the host. Idempotent. set -euo pipefail GLUETUN="${GLUETUN_CONTAINER:-gluetun}" QBT="${QBT_CONTAINER:-qbittorrent}" TIMEOUT="${TIMEOUT:-8}" red() { printf '\033[31m%s\033[0m\n' "$*"; } green() { printf '\033[32m%s\033[0m\n' "$*"; } yellow(){ printf '\033[33m%s\033[0m\n' "$*"; } require_container() { if ! docker inspect "$1" >/dev/null 2>&1; then red "FAIL: container '$1' not found"; exit 1 fi } require_container "$GLUETUN" require_container "$QBT" # --- Step 0: discover nullstone's WAN IP (so we can detect leaks). WAN_IP="$(curl -sf --max-time "$TIMEOUT" https://api.ipify.org || true)" if [ -z "$WAN_IP" ]; then yellow "WARN: could not fetch host WAN IP — leak detection will be best-effort" else echo "Host WAN IP: $WAN_IP" fi # --- Step 1: VPN-up check echo echo "Step 1: VPN-up — qbt should exit via Proton." if ! docker inspect -f '{{.State.Running}}' "$GLUETUN" | grep -q true; then yellow "gluetun not running — starting…" docker start "$GLUETUN" >/dev/null sleep 5 fi VPN_IP="$(docker exec "$QBT" curl -sf --max-time "$TIMEOUT" https://api.ipify.org || true)" if [ -z "$VPN_IP" ]; then red "FAIL: qbt could not reach api.ipify.org even with gluetun up. Check VPN config." exit 1 fi echo "qbt sees IP: $VPN_IP" if [ -n "$WAN_IP" ] && [ "$VPN_IP" = "$WAN_IP" ]; then red "FAIL: qbt's IP == host WAN IP. Traffic is NOT going through VPN." exit 1 fi green "OK: qbt egressing via VPN." # --- Step 2: kill-switch check echo echo "Step 2: kill-switch — stop gluetun, qbt MUST fail to reach internet." docker stop "$GLUETUN" >/dev/null sleep 2 set +e LEAK_IP="$(docker exec "$QBT" curl -sf --max-time "$TIMEOUT" https://api.ipify.org 2>/dev/null)" RC=$? set -e docker start "$GLUETUN" >/dev/null if [ $RC -eq 0 ] && [ -n "$LEAK_IP" ]; then red "FAIL: kill-switch broken — qbt reached the internet with VPN down." red " Leaked IP: $LEAK_IP" red " Tear down the stack and investigate before adding any torrents." exit 1 fi green "OK: qbt could not reach internet with gluetun stopped." # --- Step 3: re-verify VPN comes back echo echo "Step 3: VPN restart — wait for gluetun to be healthy again." for i in $(seq 1 30); do if [ "$(docker inspect -f '{{.State.Health.Status}}' "$GLUETUN" 2>/dev/null || echo none)" = "healthy" ]; then break fi sleep 2 done RECOVERY_IP="$(docker exec "$QBT" curl -sf --max-time "$TIMEOUT" https://api.ipify.org || true)" if [ -z "$RECOVERY_IP" ]; then yellow "WARN: gluetun did not recover within 60s. Check 'docker logs $GLUETUN'." exit 1 fi green "OK: VPN recovered. qbt sees IP $RECOVERY_IP." echo green "Kill-switch test PASSED. Safe to seed."