diff --git a/README.md b/README.md index ba73653..e6c28ea 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,13 @@ da eine Änderung nicht die Erreichbarkeit kappt). erreichbar ist), rollt die App automatisch auf die gesicherte Konfiguration zurück. Verhindert, dass ein Tippfehler bei IP/Gateway den Host von der Web-UI/SSH abschneidet. +- **DHCP-Server wird vorsorglich gestoppt**: eine vorher passende Range + (siehe DHCP-Konfiguration oben) kann nach einer IP-/Subnetzänderung + plötzlich zum falschen Netz gehören. Läuft Kea beim Anwenden oder beim + Auto-Rollback, wird der Dienst deshalb gestoppt (nicht automatisch neu + gestartet — die Range muss der Admin aktiv auf der DHCP-Seite prüfen und + den Dienst dort bewusst wieder starten, statt dass die App mit einer + möglicherweise falschen Konfiguration weiterläuft oder blind rät). - Live gegen eine echte Ubuntu-24.04-VM verifiziert (Standard-Installation, Netz per DHCP über netplan/systemd-networkd bezogen) — sowohl die Erkennung/Statusanzeige als auch der komplette Anwenden-Pfad inkl. der @@ -495,6 +502,13 @@ Voraussetzungen: so konfiguriert) — für einen echten Produktivbetrieb empfiehlt sich ohnehin ein WSGI-Server mit WebSocket-Unterstützung (z.B. Gunicorn mit `gevent`- oder `eventlet`-Worker) statt des eingebauten Dev-Servers. +- **nginx muss den WebSocket-Upgrade für `/ws/` durchreichen** (`Upgrade`/ + `Connection`-Header, siehe `etc/nginx/sites-available/poe_manager`) — ohne + das lehnt Flask den Handshake mit `400 Bad Request` ab und das Terminal + bleibt leer auf „Getrennt“ hängen, obwohl derselbe Aufruf direkt gegen + Port 5000 (an nginx vorbei) funktioniert. War auf einer frisch per + `install.sh` aufgesetzten Ubuntu-VM live reproduzierbar, da alle bisherigen + Terminal-Tests ausschließlich direkt gegen Flask liefen, nie durch nginx. - Da `poe_web.service` (wie im Original) als `root` läuft, kann über dieses Terminal im Prinzip jeder erreichbare Host per SSH kontaktiert werden — bewusst so gewählt, da Admins über die App ohnehin schon vollen Zugriff auf diff --git a/etc/nginx/sites-available/poe_manager b/etc/nginx/sites-available/poe_manager index f00ca9f..53bdd0e 100644 --- a/etc/nginx/sites-available/poe_manager +++ b/etc/nginx/sites-available/poe_manager @@ -1,7 +1,33 @@ +# Für den WebSocket-Upgrade des SSH-Terminals (/ws/ssh_terminal) nötig — +# ohne diesen Map-Trick würde nginx den "Connection: Upgrade"-Header nicht +# durchreichen und Flask/Werkzeug lehnt den Handshake dann mit 400 ab +# (live an einer echten Ubuntu-Installation reproduziert: direkt gegen +# Flask:5000 funktionierte der Handshake, durch nginx auf Port 80 nicht). +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + server { listen 80; server_name _; + # Eigener Block vor "location /", da WebSockets lang offene Verbindungen + # sind — die kurzen Standard-Timeouts von nginx (60s) würden ein länger + # inaktives SSH-Terminal sonst hart trennen. + location /ws/ { + proxy_pass http://127.0.0.1:5000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; diff --git a/srv/poe_manager/app.py b/srv/poe_manager/app.py index 3c64d6c..c5101f3 100644 --- a/srv/poe_manager/app.py +++ b/srv/poe_manager/app.py @@ -1478,6 +1478,13 @@ def _revert_network_config(token): except OSError: pass subprocess.run(["netplan", "apply"], timeout=20) + # Das Rollback ändert die Netzwerkkonfiguration erneut — falls + # zwischenzeitlich (z.B. manuell auf der DHCP-Seite) wieder gestartet, + # aus demselben Grund wie beim ursprünglichen Anwenden vorsorglich + # stoppen statt mit einer ggf. nicht mehr passenden Range weiterlaufen + # zu lassen. + if _stop_dhcp_service_if_active(): + log_action_system("settings.network_revert", backup["interface"], "DHCP-Dienst wegen Rollback gestoppt") log_action_system("settings.network_revert", backup["interface"], "automatisch nach Timeout zurückgerollt") except Exception as e: app.logger.error("Network auto-revert failed: %s", e) @@ -1533,11 +1540,22 @@ def settings(): _pending_network_revert[token] = {"timer": timer, "backup": backup} timer.start() log_action("settings.network_apply", interface, f"Modus {mode}") - flash( + msg = ( f"Netzwerkkonfiguration angewendet. Falls diese Seite jetzt noch erreichbar ist, bitte " - f"unten bestätigen — sonst wird nach {NETWORK_REVERT_SECONDS}s automatisch zurückgerollt.", - "success", + f"unten bestätigen — sonst wird nach {NETWORK_REVERT_SECONDS}s automatisch zurückgerollt." ) + # Eine vorher passende DHCP-Range kann nach einer IP-/Subnetzänderung + # plötzlich zum falschen Netz gehören — der Dienst wird deshalb + # vorsorglich gestoppt statt (ggf. falsch) weiterzulaufen. Bewusst + # kein automatischer Neustart: die Range muss der Admin aktiv auf + # der DHCP-Seite prüfen/bestätigen, siehe Kommentar oben am Kea-Block. + if _stop_dhcp_service_if_active(): + log_action("settings.network_apply", interface, "DHCP-Dienst wegen Netzwerkänderung gestoppt") + msg += ( + " Der DHCP-Server wurde dabei gestoppt, da die konfigurierte Range zum neuen Netz nicht " + "mehr passen könnte — bitte auf der DHCP-Seite prüfen und bei Bedarf neu starten." + ) + flash(msg, "success") else: flash(f"Anwenden fehlgeschlagen: {out}", "danger") @@ -1694,6 +1712,23 @@ def _detect_dhcp_status(): return {"installed": installed, "path": kea_path, "active": active, "enabled": enabled, "version": version} +def _stop_dhcp_service_if_active(): + """Stoppt den Kea-DHCP-Dienst, falls er gerade läuft — aufgerufen, wenn + sich die Host-Netzwerkkonfiguration ändert (Systemeinstellungen > + Netzwerkeinstellungen). Eine vorher passende Range/Interface-Bindung + kann nach einer IP-/Subnetzänderung plötzlich zum falschen Netz gehören. + Bewusst nur stoppen, nicht automatisch neu starten — der Admin muss die + Range aktiv auf der DHCP-Seite prüfen/bestätigen, statt dass die App mit + einer möglicherweise falschen Konfiguration weiterläuft oder blind rät.""" + try: + status = subprocess.run(["systemctl", "is-active", DHCP_SERVICE], capture_output=True, text=True, timeout=5) + if status.stdout.strip() != "active": + return False + return subprocess.run(["systemctl", "stop", DHCP_SERVICE], timeout=15).returncode == 0 + except Exception: + return False + + def _list_network_interfaces(): """Echte Netzwerkschnittstellen dieses Hosts (ohne loopback) — Quelle für die Interface-Auswahl, damit nur tatsächlich vorhandene Interfaces