docs: CLAUDE.md ausgedünnt (61→28 KB) + Diagnose-Fallen nach gotchas.md

Strukturelle Hygiene (Handoff 2026-09-22): Pflichtlektüre auf 3 Pointer,
Kritische Gotchas auf Kern-Cluster komprimiert, ausführliche Diagnose-Blöcke
(SSDP-Relay, DNS-Hang, networking.service, NTP, HA-http, Nautobot-healthcheck …)
nach docs/gotchas.md verschoben. Tote ALLNET/Omada-Einträge entfernt,
"korrigiert"-Notizen bereinigt (nur die korrigierte Angabe behalten).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 13:05:56 +02:00
parent 2f3d44d4be
commit 52a469185b
2 changed files with 251 additions and 331 deletions

162
docs/gotchas.md Normal file
View File

@@ -0,0 +1,162 @@
# Gotchas — ausführliche Diagnose-Anleitungen (Detail-Doku)
Diese Datei trägt die ausführlichen Diagnose-Blöcke, die in `CLAUDE.md` nur noch als 1-Zeilen-Merkregel
mit Zeiger stehen. Volltext hier, damit der Kern-Index schlank bleibt. Alle Blöcke sind **aktiv**
(querschnittliche Fallen), keine Historie.
---
## SSDP-Relay für die macOS-Sonos-App
LXC **108 `mcast-relay`** auf pve, zweibeinig `10.40.10.21` (VLAN10) + `10.40.40.21` (VLAN40),
`multicast-relay` mit **`--noMDNS`** (mDNS macht der Core). Unit im Repo:
**`mcast-relay/multicast-relay.service`**. **Zwei Beine sind zwingend** — SSDP ist link-lokal mit TTL 1,
ein Relay muss Mitglied beider Broadcast-Domänen sein; Routing hilft nicht.
- 🔴 **`--ssdpUnicastAddr` gehört in ein zweibeiniges Relay NICHT — die Flagge hat den Hinweg 7 Wochen
lang blockiert** (drin 2026-08-18, ausgebaut **2026-08-25**). Sie schreibt in `multicast-relay.py`
**Z. 630** die Quelle *jedes* SSDP-Pakets an `239.255.255.250:1900` auf `<addr>:1901` um (**auch
NOTIFY**); beim Senden greift dann **Z. 691**
`if srcAddr == ssdpUnicastAddr and not onNetwork(srcAddr, tx['addr'], tx['netmask']): continue` —
`10.40.10.21` liegt nicht im VLAN40-Netz, also **verwarf der Relay die M-SEARCH selbst, bevor sie die
Player erreichte**. Der Rückweg gehört stattdessen **geroutet**: `forward TP5: MEDIA->MAIN` ist offen,
die Player antworten unicast an den echten Sucher. (Upstream nennt den Rückweg selbst
`# FIXME: record more than one?` — es gibt nur **einen globalen** Merkslot.)
- 🔎 **Symptom-Merkregel:** *App sucht hunderte Male, sieht NOTIFYs, baut aber nie eine Verbindung auf*
= **Relay, nicht Netz**. Diagnose in einer Zeile — **Richtungsbilanz** im Journal:
`grep -c "via eth1"` (VLAN10→VLAN40) gegen `grep -c "via eth0"`. **0 zu n heißt: der Hinweg fehlt.**
- ⚠️ **Aus dem Relay heraus lässt sich der Relay nicht testen** (Loop-Schutz ignoriert eigene
Quelladressen) — am echten Client testen oder `journalctl -u multicast-relay` mitlesen. **Und am Client
nicht auf Port 1900 messen:** Sonos-App *und* Spotify halten dort je ein `SO_REUSEPORT`-Socket, die
Unicast-Antwort bekommt nur **eines** davon. Richtig ist eine Probe mit **ephemerem Quellport**
(`bind((ifaddr, 0))`), genau wie ein echter Client sucht.
- 🔴 **Zweite, unabhängige Ursache desselben Falls (08-25): die Sonos-App suchte gar nicht.** Nach der
Relay-Reparatur lag weiter nichts an — der Mitschnitt auf der VLAN40-Leitung
(`pct exec 108 -- tcpdump -i eth1 -n -s0 -A "udp and host <client>"`) zeigte: **jedes** SSDP-Paket des
MacBooks kam von **Chrome** (`ST: urn:dial-multiscreen-org:…dial:1`, `USER-AGENT: Chromium`), **kein
einziges** von Sonos. Die App hatte Sockets offen (`*:1900`, `*:6969`, `TCP *:3400`) und redete mit der
Cloud, legte aber nichts auf die Leitung. Ursache war das **Local-Network-Gate von macOS**: es stand auf
**„an" und griff trotzdem nicht**. Fix = Schalter unter *Datenschutz & Sicherheit → Lokales Netzwerk*
**aus, App beenden, wieder an, App starten** (hart: `sudo tccutil reset LocalNetwork com.sonos.macController2`).
⇒ **Merkregel: „die App sucht hunderte Male" ist erst dann ein Befund, wenn der Mitschnitt sagt, WER
sucht.** Im Handoff vom Vormittag waren die gezählten 291 Suchen Chrome + eigene Proben; die Zahl hat
drei Stunden in die falsche Richtung gezeigt.
- 🖥️ **Das MacBook ist dual-homed in VLAN10:** `en5` = **10.40.10.183** (USB-Dock, Default-Route) und
`en0` = **10.40.10.82** (WLAN). Die früher als „zweiter Mac" gelesene `.183` ist **dasselbe Gerät** —
beim Messen immer beide Beine prüfen.
---
## Konstanter 10-Sekunden-Hänger = DNS-Timeout, kein Lastproblem
(Fund 2026-09-01, TP16.3-Randbefund.) `gitea` hing bei ~10 % der Anfragen exakt 10,1 s — quer über
Web-UI, `/api/healthz` **und** den Git-Pfad, also kein Rendering. Ursache: **MariaDB (LXC 302) hatte
`skip-name-resolve` auskommentiert** und machte bei **jeder neuen Verbindung** einen Reverse-Lookup,
während ihr `/etc/resolv.conf` noch auf den toten `10.40.41.254` zeigte (glibc: 5 s × 2 Versuche = 10 s).
Pool-Verbindungen waren schnell ⇒ es wirkte sporadisch.
🔑 **Drei Merkregeln daraus:**
- (a) **immer gegen eine zweite Sache messen, die denselben Weg nimmt** — `vw`/`hass` über dieselbe
Strecke hatten 0 von 30 Hängern, das hat CF, Tunnel, Firewall und Caddy in einem Schritt entlastet.
- (b) **wer an einer DB hängt, sagt die Verbindungstabelle** (`ss -tn | grep 3306`), nicht die
Vermutung — hier hingen **Vaultwarden, Home Assistant und gitea** an derselben MariaDB, und die Annahme
„Vaultwarden nutzt SQLite" war schlicht falsch.
- (c) **ein toter Resolver ist selten allein** — der Sweep über alle LXCs fand **fünf** Container mit
`10.40.41.254`. Sweep:
`for c in $(pct list|awk 'NR>1{print $1}'); do pct exec $c -- grep ^nameserver /etc/resolv.conf; done`
---
## networking.service failed bei funktionierendem Netz = tote `auto`-Stanzas
(Ursache gefunden + behoben 2026-09-01.) In vier LXCs standen in `/etc/network/interfaces` noch
Einträge aus dem **alten VLAN1-Schema** (`eth0` → `10.40.41.x`) und aus früheren Versuchen (`veth16`,
`veth24`, Tippfehler `eth01`) — auf Interfaces, die es im Container gar nicht gibt. `ifup -a` scheitert
daran, das echte `veth20` kommt trotzdem hoch. **Fix:** nur die Stanzas entfernen, deren Interface in
`ip -br link` fehlt (`lo` und die echten behalten), Backup daneben.
🔴 **Abnahme ist ein echter Reboot, nicht `systemctl is-active`** — der Servicestatus sagt nichts
darüber, ob beim nächsten Start etwas hochkommt.
---
## networking.service failed im LXC, ohne dass das Netz fehlt
`networking.service` kann in einem LXC dauerhaft `failed` sein, ohne dass das Netz fehlt — und dann
startet alles mit `After=network-online.target` nach einem Reboot **nicht** (Fund 2026-09-01: `101`,
`102`, `106`, `107`; **influxdb2 kam nach dem Neustart nicht hoch**, Unit `enabled`, **kein Startversuch
im Journal**). Fällt nur auf, wenn man neu startet — die Container liefen 13+ Tage.
**Vor jedem geplanten LXC-Neustart `systemctl --failed` lesen und danach die Ports gegen vorher
vergleichen**, sonst verliert man still einen Dienst.
---
## Messwerkzeug, das die falsche Frage beantwortet
(zweimal in einer Nacht, 2026-09-01.)
- (a) **`nc -z` lieferte falsch-negative** „Port zu" für neun Dienste gleichzeitig — inklusive des
SSH-Ports, über den die Session gerade arbeitete; `curl` bekam von demselben Host **HTTP 200 in 24 ms**.
**Bei „alles gleichzeitig kaputt" zuerst das eigene Werkzeug verdächtigen**, dann das Netz: ein
Python-Socket oder `curl` entscheidet in Sekunden.
- (b) **Ein Firewall-Zähler taugt nicht als Wegbeweis**, wenn der Client die Verbindung offenhält
(`cloudflared`): er zählt nur NEUE Verbindungen und steht still, obwohl alles läuft — richtig ist die
**Verbindungstabelle** (`/ip firewall connection`).
---
## VLAN20-Docker-Hosts: Host-DNS nach dem Umzug prüfen (2026-07-10, TP14.2a)
NAS + pve-LXC zeigten `/etc/resolv.conf` noch auf totes `.41.254` → `docker pull` ghcr scheitert
(`EAI_AGAIN`, aber Egress ok!). Fix = `.20.1`: LXC `pct set 400 --nameserver 10.40.20.1`; **Synology-Falle:
DSM-„Allgemein"-DNS ≠ was `dockerd` liest** — Interface zieht DNS per DHCP → Fix an der
*Netzwerkschnittstelle* „DNS manuell". Laufende Container frieren den `127.0.0.11`-Upstream zum
Erstellzeitpunkt ein (Recreate nötig). Merkregel: `docker-pull-hängt` + `dns.lookup EAI_AGAIN` aber
`TCP zu ghcr-IP:443 ok` = toter Resolver, nicht Egress.
---
## Zeitquelle ist der Core (NTP)
`/system ntp server`, seit 2026-08-18, ausgeliefert per **DHCP-Option 42** (`ntp-server=` in jedem der
7 Netze) und drift-überwacht (ComplianceRule `system-ntp`). Vorher lief **nirgends** eine
Synchronisation: die anderen drei Geräte zeigten auf `10.40.99.1`, wo kein Server lief, und der Core
selbst hatte gar keinen NTP-Client — ap2 war dadurch **17 Stunden** versetzt.
🔴 **Zwei Fallen dabei:**
- (a) ein NTP-Server mit `use-local-clock=no` und unsynchronisiertem Client liefert **Stratum 16**, das
jeder Client verwirft — Server einschalten allein reicht nicht.
- (b) wird der Server eingeschaltet, *bevor* sein Client synchron ist, hält er diesen Zustand fest →
**`enabled=no`/`=yes` nach der Synchronisation**.
Merkregel: bei widersprüchlichen Zeitstempeln zwischen Geräten zuerst `/system ntp client get status`
lesen, und zum Eingrenzen einen Client testweise auf eine öffentliche Quelle zeigen lassen.
---
## Home Assistant: der `http:`-Block in `configuration.yaml` ist WIRKUNGSLOS
(Fund 2026-08-26, TP31.) HA hat die http-Config nach **`.storage/http`** migriert
(`"yaml_migration_done": true`); seither wird die YAML kommentarlos ignoriert — selbst ein ungueltiger
Schluessel darin erzeugt **keinen** Config-Fehler.
🔴 **Die Logzeile fuehrt aktiv in die Irre:** `Received X-Forwarded-For header from an untrusted proxy
<ip>` faellt laut Quelltext nur bei **aktivem** `use_x_forwarded_for` — man haelt die Option fuer wirksam
und die Liste fuer falsch und editiert die falsche Datei. Richtig ist der WS-Zweischritt
**`http/config/configure`** (setzt `pending`, startet HA neu) → binnen **5 Minuten**
**`http/config/promote`**, sonst Auto-Revert auf `stable`.
Merkregel zur Eingrenzung: ein Testskript in `<config>/python_scripts/` + `python_script.reload` —
taucht der Service auf, liest HA das Verzeichnis, und die Frage ist nicht mehr *welche Datei*, sondern
*warum ein gelesener Block nicht wirkt*.
---
## Nautobot auf der DS923 braucht einen abgeschalteten Image-Healthcheck
Der eingebackene Check ruft alle 10 s `nautobot-server health_check` = einen vollen Django-Start; unter
Last dauert der >2 min, die Aufrufe **stapeln sich unbegrenzt** → Speicher alle → `fork()` mit ENOMEM →
DSM räumt den ganzen Stack ab (am 2026-08-17 zweimal passiert, Load bis 73 bei 4 Kernen). Im Stack stehen
deshalb `healthcheck: disable: true` auf worker+scheduler, `--concurrency=2` und `mem_limit`.
**`cpus:` geht auf Synology nicht** (Kernel ohne CFS-Quota, der Stack-Deploy schlägt sonst fehl).