Der Tunnel haengt nicht mehr an einem huebschen Namen und nicht an einem Arbeiter
tests / pest (push) Failing after 9m49s Details
tests / assets (push) Successful in 26s Details
tests / release (push) Has been skipped Details

Zwei Ursachen, eine davon habe ich mit v1.4.2 selbst gelegt.

1. DER NETZ-ALIAS. Damit in docker/nginx/default.conf `terminal:8082` stehen
   konnte statt des Namens des Nachbarcontainers, bekam queue-provisioning einen
   Eintrag unter `networks: aliases:`. Das ist Teil seiner NETZKONFIGURATION —
   und Compose baut einen Container neu, sobald die sich aendert. Neu gebaut
   heisst neue Adresse im Compose-Netz, heisst neu geschriebene
   Weiterleitungsregeln fuer den veroeffentlichten UDP-Port 51820. Auf dem liegt
   jede bestehende WireGuard-Sitzung.

   Der Preis fuer einen sprechenderen Namen in einer Konfigurationszeile war
   also ein Abriss saemtlicher Tunnel beim Ausrollen — der des Betreibers am
   Telefon wie der jedes Hosts. Alias entfernt, nginx spricht die Bruecke unter
   `queue-provisioning:8082` an. Nachgemessen: danach laesst `docker compose
   up -d` den Tunnel-Container unangetastet, und nginx erreicht die Bruecke
   weiterhin (426 statt 502).

2. DER ARBEITER ALS PROZESS 1. wg0 stand im Namensraum von queue-provisioning,
   und dessen Prozess 1 war `exec php artisan queue:work provisioning`. Endete
   der Arbeiter, endete der Container — und mit ihm der Namensraum und jede
   Sitzung darin. Ein Arbeiter endet oefter, als man denkt: `--timeout=2100`
   beendet ihn bei einem langen Provisionierungs-Schritt, ein fataler Fehler
   beendet ihn, ein Speicherlimit beendet ihn, `queue:restart` beendet ihn
   absichtlich. Aus jedem dieser vier Faelle wurde bisher "das VPN ist
   unzuverlaessig".

   Der Rumpf liegt jetzt in docker/provisioning-worker.sh: wg0 einmal hochziehen,
   danach den Arbeiter in einer Schleife halten, SIGTERM sauber weiterreichen.
   Nachgemessen: Arbeiter getoetet -> Container hat NULL Neustarts, wg0 steht
   weiter, Arbeiter ist von allein wieder da.

Beide Male bleibt der Netz-Namensraum stehen. Damit verschwindet nebenbei auch
der Grund, aus dem update.sh die Bruecke hinterher neu starten musste — die
Zeile bleibt trotzdem, sie kostet nichts und traegt den Fall, dass der Container
doch einmal neu gebaut wird.

NICHT GETAGGT, mit Absicht: diese Aenderung fasst genau das an, was den Zugang
des Betreibers traegt. Sie gehoert ausgerollt, wenn jemand davorsitzt und die
Konsole des Anbieters als Rueckfahrkarte hat — nicht unbeaufsichtigt vom
Update-Agenten, waehrend der Betreiber unterwegs ist.

2508 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
nexxo 2026-08-02 22:58:28 +02:00
parent 7224825db1
commit 2b79a6a2ba
4 changed files with 121 additions and 20 deletions

View File

