Fix: nginx blockierte SSH-Terminal-WebSocket + DHCP-Stop bei Netzwerkaenderung

Auf der frisch installierten Test-VM zeigten sich zwei weitere reale Probleme:

1. "Verbindung testen" im Switch-Modal blieb dauerhaft auf "Getrennt"
   haengen, sobald ueber nginx (Port 80) statt direkt gegen Flask (Port
   5000) zugegriffen wurde. Ursache: etc/nginx/sites-available/poe_manager
   reichte den "Upgrade"/"Connection"-Header fuer /ws/ssh_terminal nicht
   durch -- Flask/Werkzeug lehnte den WebSocket-Handshake dadurch mit
   400 Bad Request ab, statt mit 101 Switching Protocols zu antworten.
   Live reproduziert und verifiziert (curl-Handshake direkt vs. durch
   nginx). Alle bisherigen Terminal-Tests dieser Session liefen ausschliesslich
   direkt gegen Flask, nie durch nginx, daher bis jetzt unentdeckt.
   Fix: eigener location /ws/ Block mit Upgrade-Map (Standard-nginx-Pattern)
   und grosszuegigen Timeouts fuer die lang offene Terminal-Verbindung.

2. Der Kea-DHCP-Server lief nach einer Aenderung der Host-Netzwerkkonfiguration
   unveraendert weiter, obwohl die konfigurierte Range nach einem
   IP-/Subnetzwechsel nicht mehr zum neuen Netz passen muss. Neue Funktion
   _stop_dhcp_service_if_active() stoppt Kea jetzt sowohl beim Anwenden
   als auch beim Auto-Rollback einer Netzwerkaenderung -- bewusst nur
   stoppen statt automatisch neu zu starten, die Range muss der Admin
   aktiv auf der DHCP-Seite pruefen und den Dienst dort bewusst wieder
   starten (gleiche Philosophie wie beim Rest der DHCP-Dienststeuerung).

Beide Fixes live auf der Test-VM verifiziert (WebSocket-Handshake per
curl, Netzwerk-Reapply ohne Fehler im journalctl-Log trotz nicht
installiertem Kea).
This commit is contained in:
2026-08-11 13:29:02 +02:00
parent 2b5d9d1b48
commit 3195df0fd3
3 changed files with 78 additions and 3 deletions
+14
View File
@@ -316,6 +316,13 @@ da eine Änderung nicht die Erreichbarkeit kappt).
erreichbar ist), rollt die App automatisch auf die gesicherte erreichbar ist), rollt die App automatisch auf die gesicherte
Konfiguration zurück. Verhindert, dass ein Tippfehler bei IP/Gateway den Konfiguration zurück. Verhindert, dass ein Tippfehler bei IP/Gateway den
Host von der Web-UI/SSH abschneidet. 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, - Live gegen eine echte Ubuntu-24.04-VM verifiziert (Standard-Installation,
Netz per DHCP über netplan/systemd-networkd bezogen) — sowohl die Netz per DHCP über netplan/systemd-networkd bezogen) — sowohl die
Erkennung/Statusanzeige als auch der komplette Anwenden-Pfad inkl. der Erkennung/Statusanzeige als auch der komplette Anwenden-Pfad inkl. der
@@ -495,6 +502,13 @@ Voraussetzungen:
so konfiguriert) — für einen echten Produktivbetrieb empfiehlt sich so konfiguriert) — für einen echten Produktivbetrieb empfiehlt sich
ohnehin ein WSGI-Server mit WebSocket-Unterstützung (z.B. Gunicorn mit ohnehin ein WSGI-Server mit WebSocket-Unterstützung (z.B. Gunicorn mit
`gevent`- oder `eventlet`-Worker) statt des eingebauten Dev-Servers. `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 - Da `poe_web.service` (wie im Original) als `root` läuft, kann über dieses
Terminal im Prinzip jeder erreichbare Host per SSH kontaktiert werden — Terminal im Prinzip jeder erreichbare Host per SSH kontaktiert werden —
bewusst so gewählt, da Admins über die App ohnehin schon vollen Zugriff auf bewusst so gewählt, da Admins über die App ohnehin schon vollen Zugriff auf
+26
View File
@@ -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 { server {
listen 80; listen 80;
server_name _; 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 / { location / {
proxy_pass http://127.0.0.1:5000; proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host; proxy_set_header Host $host;
+38 -3
View File
@@ -1478,6 +1478,13 @@ def _revert_network_config(token):
except OSError: except OSError:
pass pass
subprocess.run(["netplan", "apply"], timeout=20) 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") log_action_system("settings.network_revert", backup["interface"], "automatisch nach Timeout zurückgerollt")
except Exception as e: except Exception as e:
app.logger.error("Network auto-revert failed: %s", e) app.logger.error("Network auto-revert failed: %s", e)
@@ -1533,11 +1540,22 @@ def settings():
_pending_network_revert[token] = {"timer": timer, "backup": backup} _pending_network_revert[token] = {"timer": timer, "backup": backup}
timer.start() timer.start()
log_action("settings.network_apply", interface, f"Modus {mode}") log_action("settings.network_apply", interface, f"Modus {mode}")
flash( msg = (
f"Netzwerkkonfiguration angewendet. Falls diese Seite jetzt noch erreichbar ist, bitte " 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.", f"unten bestätigen — sonst wird nach {NETWORK_REVERT_SECONDS}s automatisch zurückgerollt."
"success",
) )
# 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: else:
flash(f"Anwenden fehlgeschlagen: {out}", "danger") 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} 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(): def _list_network_interfaces():
"""Echte Netzwerkschnittstellen dieses Hosts (ohne loopback) — Quelle """Echte Netzwerkschnittstellen dieses Hosts (ohne loopback) — Quelle
für die Interface-Auswahl, damit nur tatsächlich vorhandene Interfaces für die Interface-Auswahl, damit nur tatsächlich vorhandene Interfaces