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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user