@ -91,7 +91,11 @@ services:
# renders no mail, so it is not the one that was writing compiled views —
# and update.sh normalises ownership at the end of every deployment, which
# heals it if it ever does.
command: sh -c 'wg-quick up wg0 2>/dev/null || true; exec php artisan queue:work provisioning --queue=provisioning --tries=1 --timeout=2100 --sleep=3'
# Der Rumpf steht in einer Datei, nicht hier: er haelt den Arbeiter in einer
# Schleife, damit ein Zeitueberlauf oder ein fataler Fehler nicht mehr den
# ganzen Container — und mit ihm wg0 und jede WireGuard-Sitzung — mitnimmt.
# Die Begruendung steht ausfuehrlich in der Datei.
command: sh /var/www/html/docker/provisioning-worker.sh
cap_add:
- NET_ADMIN
devices:
@ -100,21 +104,6 @@ services:
- net.ipv4.ip_forward=1
ports:
- "${WG_HUB_PORT:-51820}:51820/udp"
networks:
default:
aliases:
# Der Name, unter dem nginx die Terminal-Bruecke erreicht. Sie teilt
# sich den Netz-Namensraum DIESES Containers (`network_mode` unten bei
# `terminal`) und hat deshalb keine eigene Adresse im Compose-Netz und
# keinen eigenen DNS-Eintrag — ein Container ohne eigenes Netz bekommt
# keinen. Der Alias gibt der Adresse den Namen des Dienstes, der dort
# antwortet, statt in docker/nginx/default.conf den Namen des
# Provisionierungs-Workers stehen zu haben.
#
# Faellt `network_mode` bei `terminal` je weg, muss dieser Alias mit
# weg: sonst zeigen zwei DNS-Eintraege auf denselben Namen und nur
# einer davon hat einen Zuhoerer.
- terminal
volumes:
- .:/var/www/html
- wireguard:/etc/wireguard

View File

