From 57e13800e0fa35fa5ae1e946ca5acadff176ec46 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 20 Jul 2026 19:46:34 +0200 Subject: [PATCH] =?UTF-8?q?fix(satellite):=20Selbstdiagnose=20fuers=20Netz?= =?UTF-8?q?=20=E2=80=94=20warnt=20bei=20Docker-/NAT-Netz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: Satellit fand nur Docker-Container (192.168.65.x / 172.18.x) statt der echten LAN-Geraete. Ursache ist kein Bug, sondern das Deployment-Netz: auf Docker Desktop (Mac/Windows) ist network_mode:host das Docker-VM-NAT, nicht das echte LAN — mDNS/SSDP erreichen die realen Geraete nicht. - satellite.py: _net_context() ermittelt primary_ip + alle IPs und WARNT, wenn der Satellit in einem Docker-/NAT-Netz laeuft (192.168.65.x oder 172.16-31.x). Netz- Info wird in sat_hello + sat_devices mitgeschickt und beim Start geloggt. - Diagnostic: zeigt Netz (primary_ip) pro Satellit + eine rote ⚠-Box mit der Warnung, wenn er im falschen Netz sitzt. - README/compose: klar dokumentiert, dass der Satellit im ECHTEN Ziel-LAN laufen muss (Linux Docker Engine ODER nativ python satellite.py); Docker-Desktop-Falle. py/node clean. Co-Authored-By: Claude Opus 4.8 --- diagnostic/index.html | 4 ++- diagnostic/server.js | 5 ++-- satellite/README.md | 33 ++++++++++++++++++---- satellite/docker-compose.yml | 5 ++++ satellite/satellite.py | 53 +++++++++++++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 9c353cd..56cec18 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -4140,7 +4140,9 @@ '' + '' + - '
kann: ' + escapeHtml(caps) + ' · ' + ctrl + '
' + + '
kann: ' + escapeHtml(caps) + ' · ' + ctrl + + (s.net && s.net.primary_ip ? ' · Netz: ' + escapeHtml(s.net.primary_ip) : '') + '
' + + (s.net && s.net.warning ? '
⚠ ' + escapeHtml(s.net.warning) + '
' : '') + devHtml + ''; }).join(''); diff --git a/diagnostic/server.js b/diagnostic/server.js index 9a2b098..92b0d9f 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -481,7 +481,7 @@ const satellites = new Map(); // id → {id, location, caps, control, last_seen} function satelliteList() { const now = Date.now(); return Array.from(satellites.values()).map(s => ({ - id: s.id, location: s.location, caps: s.caps, control: s.control, + id: s.id, location: s.location, caps: s.caps, control: s.control, net: s.net || null, online: (now - (s.last_seen || 0)) < 300000, })); } @@ -924,7 +924,8 @@ function connectRVS(forcePlain) { if (p.id) { satellites.set(p.id, { id: p.id, location: p.location || p.id, - caps: p.caps || [], control: !!p.control, last_seen: Date.now(), + caps: p.caps || [], control: !!p.control, net: p.net || null, + last_seen: Date.now(), }); broadcastSatellites(); } diff --git a/satellite/README.md b/satellite/README.md index 026310c..df5f8ef 100644 --- a/satellite/README.md +++ b/satellite/README.md @@ -10,21 +10,44 @@ Ferienwohnung …). Er verbindet sich als RVS-Client in Stefans Raum und gibt AR - **Hände (Steuerung):** **DIAL-App-Launch** (z.B. YouTube-Video auf dem Fire TV), **Wake-on-LAN**, generisches **HTTP**. Nur wenn freigeschaltet (siehe Sicherheit). -## Deploy +## ⚠️ Wichtig: der Satellit MUSS im echten Ziel-LAN laufen +Discovery (mDNS/SSDP-Multicast + ARP) funktioniert **nur**, wenn der Prozess +tatsächlich im selben LAN wie die Geräte hängt — z.B. `192.168.177.0/24`, wo der +Fire TV steht. + +**Docker Desktop (Mac/Windows) geht NICHT.** Dort ist `network_mode: host` das Netz +der Docker-Linux-VM (NAT, `192.168.65.x` / `172.x`), **nicht** dein echtes LAN. +Der Satellit sieht dann nur Docker-Container statt der echten Geräte. (Der Satellit +erkennt das selbst und meldet eine ⚠-Warnung im Diagnostic + Log.) + +Richtig deployen — zwei Wege: + +**A) Linux-Box im Ziel-LAN mit Docker Engine** (empfohlen, z.B. Raspberry Pi / NUC im Büro): ```bash cd satellite -cp .env.example .env # RVS-Zugang + SATELLITE_LOCATION eintragen +cp .env.example .env # RVS-Zugang + SATELLITE_LOCATION docker compose up -d --build -docker compose logs -f # "sat_hello gesendet" + "[scan] N Geraete" +docker compose logs -f # "Netz: primary_ip=192.168.177.x" + "[scan] N Geraete" +``` +`network_mode: host` (schon gesetzt) gibt hier echtes LAN + Multicast. + +**B) Nativ als Python-Prozess** (für Mac/Windows-Test oder ohne Docker) — läuft direkt +auf einer Maschine im Ziel-LAN: +```bash +cd satellite +pip install -r requirements.txt +export RVS_HOST=... RVS_TOKEN=... SATELLITE_LOCATION="Wohnung" CONTROL_ENABLED=true +python satellite.py ``` `RVS_HOST/PORT/TLS/TOKEN` **identisch** zum Haupt-Stack (gleicher Raum, damit ARIA den Satelliten erreicht). `SATELLITE_LOCATION` ist der Name, über den ARIA das Netz anspricht („Büro"). -> **`network_mode: host` ist Pflicht** (schon in der compose gesetzt): nur so sieht -> der Container die mDNS/SSDP-Broadcasts und die Geräte-IPs des LAN. +**Kontrolle:** im Log/Diagnostic muss `primary_ip` im Ziel-LAN liegen +(`192.168.177.x`) — steht da `192.168.65.x` oder `172.x`, sitzt der Satellit im +falschen (Docker-)Netz. ## So nutzt ARIA es diff --git a/satellite/docker-compose.yml b/satellite/docker-compose.yml index aaea78c..b04da7d 100644 --- a/satellite/docker-compose.yml +++ b/satellite/docker-compose.yml @@ -9,6 +9,11 @@ # sieht er die mDNS/SSDP-Broadcasts + Geraete-IPs des LAN nicht (Docker-Bridge # wuerde das isolieren). Damit ist er zugleich als RVS-Client raus ins Internet # verbunden. Keine Ports zu veroeffentlichen — er ist reiner Client. +# +# ⚠ NUR auf LINUX Docker Engine, und der Host muss physisch im Ziel-LAN haengen! +# Docker Desktop (Mac/Windows) gibt hier NUR das Docker-VM-Netz (192.168.65.x / +# 172.x), NICHT dein echtes LAN → Discovery findet dann nur Docker-Container. +# Fuer Mac/Windows: satellite.py nativ starten (siehe README, Weg B). services: satellite: build: . diff --git a/satellite/satellite.py b/satellite/satellite.py index 690dc0e..de9955e 100644 --- a/satellite/satellite.py +++ b/satellite/satellite.py @@ -102,6 +102,52 @@ if CONTROL_ENABLED: CAPABILITIES += CONTROL_ALLOWLIST +# ─── Netz-Kontext / Selbstdiagnose ────────────────────────────────── + +def _in_docker_bridge(ip: str) -> bool: + # Docker-Default-Bridge-Range 172.16.0.0/12 + try: + a, b = ip.split(".")[:2] + return a == "172" and 16 <= int(b) <= 31 + except Exception: + return False + + +def _net_context() -> dict: + """Ermittelt in welchem Netz der Satellit LAeUFT — und warnt, wenn das ein + Docker-/NAT-Netz ist (dann erreicht Discovery das echte LAN nicht).""" + ips: list[str] = [] + primary = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(1) + s.connect(("8.8.8.8", 80)) + primary = s.getsockname()[0] + s.close() + except Exception: + pass + try: + for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET): + ip = info[4][0] + if ip and not ip.startswith("127.") and ip not in ips: + ips.append(ip) + except Exception: + pass + if primary and primary not in ips: + ips.insert(0, primary) + p = primary or (ips[0] if ips else "") + warning = "" + if p.startswith("192.168.65.") or _in_docker_bridge(p): + warning = (f"Satellit laeuft in einem Docker-/NAT-Netz ({p}), NICHT im echten LAN. " + "mDNS/SSDP erreichen die realen Geraete so nicht. Auf Docker Desktop " + "(Mac/Windows) geht LAN-Discovery nicht — den Satelliten NATIV (python " + "satellite.py) oder auf einem Linux-Host im Ziel-LAN betreiben.") + return {"primary_ip": p, "ips": ips, "warning": warning} + + +NET = _net_context() + + # ─── Discovery ────────────────────────────────────────────────────── def _discover_mdns(timeout: float) -> list[dict]: @@ -467,6 +513,7 @@ class Satellite: "location": SATELLITE_LOCATION, "caps": CAPABILITIES, "control": CONTROL_ENABLED, + "net": NET, }, "timestamp": int(time.time() * 1000), }) @@ -496,7 +543,8 @@ class Satellite: await self._send({ "type": "sat_devices", "payload": {"requestId": req_id, "satellite": SATELLITE_ID, - "location": SATELLITE_LOCATION, "devices": devices}, + "location": SATELLITE_LOCATION, "devices": devices, + "net": NET}, "timestamp": int(time.time() * 1000), }) @@ -566,6 +614,9 @@ class Satellite: def main() -> None: logger.info("ARIA Satellit startet — id=%s location=%s control=%s", SATELLITE_ID, SATELLITE_LOCATION, CONTROL_ENABLED) + logger.info("Netz: primary_ip=%s alle=%s", NET.get("primary_ip"), NET.get("ips")) + if NET.get("warning"): + logger.warning("⚠ %s", NET["warning"]) try: asyncio.run(Satellite().run()) except KeyboardInterrupt: