# Runbook: Wenn der Tunnel weg ist

Der WireGuard-Tunnel trägt alles: die Konsole, jeden Host, jede
Provisionierung, das Terminal. Fällt er aus, sieht es aus, als sei das ganze
Produkt kaputt — obwohl meistens nur eine Sitzung nicht wieder aufgebaut wurde.

Dieses Runbook beantwortet zwei Fragen. Erstens: **womit komme ich noch hinein,
wenn der Tunnel nicht steht?** Zweitens: **wie bringe ich ihn zurück?**

---

## Die eine Regel

**Kein Weg zurück darf durch den Tunnel führen, der gerade kaputt ist.**

Alles unten ist danach sortiert. Wer als Rettungsweg etwas einplant, das selbst
WireGuard braucht, hat keinen Rettungsweg eingeplant.

---

## Die vier Wege hinein, in dieser Reihenfolge

### 1. SSH auf den CluPilot-Server

Der Hebel für fast alles. Er hängt an nichts, was hier beschrieben wird — der
Server ist über seine öffentliche Adresse erreichbar, unabhängig vom Tunnel.

```bash
ssh <benutzer>@<clupilot-server>
cd /opt/clupilot
```

Von hier aus laufen alle Befehle weiter unten.

### 2. Die Konsole des Anbieters (Hetzner: KVM oder Rettungssystem)

Für einen **Host**, nicht für den CluPilot-Server. Immer verfügbar, hängt an
keinem Dienst von uns. Der Weg, wenn ein Host weder über den Tunnel noch über
SSH antwortet.

### 3. Das Notfallskript auf dem Host

Liegt auf jedem übernommenen Host:

```
/usr/local/sbin/clupilot-emergency-open-firewall.sh
```

`SecureHostFirewall` lässt Port 22 nur aus dem WireGuard-Netz zu. Steht der
Tunnel nicht, ist SSH aus dem Internet **zu** — das ist Absicht und genau der
Grund, warum es dieses Skript gibt. Aufgerufen wird es über die Konsole des
Anbieters (Weg 2), nicht per SSH; per SSH käme man ja gerade nicht hin.

Danach ist der Host wieder aus dem Internet erreichbar — und bleibt es, bis
jemand die Regeln zurückschreibt. **Nach der Reparatur, vom CluPilot-Server
aus:**

```bash
cd /opt/clupilot
sudo -u clupilot docker compose exec -T app php artisan clupilot:refresh-host-firewall --host=<name-oder-ip>
```

Ohne `--host` nimmt der Befehl jeden übernommenen Host; `--dry-run` sagt zuerst,
welche das wären. Er startet je Host einen `host-firewall`-Lauf, der denselben
Schritt `SecureHostFirewall` fährt wie die Übernahme — der Lauf ist also in der
Konsole zu sehen, wird wiederholt und prüft vorher von der Hostseite aus nach,
dass der Tunnel steht.

> **Nicht** den ursprünglichen Übernahme-Lauf erneut anstoßen. `SecureHostFirewall`
> kürzt sich über den `host_firewall`-Brotkrumen ab, und der hängt am LAUF: auf
> einem bereits übernommenen Host würde der Schritt gar nichts tun und trotzdem
> „erledigt" melden. Genau darum gibt es den Befehl oben.

Derselbe Befehl ist auch der Weg, einem Host, der vor dem Frühwarnsystem
übernommen wurde, die beiden Sperrmengen (`clupilot_blocked`,
`clupilot_blocked6`) nachzureichen. Ohne sie scheitert jede Sperre still auf der
Maschine, während sie im Portal als aktiv steht.

### 4. Eine WireGuard-Konfiguration, die offline liegt

In der Zugangsliste der Konsole gibt es zu jedem Mitarbeiter-Zugang einen
Download. **Eine davon gehört außerhalb der Konsole aufbewahrt** — Passwort-Safe,
zweites Gerät, ausgedruckt. Sonst braucht man die Konsole, um an den Zugang zu
kommen, der einen zur Konsole bringt.

---

## Zuerst: ein Befehl, der das meiste allein macht

```bash
cd /opt/clupilot && sudo -u clupilot bash deploy/rescue-tunnel.sh
```

Er stellt der Reihe nach fest, wo es klemmt, startet und zieht hoch, was fehlt,
räumt die gemerkten Ströme weg — und sagt am Ende, was noch von Hand bleibt. Er
fasst den Tunnel-Container **nicht** neu an; genau das wäre die Ursache und
nicht die Lösung.

