From 4b3f1bb4ff77b4b45f656cd38fb4c7f5fb03f24b Mon Sep 17 00:00:00 2001 From: nexxo Date: Mon, 3 Aug 2026 07:09:12 +0200 Subject: [PATCH] Ein Update fasst den Tunnel nicht mehr an MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Betreiber hat es dreimal hintereinander erlebt: Update gefahren, VPN weg. Die Ursache dafuer, dass es JEDES Mal passierte, stand hier: docker compose restart queue queue-provisioning scheduler reverb In dessen Netz-Namensraum lebt wg0. `restart` baut den Namensraum neu auf, also riss jede WireGuard-Sitzung 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 (v1.4.4), geht es billiger: `queue:restart` setzt ein Signal, der Arbeiter beendet sich nach dem laufenden Auftrag, die Schleife startet ihn mit dem neuen Code neu. Der Container bleibt stehen, wg0 bleibt oben, niemand merkt etwas. Und fuer den Fall, dass `up -d` ihn doch neu baut (neues Abbild, geaenderte Konfiguration): das Skript merkt sich die Container-ID vorher und nachher. Hat sie sich geaendert, raeumt es die gemerkten UDP-Stroeme selbst weg — `sudo -n conntrack -D -p udp --dport `, und wenn es das nicht darf, steht der Befehl als Warnung im Protokoll statt gar nichts. Dieser Handgriff war bisher muendliche Ueberlieferung. Ohne ihn zeigen die gemerkten Stroeme weiter auf den alten Container, und sie verfallen nicht: WireGuard schickt alle 25 Sekunden ein Lebenszeichen und haelt den kaputten Eintrag am Leben. Genau deshalb kam ein Telefon nach Aus- und Einschalten sofort zurueck (neuer Quellport) und ein Host mit festem Port ueberhaupt nicht. Ein Test haelt beides fest: queue-provisioning darf nicht in der Neustart-Liste stehen, und der conntrack-Griff muss im Skript bleiben. 2509 Tests gruen. Co-Authored-By: Claude Opus 5 --- deploy/update.sh | 53 ++++++++++++++++++- .../DeploymentRunsAsTheAppUserTest.php | 23 ++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) 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'); +});