diff --git a/deploy/update.sh b/deploy/update.sh index cabe638..49be72f 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -525,10 +525,32 @@ fi normalise_ownership phase restart "Restarting services" +# Vorher/nachher merken: nur wenn `up -d` den Tunnel-Container WIRKLICH neu baut +# (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. +hub_before="$(docker compose ps -q queue-provisioning 2>/dev/null || true)" docker compose up -d +hub_after="$(docker compose ps -q queue-provisioning 2>/dev/null || true)" # Workers hold their PHP classes for the life of the process; without this they # keep running the code from before the update. -docker compose restart queue queue-provisioning scheduler reverb +# +# queue-provisioning steht mit Absicht NICHT in dieser Liste. In seinem +# Netz-Namensraum lebt wg0, und `docker compose restart` baut den Namensraum neu +# auf — jede WireGuard-Sitzung reisst dabei ab, die des Betreibers am Telefon +# wie die jedes Hosts. Fuer ein Update, das nur PHP-Code aendert, ist das ein +# absurder Preis. +# +# Seit der Arbeiter dort in einer Schleife laeuft (docker/provisioning-worker.sh) +# geht es billiger: `queue:restart` setzt ein Signal, der Arbeiter beendet sich +# nach dem laufenden Auftrag, und die Schleife startet ihn mit dem neuen Code neu. +# Der Container bleibt stehen, wg0 bleibt oben, niemand merkt etwas. +# +# `|| true`: laeuft der Container gerade nicht, ist das kein Grund, das +# Deployment abzubrechen — `docker compose up -d` oben hat ihn dann ohnehin +# frisch gestartet, und ein frischer Prozess hat den neuen Code schon. +docker compose restart queue scheduler reverb +docker compose exec -T queue-provisioning php artisan queue:restart >/dev/null 2>&1 || true # Und die Terminal-Brücke gleich hinterher, aus demselben Grund wie die beiden # VPN-Dienste unten: sie lebt im Netz-Namensraum des Provisioning-Containers @@ -547,6 +569,35 @@ docker compose restart queue queue-provisioning scheduler reverb # die Tunneladresse; die braucht sie erst, wenn jemand ein Ticket einlöst. docker compose restart terminal +# Die gemerkten UDP-Ströme, wenn der Tunnel-Container neu gebaut wurde. +# +# Der Kernel merkt sich laufende UDP-Ströme samt Ziel. Ein neu gebauter Container +# bekommt eine neue Adresse im Compose-Netz, die Weiterleitung für den +# WireGuard-Port wird neu geschrieben — die gemerkten Einträge zeigen aber weiter +# auf den alten. Und sie verfallen nicht: WireGuard schickt alle 25 Sekunden ein +# Lebenszeichen und hält den kaputten Eintrag damit am Leben. +# +# Genau das hat einen Host nach einem Update dauerhaft draussen gelassen, während +# ein Telefon nach Aus- und Einschalten sofort wieder drin war — ein Client mit +# neuem Quellport bekommt einen frischen Eintrag, ein Host mit festem nicht. +# +# Braucht Root auf dem Wirt; dieses Skript läuft als Dienstbenutzer. Deshalb +# `sudo -n` (fragt nicht nach einem Passwort) und, wenn das nicht darf, eine +# deutliche Zeile statt eines stillen Fehlschlags. +if [[ -n "$hub_before" && "$hub_before" != "$hub_after" ]]; then + wg_port="$(grep -m1 '^WG_HUB_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"'"'"' ' || true)" + wg_port="${wg_port:-51820}" + + if sudo -n conntrack -D -p udp --dport "$wg_port" >/dev/null 2>&1; then + log "Gemerkte UDP-Ströme auf Port $wg_port verworfen — die Tunnel bauen sich neu auf." + else + warn "Der Tunnel-Container wurde neu gebaut. Bestehende WireGuard-Sitzungen" + warn "zeigen jetzt auf den alten Container und kommen von allein NICHT zurück." + warn "Auf dem Wirt einmal ausführen:" + warn " sudo conntrack -D -p udp --dport $wg_port" + fi +fi + # AFTER the hub, always. Both VPN services live in the provisioning container's # network namespace, and a process holds the namespace it started in — so once # the hub is restarted they are listening inside one that no longer exists. diff --git a/tests/Feature/DeploymentRunsAsTheAppUserTest.php b/tests/Feature/DeploymentRunsAsTheAppUserTest.php index 871e1b4..eacb608 100644 --- a/tests/Feature/DeploymentRunsAsTheAppUserTest.php +++ b/tests/Feature/DeploymentRunsAsTheAppUserTest.php @@ -250,3 +250,26 @@ it('haengt den Tunnel nicht an das Leben eines Warteschlangen-Arbeiters', functi // zehn Sekunden und schlägt dann mitten in einem Schritt mit SIGKILL zu. ->and($runner)->toContain('trap stop TERM INT'); }); + +it('startet den Tunnel-Container beim Ausrollen nicht neu', function () { + // `docker compose restart queue-provisioning` baut dessen Netz-Namensraum + // neu auf, und darin lebt wg0. Für ein Update, das nur PHP-Code ändert, + // reisst das jede WireGuard-Sitzung ab — die des Betreibers am Telefon wie + // die jedes Hosts. Genau so ist es passiert. + // + // Seit der Arbeiter dort in einer Schleife läuft, genügt ein Signal: der + // Arbeiter beendet sich nach dem laufenden Auftrag, die Schleife startet ihn + // mit neuem Code neu, der Container bleibt stehen. + $update = File::get(base_path('deploy/update.sh')); + + expect($update)->not->toContain('restart queue queue-provisioning') + ->and($update)->toContain('queue-provisioning php artisan queue:restart'); + + // Und wenn `up -d` ihn doch neu baut (neues Abbild, geänderte + // Konfiguration), müssen die gemerkten UDP-Ströme weg — sonst zeigen sie + // weiter auf den alten Container, und ein Host mit festem Quellport kommt + // nie zurück. + expect($update)->toContain('conntrack -D -p udp --dport') + ->and($update)->toContain('hub_before') + ->and($update)->toContain('hub_after'); +});