feat(host-agent,satellite): RVS_TLS_FALLBACK wie in den Compute-Bridges

Host-Agent und Satellit hatten keinen TLS-Fallback (nur die vier Compute-Bridges).
Jetzt konsistent: bei TLS-Fehlschlag einmal auf ws:// zurueckfallen, danach wieder
mit RVS_TLS starten (kein Sticky-Fallback, wie bei den Bridges). uri_host/proto
werden pro Versuch aus use_tls berechnet, damit der RVS_SNI-Pfad nur bei wss gilt.

Hilft nur wo der RVS plaintext erreichbar ist; gegen Caddy-TLS bleibt wss. RVS_TLS_
FALLBACK in beiden .env.example dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 17:58:19 +02:00
co-authored by Claude Opus 4.8
parent df5cd3be99
commit 6cb29a28ce
4 changed files with 49 additions and 16 deletions
+1
View File
@@ -6,6 +6,7 @@
RVS_HOST=rvs.example.de
RVS_PORT=443
RVS_TLS=true
RVS_TLS_FALLBACK=true # bei TLS-Fehlschlag einmal auf ws:// zurueckfallen
RVS_TOKEN=
# RVS_SNI: nur noetig, wenn RVS_HOST eine IP ist (Agent im selben Netz wie der
# RVS, direkt auf die interne IP). Dann hier den Zertifikats-/Hostnamen angeben,
+24 -8
View File
@@ -98,6 +98,9 @@ def _default_id() -> str:
RVS_HOST = os.environ.get("RVS_HOST", "")
RVS_PORT = int(os.environ.get("RVS_PORT", "443") or "443")
RVS_TLS = _env_bool("RVS_TLS", True)
# Bei TLS-Fehlschlag einmal auf ws:// zurueckfallen (wie die Compute-Bridges).
# Hilft nur, wenn der RVS plaintext erreichbar ist; gegen Caddy-TLS bleibt wss.
RVS_TLS_FALLBACK = _env_bool("RVS_TLS_FALLBACK", True)
RVS_TOKEN = os.environ.get("RVS_TOKEN", "")
# TLS-Hostname (SNI + Zertifikatspruefung), falls RVS_HOST eine IP ist — z.B. der
# Agent laeuft im selben Netz wie der RVS und verbindet direkt auf die interne IP,
@@ -417,22 +420,26 @@ class HostAgent:
logger.error("RVS_HOST und RVS_TOKEN sind Pflicht (siehe .env.example).")
return
backoff = 1
# RVS_SNI gesetzt (Verbindung auf eine interne IP): die URI nutzt den
# HOSTNAMEN, damit Host-Header + SNI + Cert-Pruefung stimmen (sonst weist
# Caddy die Verbindung ab -> HTTP 200 statt WS-Upgrade). Der TCP-Connect
# wird per host=/port= auf die IP in RVS_HOST umgebogen.
proto = "wss" if RVS_TLS else "ws"
uri_host = RVS_SNI if (RVS_TLS and RVS_SNI) else RVS_HOST
url = f"{proto}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
# use_tls kann bei Fehlschlag einmal auf ws:// fallen (RVS_TLS_FALLBACK),
# danach wieder zurueck auf RVS_TLS (kein Sticky-Fallback).
use_tls = RVS_TLS
tls_fallback_tried = False
connect_kwargs = {"max_size": 16 * 1024 * 1024,
"ping_interval": 20, "ping_timeout": 20}
while True:
proto = "wss" if use_tls else "ws"
# Bei TLS + RVS_SNI: URI nutzt den HOSTNAMEN (Host-Header/SNI/Cert),
# getaddrinfo mappt ihn auf die IP in RVS_HOST. Bei ws:// direkt die IP.
uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{proto}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
fallback = False
try:
logger.info("Verbinde mit RVS %s://%s:%s%s …", proto, uri_host, RVS_PORT,
f" (TCP {RVS_HOST})" if (RVS_TLS and RVS_SNI) else "")
f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, **connect_kwargs) as ws:
self.ws = ws
backoff = 1
tls_fallback_tried = False
await self._hello(log=True)
hb = asyncio.create_task(self._heartbeat())
try:
@@ -442,10 +449,19 @@ class HostAgent:
hb.cancel()
except Exception as exc:
logger.warning("RVS-Verbindung verloren: %s", exc)
if use_tls and RVS_TLS_FALLBACK and not tls_fallback_tried:
logger.info("TLS fehlgeschlagen — Fallback auf ws://")
use_tls = False
tls_fallback_tried = True
fallback = True
finally:
self.ws = None
if fallback:
continue # sofort erneut mit ws://
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30)
use_tls = RVS_TLS # kein Sticky-Fallback
tls_fallback_tried = False
def main() -> None:
+1
View File
@@ -6,6 +6,7 @@
RVS_HOST=rvs.example.de
RVS_PORT=443
RVS_TLS=true
RVS_TLS_FALLBACK=true # bei TLS-Fehlschlag einmal auf ws:// zurueckfallen
RVS_TOKEN=
# RVS_SNI: nur noetig, wenn RVS_HOST eine IP ist (Satellit im selben Netz wie der
# RVS, direkt auf die interne IP). Dann hier den Zertifikats-/Hostnamen angeben,
+23 -8
View File
@@ -106,6 +106,8 @@ def _default_id() -> str:
RVS_HOST = os.environ.get("RVS_HOST", "")
RVS_PORT = int(os.environ.get("RVS_PORT", "443") or "443")
RVS_TLS = _env_bool("RVS_TLS", True)
# Bei TLS-Fehlschlag einmal auf ws:// zurueckfallen (wie die Compute-Bridges).
RVS_TLS_FALLBACK = _env_bool("RVS_TLS_FALLBACK", True)
RVS_TOKEN = os.environ.get("RVS_TOKEN", "")
# TLS-Hostname (SNI + Zertifikatspruefung), falls RVS_HOST eine IP ist — z.B. ein
# Satellit im selben Netz wie der RVS, der direkt auf die interne IP verbindet
@@ -1332,22 +1334,26 @@ class Satellite:
_creds_load()
asyncio.create_task(self._periodic_scan())
backoff = 1
# RVS_SNI gesetzt (Verbindung auf eine interne IP): die URI nutzt den
# HOSTNAMEN, damit Host-Header + SNI + Cert-Pruefung stimmen (sonst weist
# Caddy die Verbindung ab -> HTTP 200 statt WS-Upgrade). Der TCP-Connect
# wird per host=/port= auf die IP in RVS_HOST umgebogen.
proto = "wss" if RVS_TLS else "ws"
uri_host = RVS_SNI if (RVS_TLS and RVS_SNI) else RVS_HOST
url = f"{proto}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
# use_tls kann bei Fehlschlag einmal auf ws:// fallen (RVS_TLS_FALLBACK),
# danach wieder zurueck auf RVS_TLS (kein Sticky-Fallback). Bei TLS+RVS_SNI
# nutzt die URI den HOSTNAMEN (Host-Header/SNI/Cert), getaddrinfo mappt ihn
# auf die IP in RVS_HOST.
use_tls = RVS_TLS
tls_fallback_tried = False
connect_kwargs = {"max_size": 8 * 1024 * 1024,
"ping_interval": 20, "ping_timeout": 20}
while True:
proto = "wss" if use_tls else "ws"
uri_host = RVS_SNI if (use_tls and RVS_SNI) else RVS_HOST
url = f"{proto}://{uri_host}:{RVS_PORT}?token={RVS_TOKEN}"
fallback = False
try:
logger.info("Verbinde mit RVS %s://%s:%s%s …", proto, uri_host, RVS_PORT,
f" (TCP {RVS_HOST})" if (RVS_TLS and RVS_SNI) else "")
f" (TCP {RVS_HOST})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, **connect_kwargs) as ws:
self.ws = ws
backoff = 1
tls_fallback_tried = False
await self._hello(log=True)
hb = asyncio.create_task(self._heartbeat())
try:
@@ -1357,10 +1363,19 @@ class Satellite:
hb.cancel()
except Exception as exc:
logger.warning("RVS-Verbindung verloren: %s", exc)
if use_tls and RVS_TLS_FALLBACK and not tls_fallback_tried:
logger.info("TLS fehlgeschlagen — Fallback auf ws://")
use_tls = False
tls_fallback_tried = True
fallback = True
finally:
self.ws = None
if fallback:
continue
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30)
use_tls = RVS_TLS
tls_fallback_tried = False
def main() -> None: