This repository has been archived on 2026-05-20. You can view files and clone it, but cannot push or open issues or pull requests.
media-acquisition/scripts/killswitch-test.sh
obsidian-ai d300d83ce1 init: media-acquisition pipeline scaffold
Self-hosted BitTorrent + arr-stack + catalog-update pipeline targeting
nullstone (Debian 13). Replaces the legacy onyx -> rsync -> import
round-trip.

Contents:
- README.md          headline + ASCII architecture diagram + quickstart
- CLAUDE.md          project rules (mirrors beta-flix style)
- .gitignore         secrets dirs (.env, gluetun, qbt config, ssh keys)
- .gitleaksignore    allowlist nullstone LAN addr + Tailscale CGNAT
- docs/architecture.md   the plan in detail (gluetun + qbt + arr + catalog)
- docs/migration.md  onyx-qbt -> nullstone-qbt runbook (3 phases)
- docs/trackers.md   tracker schema + IP-pinning + ratio notes (user-curated)
- compose/docker-compose.yml  gluetun v3.40 + qbt 5.0.5 (netns=gluetun) +
                              sonarr/radarr/prowlarr (hotio) + betaflix-catalog
- compose/.env.example       documented env-var template (no secrets)
- compose/traefik/arr.yml    file-provider for qbt/sonarr/radarr/prowlarr
                             .s8n.ru subdomains, LAN+TS only via
                             trusted-only@file + authentik-forwardauth@file
- catalog/catalog.py         Flask service, ~340 LoC, /sonarr + /radarr +
                             /healthz; pulls beta-flix, inserts alphabetic
                             row into MEDIA-LIST.md, writes run log, commits
                             + pushes as obsidian-ai. Idempotent via
                             payload-hash cache.
- catalog/Dockerfile         python:3.12-slim + git + tini
- catalog/requirements.txt   flask + jinja2 + requests + gitpython + pyyaml (pinned)
- catalog/templates/*.j2     run log + catalog row Jinja templates
- catalog/README.md          service docs
- scripts/migrate-onyx.sh    phase-2 helper (rsync + .torrent ship, dry-run by default)
- scripts/add-tracker.sh     Prowlarr API helper
- scripts/killswitch-test.sh gluetun kill-switch verification (3 steps)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 01:15:43 +01:00

96 lines
3.2 KiB
Bash
Executable file

#!/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."