CluPilotCloud/deploy/rescue-tunnel.sh

169 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# CluPilot — den Tunnel wieder hinbekommen.
#
# Ein Befehl statt einer Befehlsfolge, die man sich im Ernstfall merken müsste.
# Das Skript stellt der Reihe nach fest, WO es klemmt, sagt es in klaren Worten,
# und behebt, was sich von hier aus beheben lässt.
#
# Es macht nichts Zerstörerisches: kein Neubau, kein Löschen, kein Neustart des
# Tunnel-Containers. Genau der wäre nämlich die Ursache und nicht die Lösung —
# ein Neubau schreibt die Weiterleitungsregeln neu und reisst alle Sitzungen ab.
#
# sudo -u clupilot bash deploy/rescue-tunnel.sh
#
# Ausführlich: docs/runbooks/tunnel-recovery.md
set -uo pipefail
cd "$(cd "$(dirname "$0")/.." && pwd)"
log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; }
bad() { printf '\033[1;31m ✗\033[0m %s\n' "$*"; }
# Wie viele Stellen etwas gefunden haben, das nicht ausgerichtet ist, wie es
# sein sollte — und zwar am ENDE des Laufs, nicht dem, was ein einzelner Griff
# gerade behoben zu haben glaubt. Der Aufrufer (deploy/update-agent.sh) macht
# bisher NUR den Exit-Code zur Wahrheit; ohne diesen Zähler endete das Skript
# unter `set -uo pipefail` (kein `-e`) nach jedem `bad`/`warn` auf ein
# schlichtes `echo` weiter unten — Exit 0, auch wenn mittendrin etwas offen
# blieb. Nicht jede `bad`/`warn`-Zeile zählt: eine, die einen unmittelbar
# folgenden `exit 1` hat (Container startet nicht, wg0 laesst sich nicht
# hochziehen), braucht den Zähler nicht — der Exit-Code steht da schon fest.
# Und eine rein informative Meldung (dieser Server hat noch keinen eigenen
# Tunnel-Container) sagt nichts darüber, ob DIESER Lauf etwas nicht
# hinbekommen hat — sie stünde bei jedem Lauf auf einem älteren Wirt, auch
# wenn der Tunnel tadellos steht.
probleme=0
if [[ $EUID -eq 0 ]]; then
echo "Bitte als Dienstbenutzer starten, nicht als root:" >&2
echo " sudo -u clupilot bash $0" >&2
exit 1
fi
hub() { docker compose exec -T vpn-hub "$@" 2>/dev/null; }
WG_PORT="$(grep -m1 '^WG_HUB_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"'"'"' ')"
WG_PORT="${WG_PORT:-51820}"
# ── 1. Läuft der Container, in dessen Namensraum wg0 lebt? ───────────────────
log "Der Container mit dem Tunnel"
if ! docker compose ps --status running vpn-hub 2>/dev/null | grep -q vpn-hub; then
bad "vpn-hub läuft nicht. Ich starte ihn."
docker compose up -d vpn-hub || true
sleep 8
fi
if docker compose ps --status running vpn-hub 2>/dev/null | grep -q vpn-hub; then
ok "läuft"
else
bad "startet nicht. Das Protokoll sagt warum:"
docker compose logs --tail=30 vpn-hub
exit 1
fi
# Prozess 1 verrät den Stand. Gehört der Namensraum noch dem
# Warteschlangen-Arbeiter, nimmt ihn jede Freigabe und jeder Absturz mit.
if hub sh -c 'tr "\0" " " < /proc/1/cmdline' | grep -q 'vpn-hub'; then
ok "Der Tunnel hat einen eigenen Container (v1.4.8 oder neuer)"
else
warn "Dieser Server hat den eigenen Tunnel-Container noch nicht."
warn "Bis dahin nimmt jede Freigabe den Tunnel mit. Update einspielen."
fi
# ── 2. Steht wg0? ────────────────────────────────────────────────────────────
log "Das Interface wg0"
if ! hub wg show wg0 >/dev/null 2>&1; then
bad "wg0 fehlt. Ich ziehe es hoch."
hub wg-quick up wg0 || true
sleep 3
fi
if hub wg show wg0 >/dev/null 2>&1; then
ok "steht"
else
bad "lässt sich nicht hochziehen. Liegt die Konfiguration überhaupt da?"
hub ls -l /etc/wireguard/ || true
echo
bad "Weiter in docs/runbooks/tunnel-recovery.md, Abschnitt 'Der Tunnel kommt nicht hoch'."
exit 1
fi
peers="$(hub wg show wg0 peers | grep -c . || true)"
if [[ "${peers:-0}" -gt 0 ]]; then
ok "$peers Zugänge geladen"
else
# Nicht bloss eine Randnotiz: ohne einen einzigen Zugang tut der Tunnel
# nichts, egal wie gesund wg0 sonst aussieht. Vorher endete der Lauf hier
# trotzdem auf Exit 0.
warn "Kein einziger Zugang geladen — /etc/wireguard/wg0.conf ansehen."
probleme=$((probleme + 1))
fi
# ── 3. Kommt auch etwas an? ──────────────────────────────────────────────────
#
# Das eigentliche Krankheitsbild: der Hub sendet, aber empfängt nichts. Dann
# zeigen die im Kernel gemerkten UDP-Ströme noch auf einen Container, den es
# nicht mehr gibt — und sie verfallen NICHT, weil WireGuards Keepalive sie alle
# 25 Sekunden auffrischt. Das heilt nie von allein.
log "Kommen Antworten an?"
stumm=0
while read -r line; do
[[ -z "$line" ]] && continue
empfangen="$(awk '{print $1}' <<<"$line")"
[[ "$empfangen" == "0" ]] && stumm=$((stumm + 1))
done < <(hub wg show wg0 transfer | awk '{print $2, $3}')
gesamt="$(hub wg show wg0 transfer | grep -c . || echo 0)"
if [[ "${stumm:-0}" -gt 0 ]]; then
warn "$stumm von $gesamt Zugängen haben noch NIE etwas geschickt."
log "Gemerkte UDP-Ströme wegräumen"
if sudo -n conntrack -D -p udp --dport "$WG_PORT" >/dev/null 2>&1; then
ok "erledigt — die Zugänge bauen sich in den nächsten 30 Sekunden neu auf"
else
# DAS Krankheitsbild, das nie von allein heilt (siehe Kopf dieser
# Datei): stumme Zugänge bleiben stumm, wenn dieser Griff scheitert.
# Der conntrack-Sudoers-Eintrag ist Handarbeit (siehe unten) und auf
# einem frischen Wirt der Normalfall, nicht die Ausnahme — genau
# deshalb darf ein gescheiterter Versuch hier nicht als Erfolg enden.
bad "Dafür fehlt mir Root. Bitte einmal von Hand ausführen:"
echo
echo " sudo conntrack -D -p udp --dport $WG_PORT"
echo
warn "Damit ich das künftig selbst darf, siehe docs/runbooks/tunnel-recovery.md,"
warn "Abschnitt 'Einmal einrichten'."
probleme=$((probleme + 1))
fi
else
ok "Alle Zugänge haben schon Daten geschickt"
fi
# ── 4. Der Stand, zum Mitlesen ───────────────────────────────────────────────
log "Stand jetzt"
hub wg show wg0 || true
echo
log "Wenn ein HOST weiter fehlt, während andere Zugänge stehen"
echo " Dann liegt es auf der Maschine, nicht hier. Über die Konsole des"
echo " Anbieters (SSH ist von aussen zu, das ist Absicht):"
echo
echo " systemctl status wg-quick@wg0"
echo " systemctl restart wg-quick@wg0"
echo
echo " Kommt man dort nicht weit genug, macht dieses Skript auf dem Host SSH"
echo " wieder auf — und muss danach zurückgenommen werden:"
echo
echo " /usr/local/sbin/clupilot-emergency-open-firewall.sh"
echo
# Der Aufrufer (deploy/update-agent.sh) macht den Exit-Code zur einzigen
# Wahrheitsquelle — siehe `probleme` oben. Kein Problem gezählt heisst hier
# tatsächlich Exit 0; eins oder mehr heisst, der Lauf hat nicht ausgerichtet,
# was nötig war, auch wenn er bis hierher durchgelaufen ist.
exit "$probleme"