@ -77,7 +77,20 @@ server {
# eingebauter DNS) wird daraus ein 502 an genau dieser Stelle, waehrend
# alles andere weiterlaeuft.
resolver 127.0.0.11 valid=10s ipv6=off;
set $terminal_upstream terminal:8082;
# Der Name des Provisionierungs-Workers, nicht ein huebscherer Alias.
#
# Hier stand `terminal:8082`, und dieser Alias war ein Eintrag unter
# `networks:` von queue-provisioning. Genau das hat den Tunnel-Container
# beim ersten Ausrollen NEU BAUEN lassen — Compose baut einen Container
# neu, sobald sich seine Netzkonfiguration aendert. Ein neu gebauter
# Container bekommt eine neue Adresse im Compose-Netz, und damit werden
# die veroeffentlichten Regeln fuer UDP 51820 neu geschrieben, auf denen
# JEDER bestehende WireGuard-Fluss liegt.
#
# Der Preis war also: ein Wort in dieser Zeile gegen einen Abriss aller
# Tunnel. Die Bruecke teilt sich ohnehin den Namensraum, `:8082` ist ihr
# Port darin — der Name daneben darf ruhig der des Nachbarn sein.
set $terminal_upstream queue-provisioning:8082;
proxy_pass http://$terminal_upstream;
proxy_http_version 1.1;

51
docker/provisioning-worker.sh Executable file
View File

@ -0,0 +1,51 @@
#!/bin/sh
# Der Tunnel und der Arbeiter, die sich einen Container teilen — aber nicht
# mehr ein Schicksal.
#
# Bis hierher war `php artisan queue:work` der Prozess 1 dieses Containers.
# Damit hing wg0 an seinem Leben: jedes Mal, wenn der Arbeiter endete, endete
# der Container, und mit ihm der Netz-Namensraum, in dem der Tunnel steht.
#
# Ein Arbeiter endet oefter, als man denkt. `--timeout=2100` beendet ihn, wenn
# ein Provisionierungs-Schritt zu lange braucht. Ein fataler Fehler beendet ihn.
# Ein ueberschrittenes Speicherlimit beendet ihn. `queue:restart` beendet ihn
# absichtlich. In allen vier Faellen fielen bisher saemtliche WireGuard-Sitzungen
# mit — die des Betreibers am Telefon genauso wie die jedes Hosts. Von aussen sah
# das aus wie „das VPN ist unzuverlaessig", und die Ursache stand in einer Zeile
# docker-compose.yml, die von Warteschlangen handelt.
#
# Deshalb: wg0 einmal hochziehen, und danach den Arbeiter in einer Schleife
# halten. Faellt er, kommt er wieder — der Tunnel merkt nichts davon.
#
# Der Container endet dann nur noch, wenn Docker ihn beendet. Das ist Absicht:
# ein Arbeiter, der nicht startet, ist ein Fall fuers Protokoll, kein Grund,
# jedem Host die Verbindung abzuschneiden.
set -u
wg-quick up wg0 2>/dev/null || true
# SIGTERM sauber weiterreichen: sonst wartet `docker compose stop` zehn Sekunden
# und schlaegt dann mit SIGKILL zu — mitten in einem Provisionierungs-Schritt.
child=''
stop() {
[ -n "$child" ] && kill -TERM "$child" 2>/dev/null
exit 0
}
trap stop TERM INT
while true; do
php artisan queue:work provisioning \
--queue=provisioning \
--tries=1 \
--timeout=2100 \
--sleep=3 &
child=$!
wait "$child"
code=$?
# Ins Protokoll, nicht ins Nichts: ein Arbeiter, der staendig neu startet,
# ist ein eigenes Problem — es soll nur nicht mehr das Problem des Tunnels
# sein.
echo "[provisioning] Arbeiter beendet (Code ${code}) — wg0 bleibt oben, Neustart in 2 s" >&2
sleep 2
done

View File

@ -197,8 +197,56 @@ it('laesst die Terminal-Bruecke dort stehen, wo ein Host ueberhaupt erreichbar i
// — Docker lehnt das Compose-File dann komplett ab.
->and($block)->not->toContain('ports:');
// Und der Name, unter dem nginx ihn anspricht, muss der Alias des
// Namensraum-Eigentümers sein: ein Container ohne eigenes Netz hat keinen
// Und nginx muss ihn unter dem Namen des Namensraum-Eigentümers ansprechen,
// nicht unter einem eigenen: ein Container ohne eigenes Netz hat keinen
// eigenen DNS-Eintrag.
expect($compose)->toContain(' - terminal');
//
// Hier stand einmal ein Netz-Alias `terminal` auf queue-provisioning, damit
// in der nginx-Konfiguration ein sprechender Name steht. Der Preis dafür war
// hoch und fiel erst im Betrieb auf: ein Eintrag unter `networks:` ist Teil
// der Netzkonfiguration eines Dienstes, und Compose BAUT einen Container NEU,
// sobald die sich ändert. Neu gebaut heißt neue Adresse im Compose-Netz,
// heißt neu geschriebene Weiterleitungsregeln für den veröffentlichten
// UDP-Port 51820 — auf dem jede bestehende WireGuard-Sitzung liegt. Ein
// hübscherer Name in einer Konfigurationszeile hat sämtliche Tunnel gekostet.
//
// Deshalb: kein Alias. Der Dienstname des Nachbarn tut es auch.
expect($compose)->not->toContain('aliases:');
$hub = preg_split('/^ queue-provisioning:$/m', $compose)[1] ?? '';
$hub = preg_split('/^ \S/m', $hub)[0] ?? '';
expect($hub)->not->toContain('networks:');
expect(File::get(base_path('docker/nginx/default.conf')))
->toContain('set $terminal_upstream queue-provisioning:8082;');
});
it('haengt den Tunnel nicht an das Leben eines Warteschlangen-Arbeiters', function () {
// Der Fall, der den Betreiber unterwegs erwischt hat: wg0 stand im
// Namensraum von queue-provisioning, und dessen Prozess 1 war
// `php artisan queue:work`. Endete der Arbeiter — Zeitüberlauf nach 2100
// Sekunden, fataler Fehler, Speicherlimit, `queue:restart` —, endete der
// Container, und mit ihm fielen ALLE WireGuard-Sitzungen: die des Betreibers
// am Telefon wie die jedes Hosts. Von außen las sich das als „das VPN ist
// unzuverlässig"; die Ursache stand in einer Zeile, die von Warteschlangen
// handelt.
$compose = File::get(base_path('docker-compose.yml'));
$hub = preg_split('/^ queue-provisioning:$/m', $compose)[1] ?? '';
$hub = preg_split('/^ \S/m', $hub)[0] ?? '';
// Kein `exec` auf den Arbeiter — das ist genau das Ersetzen von Prozess 1,
// das den Container am Arbeiter sterben ließ.
expect($hub)->not->toContain('exec php artisan queue:work')
->and($hub)->toContain('docker/provisioning-worker.sh');
$runner = File::get(base_path('docker/provisioning-worker.sh'));
expect($runner)->toContain('wg-quick up wg0')
// Die Schleife ist der ganze Punkt: fällt der Arbeiter, kommt er wieder,
// ohne dass der Namensraum stirbt.
->and($runner)->toContain('while true')
// Und SIGTERM wird weitergereicht, sonst wartet `docker compose stop`
// zehn Sekunden und schlägt dann mitten in einem Schritt mit SIGKILL zu.
->and($runner)->toContain('trap stop TERM INT');
});