Was er tut, steht unten einzeln, falls man es von Hand nachvollziehen will.

---

## Von Hand feststellen, wo es klemmt

Vom CluPilot-Server aus, in dieser Reihenfolge. Jede Zeile schließt eine
Möglichkeit aus.

```bash
cd /opt/clupilot

# 1. Steht wg0 überhaupt, und wie viele Zugänge kennt es?
sudo -u clupilot docker compose exec -T vpn-hub wg show

# 2. Läuft der Container, dem der Namensraum mit wg0 gehört?
sudo -u clupilot docker compose ps vpn-hub

# 3. Was sagt er beim Starten?
sudo -u clupilot docker compose logs --tail=40 vpn-hub
```

**Kein Interface, kein `wg0`** → weiter bei „Der Tunnel kommt nicht hoch".

**Interface da, Zugänge da, aber `Letzter Kontakt: nie` oder uralt, und die
Zahlen stehen bei ↑ etwas / ↓ 0 B** → weiter bei „Der Tunnel steht, aber
niemand kommt an".

**Ein einzelner Zugang fehlt, die anderen stehen** → das Problem liegt auf
dessen Gegenseite, nicht hier. Bei einem Host: Weg 2 und `systemctl status
wg-quick@wg0` auf der Maschine.

---

## Der Tunnel steht, aber niemand kommt an

Das häufigste Bild, und das mit der unauffälligsten Ursache.

Der Kernel merkt sich laufende UDP-Ströme samt Ziel. Wird der Tunnel-Container
**neu gebaut** (nicht neu gestartet — neu gebaut), bekommt er eine neue Adresse
im Compose-Netz, und die Weiterleitung für den WireGuard-Port wird neu
geschrieben. Die gemerkten Einträge zeigen aber weiter auf den alten Container.

Und sie verfallen nicht: WireGuard schickt alle 25 Sekunden ein Lebenszeichen
und hält den kaputten Eintrag damit am Leben. Der Zustand heilt also **nie** von
allein.

Daran erkennt man ihn:

* Ein Telefon kommt nach Aus- und Einschalten sofort zurück — ein neuer
  Quellport bekommt einen frischen Eintrag.
* Ein **Host** kommt gar nicht zurück — er schickt stur vom selben Port.
* Auf der Zugangsseite: ↑ wächst weiter, ↓ bleibt bei 0 B.

Der Griff, auf dem **Wirt**, nicht im Container:

```bash
sudo conntrack -D -p udp --dport 51820
```

Fehlt das Werkzeug: `sudo apt-get install -y conntrack`. Danach dreißig Sekunden
warten — jeder Zugang meldet sich von selbst wieder, alle 25 Sekunden läuft sein
Keepalive.

> **Seit v1.4.9 kann das gar nicht mehr passieren.** Der Hub hat eine feste
> Adresse im Compose-Netz (`172.18.0.240`, über `CLUPILOT_VPN_HUB_IP`
> änderbar). Nach einem Neubau entsteht damit exakt dieselbe Weiterleitung, und
> die gemerkten Ströme bleiben gültig. Was unten steht, gilt für Server, die
> älter sind — und als Rückfahrkarte, falls jemand das Subnetz ändert.
>
> **Seit v1.4.6 macht das Update das Aufräumen ohnehin selbst.** Es merkt sich die Container-ID
> vor und nach `docker compose up -d`; hat sie sich geändert, räumt es die
> Ströme weg. Kann es das nicht (es läuft als Dienstbenutzer, der Griff braucht
> Root), steht der Befehl als Warnung im Protokoll. Siehe „Einmal einrichten"
> unten.

---

## Der Tunnel kommt nicht hoch

`wg show` zeigt gar kein Interface.

```bash
cd /opt/clupilot

# Von Hand hochziehen — sagt auch gleich, woran es scheitert.
sudo -u clupilot docker compose exec -T vpn-hub wg-quick up wg0

# Steht die Konfiguration überhaupt noch im Datenträger?
sudo -u clupilot docker compose exec -T vpn-hub ls -l /etc/wireguard/
```

Die Zugänge stehen **in `wg0.conf`** und überleben einen Neustart. Fehlt die
Datei, ist der `wireguard`-Datenträger das Problem, nicht WireGuard.

Läuft der Container gar nicht:

```bash
sudo -u clupilot docker compose up -d vpn-hub
sudo -u clupilot docker compose logs --tail=40 vpn-hub
```

