diff --git a/deploy/update.sh b/deploy/update.sh index 7350a71..98217a7 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -529,6 +529,29 @@ phase restart "Restarting services" # (neues Image, geänderte Konfiguration), ist sein Netz-Namensraum ein anderer — # und nur dann müssen die Nachbarn hinterher und die gemerkten Ströme weg. Ein # blosses „lief schon" lässt beides in Ruhe. +# EINMALIGE Umstellung: das Compose-Netz bekommt ein erklärtes Subnetz, damit +# vpn-hub eine feste Adresse haben kann (siehe docker-compose.yml, ganz unten). +# +# Docker kann ein bestehendes Netz nicht umdefinieren — es muss neu angelegt +# werden, und das geht nur, wenn KEIN Container mehr daranhängt. `docker compose +# up -d` allein scheitert daran mit "network … has active endpoints" und lässt +# den Stapel halb unten stehen. Beim Bauen genau so passiert; deshalb macht das +# Deployment es geordnet, statt darüber zu stolpern. +# +# Nach dieser einen Umstellung stimmen die Werte überein und der Block tut nie +# wieder etwas. +net_migrated=false +want_subnet="$(sed -n 's/^CLUPILOT_NET_SUBNET=//p' .env 2>/dev/null | tail -1)" +want_subnet="${want_subnet:-172.18.0.0/16}" +net_name="${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}_default" +have_subnet="$(docker network inspect "$net_name" --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null || true)" + +if [[ -n "$have_subnet" && "$have_subnet" != "$want_subnet" ]]; then + log "Das Netz bekommt ein festes Subnetz ($want_subnet) — dafür muss der Stapel einmal ganz herunter." + docker compose down --remove-orphans || true + net_migrated=true +fi + hub_before="$(docker compose ps -q vpn-hub 2>/dev/null || true)" docker compose up -d hub_after="$(docker compose ps -q vpn-hub 2>/dev/null || true)" @@ -546,6 +569,12 @@ hub_rebuilt=false if [[ -n "$hub_before" && "$hub_before" != "$hub_after" ]]; then hub_rebuilt=true fi +# Nach der Netz-Umstellung war der Hub vorher schon weg, der Vergleich oben +# greift also nicht — die gemerkten Ströme zeigen aber trotzdem auf die alte, +# automatisch vergebene Adresse und müssen weg. +if [[ "$net_migrated" == true ]]; then + hub_rebuilt=true +fi # Workers hold their PHP classes for the life of the process; without this they # keep running the code from before the update. # diff --git a/docker-compose.yml b/docker-compose.yml index 7da93f5..a5430da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -101,6 +101,24 @@ services: - net.ipv4.ip_forward=1 ports: - "${WG_HUB_PORT:-51820}:51820/udp" + # FESTE Adresse, nicht die naechste freie. + # + # Der veroeffentlichte UDP-Port wird von Docker als Weiterleitung auf die + # Container-Adresse eingetragen, und der Kernel merkt sich jeden laufenden + # UDP-Strom samt dieser Adresse. Bekam der Container beim Neubau eine andere, + # zeigten alle gemerkten Stroeme ins Leere — und sie verfallen nicht, weil + # WireGuards Lebenszeichen sie alle 25 Sekunden auffrischt. Das war die + # Ursache dafuer, dass ein Host nach einem Neubau NIE von allein zurueckkam. + # + # Mit fester Adresse entsteht nach einem Neubau exakt dieselbe Weiterleitung. + # Die gemerkten Stroeme bleiben gueltig, und es gibt nichts mehr aufzuraeumen. + # + # .240 ist bewusst hoch: Docker vergibt automatisch von unten, hier kommt es + # also nie zu einer Kollision. Beides ueber .env aenderbar, falls das Subnetz + # auf einem Server schon anderweitig belegt ist. + networks: + default: + ipv4_address: ${CLUPILOT_VPN_HUB_IP:-172.18.0.240} volumes: - wireguard:/etc/wireguard @@ -330,3 +348,13 @@ volumes: redis-data: wireguard: dns-hosts: + +# Ein erklaertes Subnetz, damit vpn-hub oben eine feste Adresse bekommen kann — +# ohne das lehnt Docker `ipv4_address` ab. Der Wert ist der, den Docker hier +# ohnehin schon selbst gewaehlt hatte; er steht jetzt nur schriftlich da, statt +# sich bei jedem Neuaufbau des Netzes neu zu ergeben. +networks: + default: + ipam: + config: + - subnet: ${CLUPILOT_NET_SUBNET:-172.18.0.0/16} diff --git a/docs/runbooks/tunnel-recovery.md b/docs/runbooks/tunnel-recovery.md index bdc0e94..ae0b748 100644 --- a/docs/runbooks/tunnel-recovery.md +++ b/docs/runbooks/tunnel-recovery.md @@ -139,7 +139,13 @@ Fehlt das Werkzeug: `sudo apt-get install -y conntrack`. Danach dreißig Sekunde warten — jeder Zugang meldet sich von selbst wieder, alle 25 Sekunden läuft sein Keepalive. -> **Seit v1.4.6 macht das Update das selbst.** Es merkt sich die Container-ID +> **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" @@ -219,7 +225,7 @@ Vier Dinge, die man vorher macht und nicht, wenn es brennt. ## Was inzwischen NICHT mehr passieren sollte -Vier Ursachen sind abgestellt. Wer sie trotzdem sieht, hat einen Server auf +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 @@ -244,6 +250,10 @@ altem Stand. 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 diff --git a/tests/Feature/DeploymentRunsAsTheAppUserTest.php b/tests/Feature/DeploymentRunsAsTheAppUserTest.php index 441706f..649e719 100644 --- a/tests/Feature/DeploymentRunsAsTheAppUserTest.php +++ b/tests/Feature/DeploymentRunsAsTheAppUserTest.php @@ -213,9 +213,13 @@ it('laesst die Terminal-Bruecke dort stehen, wo ein Host ueberhaupt erreichbar i // Deshalb: kein Alias. Der Dienstname des Nachbarn tut es auch. expect($compose)->not->toContain('aliases:'); + // Ein `networks:`-Block am Hub selbst ist dagegen erlaubt und noetig — er + // traegt die FESTE Adresse. Der Unterschied: ein Alias war reine Kosmetik + // und hat einen Neubau ausgeloest, die feste Adresse macht einen Neubau + // folgenlos. Was hier nicht stehen darf, steht eine Zeile hoeher. $hub = preg_split('/^ vpn-hub:$/m', $compose)[1] ?? ''; $hub = preg_split('/^ \S/m', $hub)[0] ?? ''; - expect($hub)->not->toContain('networks:'); + expect($hub)->toContain('ipv4_address:'); // Angesprochen wird der Container, dem der Namensraum GEHOERT. Ein // Mitbewohner hat keinen eigenen DNS-Eintrag. @@ -294,3 +298,39 @@ it('startet den Tunnel-Container beim Ausrollen nicht neu', function () { ->and($update)->toContain('hub_before') ->and($update)->toContain('hub_after'); }); + +it('gibt dem Tunnel eine feste Adresse, damit gemerkte Stroeme nie ins Leere zeigen', function () { + // Die Wurzel des ganzen Uebels: Docker traegt den veroeffentlichten UDP-Port + // als Weiterleitung auf die Container-Adresse ein, und der Kernel merkt sich + // jeden laufenden Strom samt dieser Adresse. Bekam der Container beim Neubau + // eine andere, zeigten alle gemerkten Stroeme ins Leere — und sie verfallen + // nicht, weil WireGuards Lebenszeichen sie alle 25 Sekunden auffrischt. + // + // Mit fester Adresse entsteht nach einem Neubau exakt dieselbe Weiterleitung. + // Nachgemessen: 172.18.0.240 vor und nach `up -d --force-recreate vpn-hub`. + $compose = File::get(base_path('docker-compose.yml')); + + $hub = preg_split('/^ vpn-hub:$/m', $compose)[1] ?? ''; + $hub = preg_split('/^ \S/m', $hub)[0] ?? ''; + + expect($hub)->toContain('ipv4_address:') + // Ueber .env aenderbar, falls das Subnetz auf einem Server belegt ist. + ->and($hub)->toContain('CLUPILOT_VPN_HUB_IP'); + + // Eine feste Adresse geht nur in einem Netz mit erklaertem Subnetz. + expect($compose)->toContain('CLUPILOT_NET_SUBNET'); + + // Und die Umstellung darauf muss GEORDNET passieren: Docker kann ein + // bestehendes Netz nicht umdefinieren, es muss neu angelegt werden — und das + // scheitert, solange auch nur ein Container daranhaengt. `up -d` allein + // bricht dann mit "network … has active endpoints" ab und laesst den Stapel + // halb unten stehen. Beim Bauen genau so passiert. + $update = File::get(base_path('deploy/update.sh')); + + expect($update)->toContain('net_migrated') + ->and($update)->toContain('docker compose down --remove-orphans') + // Nach der Umstellung zeigen die gemerkten Stroeme auf die alte, + // automatisch vergebene Adresse — der Vergleich der Container-IDs greift + // dabei nicht, also muss das Aufraeumen ausdruecklich ausgeloest werden. + ->and($update)->toContain('"$net_migrated" == true'); +});