fix(satellite): Selbstdiagnose fuers Netz — warnt bei Docker-/NAT-Netz
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 <noreply@anthropic.com>
This commit is contained in:
@@ -4140,7 +4140,9 @@
|
|||||||
'<button class="btn secondary" onclick="scanSatellite(\'' + escapeHtml(s.id) + '\')" style="padding:3px 8px;font-size:11px;"' +
|
'<button class="btn secondary" onclick="scanSatellite(\'' + escapeHtml(s.id) + '\')" style="padding:3px 8px;font-size:11px;"' +
|
||||||
(scanning ? ' disabled' : '') + '>Geraete scannen</button>' +
|
(scanning ? ' disabled' : '') + '>Geraete scannen</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">kann: ' + escapeHtml(caps) + ' · ' + ctrl + '</div>' +
|
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">kann: ' + escapeHtml(caps) + ' · ' + ctrl +
|
||||||
|
(s.net && s.net.primary_ip ? ' · Netz: ' + escapeHtml(s.net.primary_ip) : '') + '</div>' +
|
||||||
|
(s.net && s.net.warning ? '<div style="color:#FF6E6E;font-size:11px;margin-top:6px;padding:6px;background:#3A1F1F;border-radius:4px;">⚠ ' + escapeHtml(s.net.warning) + '</div>' : '') +
|
||||||
devHtml +
|
devHtml +
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ const satellites = new Map(); // id → {id, location, caps, control, last_seen}
|
|||||||
function satelliteList() {
|
function satelliteList() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
return Array.from(satellites.values()).map(s => ({
|
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,
|
online: (now - (s.last_seen || 0)) < 300000,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -924,7 +924,8 @@ function connectRVS(forcePlain) {
|
|||||||
if (p.id) {
|
if (p.id) {
|
||||||
satellites.set(p.id, {
|
satellites.set(p.id, {
|
||||||
id: p.id, location: p.location || 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();
|
broadcastSatellites();
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-5
@@ -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),
|
- **Hände (Steuerung):** **DIAL-App-Launch** (z.B. YouTube-Video auf dem Fire TV),
|
||||||
**Wake-on-LAN**, generisches **HTTP**. Nur wenn freigeschaltet (siehe Sicherheit).
|
**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
|
```bash
|
||||||
cd satellite
|
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 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
|
`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
|
den Satelliten erreicht). `SATELLITE_LOCATION` ist der Name, über den ARIA das Netz
|
||||||
anspricht („Büro").
|
anspricht („Büro").
|
||||||
|
|
||||||
> **`network_mode: host` ist Pflicht** (schon in der compose gesetzt): nur so sieht
|
**Kontrolle:** im Log/Diagnostic muss `primary_ip` im Ziel-LAN liegen
|
||||||
> der Container die mDNS/SSDP-Broadcasts und die Geräte-IPs des LAN.
|
(`192.168.177.x`) — steht da `192.168.65.x` oder `172.x`, sitzt der Satellit im
|
||||||
|
falschen (Docker-)Netz.
|
||||||
|
|
||||||
## So nutzt ARIA es
|
## So nutzt ARIA es
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
# sieht er die mDNS/SSDP-Broadcasts + Geraete-IPs des LAN nicht (Docker-Bridge
|
# 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
|
# wuerde das isolieren). Damit ist er zugleich als RVS-Client raus ins Internet
|
||||||
# verbunden. Keine Ports zu veroeffentlichen — er ist reiner Client.
|
# 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:
|
services:
|
||||||
satellite:
|
satellite:
|
||||||
build: .
|
build: .
|
||||||
|
|||||||
+52
-1
@@ -102,6 +102,52 @@ if CONTROL_ENABLED:
|
|||||||
CAPABILITIES += CONTROL_ALLOWLIST
|
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 ──────────────────────────────────────────────────────
|
# ─── Discovery ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _discover_mdns(timeout: float) -> list[dict]:
|
def _discover_mdns(timeout: float) -> list[dict]:
|
||||||
@@ -467,6 +513,7 @@ class Satellite:
|
|||||||
"location": SATELLITE_LOCATION,
|
"location": SATELLITE_LOCATION,
|
||||||
"caps": CAPABILITIES,
|
"caps": CAPABILITIES,
|
||||||
"control": CONTROL_ENABLED,
|
"control": CONTROL_ENABLED,
|
||||||
|
"net": NET,
|
||||||
},
|
},
|
||||||
"timestamp": int(time.time() * 1000),
|
"timestamp": int(time.time() * 1000),
|
||||||
})
|
})
|
||||||
@@ -496,7 +543,8 @@ class Satellite:
|
|||||||
await self._send({
|
await self._send({
|
||||||
"type": "sat_devices",
|
"type": "sat_devices",
|
||||||
"payload": {"requestId": req_id, "satellite": SATELLITE_ID,
|
"payload": {"requestId": req_id, "satellite": SATELLITE_ID,
|
||||||
"location": SATELLITE_LOCATION, "devices": devices},
|
"location": SATELLITE_LOCATION, "devices": devices,
|
||||||
|
"net": NET},
|
||||||
"timestamp": int(time.time() * 1000),
|
"timestamp": int(time.time() * 1000),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -566,6 +614,9 @@ class Satellite:
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
logger.info("ARIA Satellit startet — id=%s location=%s control=%s",
|
logger.info("ARIA Satellit startet — id=%s location=%s control=%s",
|
||||||
SATELLITE_ID, SATELLITE_LOCATION, CONTROL_ENABLED)
|
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:
|
try:
|
||||||
asyncio.run(Satellite().run())
|
asyncio.run(Satellite().run())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Reference in New Issue
Block a user