Prozess 1 ist `/usr/local/bin/vpn-hub`. Das Skript zieht wg0 hoch und tut danach
nichts mehr — genau das ist die Aufgabe. Steht dort etwas anderes, läuft der
Server auf einem Stand vor v1.4.8, und der Tunnel hängt noch am
Provisionierungs-Arbeiter.

---

## Ein Host kommt nicht zurück, alle anderen schon

Dann liegt es auf der Maschine. Über die Konsole des Anbieters (Weg 2):

```bash
systemctl status wg-quick@wg0
wg show
systemctl restart wg-quick@wg0
```

`ConfigureWireguard` schreibt dem Host `Endpoint = <Hub>` und
`PersistentKeepalive = 25` — er meldet sich also von sich aus, alle 25 Sekunden.
Tut er das nicht, läuft wg0 dort nicht.

Kommt man über die Konsole nicht weit genug, weil SSH zu ist: erst Weg 3, dann
weiter.

---

## Einmal einrichten, damit der Plan später trägt

Vier Dinge, die man vorher macht und nicht, wenn es brennt.

1. **Eine WireGuard-Konfiguration offline ablegen.** Siehe Weg 4.
2. **Zugang zur Anbieterkonsole prüfen** — einmal einloggen, bevor man es
   braucht. Ein vergessenes Passwort ist im Ernstfall ein zweiter Ausfall.
3. **Dem Dienstbenutzer den conntrack-Griff erlauben**, damit das Update ihn
   selbst ausführen kann. In `/etc/sudoers.d/clupilot-conntrack`:

   ```
   clupilot ALL=(root) NOPASSWD: /usr/sbin/conntrack -D -p udp --dport 51820
   ```

   Genau dieser eine Aufruf, nicht `conntrack` allgemein.
4. **`conntrack` installieren**, falls nicht vorhanden:
   `sudo apt-get install -y conntrack`.

---

## Was inzwischen NICHT mehr passieren sollte

Fünf Ursachen sind abgestellt. Wer sie trotzdem sieht, hat einen Server auf
altem Stand.

* **Bis v1.4.3** hing wg0 am Leben des Warteschlangen-Arbeiters: Prozess 1 des
  Containers war `php artisan queue:work`. Ein Zeitüberlauf, ein fataler Fehler
  oder ein Speicherlimit beendete den Arbeiter, damit den Container, damit den
  Tunnel. Das las sich von außen als „das VPN ist unzuverlässig". Seit v1.4.4
  läuft der Arbeiter in einer Schleife neben dem Tunnel.
* **v1.4.2** gab dem Tunnel-Container einen Netz-Alias. Ein Alias ist Teil der
  Netzkonfiguration, und Compose baut einen Container neu, sobald die sich
  ändert — mit allem, was oben unter „gemerkte Ströme" steht. Seit v1.4.4 ist
  der Alias weg; nginx spricht die Terminal-Brücke unter dem Dienstnamen des
  Nachbarn an.
* **Bis v1.4.5** startete jedes Update `queue-provisioning` neu und riss damit
  jede Sitzung ab, auch wenn sich nur PHP-Code geändert hatte. Seit v1.4.6
  bekommt der Arbeiter ein Signal und wird von seiner Schleife neu gestartet,
  während der Container steht. Dasselbe gilt seither für den Knopf „Dienste neu
  starten" in der Konsole — der lief über denselben Befehl.
* **Bis v1.4.7** wohnte wg0 im Container des Provisionierungs-Arbeiters, also im
  Abbild `clupilot-app`. Das wird bei fast jeder Freigabe neu gebaut, und ein
  neu gebauter Container bekommt eine neue Adresse — mit allem, was oben unter
  „gemerkte Ströme" steht. Seit **v1.4.8** gehört der Namensraum einem eigenen
  Container `vpn-hub` mit eigenem, selten wechselndem Abbild. Nachgemessen:
  App-Abbild neu gebaut, Arbeiter und Brücke neu erzeugt — der Hub blieb
  derselbe Container, wg0 und beide Zugänge unangetastet.
* **Bis v1.4.8** bekam der Hub bei jedem Neubau die nächste freie Adresse im
  Compose-Netz. Genau darauf zeigen die gemerkten UDP-Ströme — eine neue Adresse
  machte sie alle ungültig. Seit **v1.4.9** hat er eine feste. Nachgemessen:
  `172.18.0.240` vor und nach `up -d --force-recreate vpn-hub`.

Erzwungen durch `tests/Feature/DeploymentRunsAsTheAppUserTest.php` — der Container
darf nicht in der Neustart-Liste stehen, und der conntrack-Griff muss im Skript
bleiben.
