From 52a469185b612e9b95c8eeb51dac5ab9c38febf9 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Tue, 22 Sep 2026 13:05:56 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20CLAUDE.md=20ausged=C3=BCnnt=20(61?= =?UTF-8?q?=E2=86=9228=20KB)=20+=20Diagnose-Fallen=20nach=20gotchas.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 420 ++++++++++-------------------------------------- docs/gotchas.md | 162 +++++++++++++++++++ 2 files changed, 251 insertions(+), 331 deletions(-) create mode 100644 docs/gotchas.md diff --git a/CLAUDE.md b/CLAUDE.md index fde5ca7..7ecadfe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,54 +1,16 @@ # BF-Netzwerk — Projekt-Kontext (auto-read) **Standort BF:** VLAN-segmentiertes Heimnetz auf MikroTik/RouterOS. Ziel: CAKE/QoS gegen Bufferbloat, -DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der Standalone-Entscheid von -2026-07-22 ist revidiert), Firewall Least-Privilege (**TP5 deny-by-default scharf seit 2026-08-18**), Multi-Site VPN. +DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17), Firewall Least-Privilege +(**TP5 deny-by-default scharf seit 2026-08-18**), Multi-Site VPN. ## Pflichtlektüre pro Sitzung (Reihenfolge) -0. 🟢 **TP45 (Gitea #71) — Schicht C (DNS/FQDN) ist durch (2026-09-22, Commit `6d1d56e`), vorgezogen.** - Die internen `*.bf.emsenhuber.net` lösen wieder auf (der Core hält jetzt **27** Statics statt 10), - `ds-bf` ist öffentlich heil (läuft über Caddy), AGH macht **nur noch Filtering** (Upstream = Core). - Der *sichtbare* Schaden ist damit weg — der Ursachen-Kontext steht unter `### TP45` (AGH wertete - Rewrites im Filter-Pfad aus → unfiltrierte Clients fielen auf den Core zurück). - **Offen sind die IP-Schichten, in dieser Reihenfolge:** **A** VLAN20 ins TP32-Schema (Nautobot - `.30–.34` → `.154–.158` zuerst, dann AGH `.36/.37` → `.30/.31` — ⚠️ AGH ist **doppelt adressiert**, - beide Adressen live) → **B** VLAN30-Pool-Grenze (braucht den DHCP-Reservierungs-Renderer) → - **E** nas/pve/macmini umziehen (eigene Session) → **C4** (TP34: FQDN als Daten). - **`### TP46`** = CF-/Tunnel-SSoT + Recovery (eigene Baustelle). -1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Für den **Detail/Kontext** eines TP dann den `### TPnn`-Prosa-Abschnitt in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`** lesen. *(Die frühere „Quick-Index-Block, 1 Read reicht"-Zeile ist überholt — Index/Status stehen nicht mehr in der Datei.)* +1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Detail/Kontext eines TP → `### TPnn` in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`**. 2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat. -3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**. -4. ✅ **TP16.3 §5 durch (2026-09-01):** [`docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md`](docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md) - — alle vier oeffentlichen Namen laufen ueber `bf-public`/Caddy; Alttunnel bleiben als Rueckfallebene - (§6 offen). Enthaelt den 10-Sekunden-DNS-Fund und drei Messfallen. -5. ✅ **WLAN/Fund E — GESCHLOSSEN 2026-09-02: 7.24.1 war die Ursache, nicht `ft-over-ds`:** - [`docs/handoff-20260902-tp25-fund-e-messung.md`](docs/handoff-20260902-tp25-fund-e-messung.md) - — die Abschlussmessung ergab am 09-02 nur **4 `reauthenticating` in 8,6 h** (alle iPhone `F2:62`, - isolierte Einzel, 0 Ausfaelle), Verlauf **218 → 40 → 4**; Vorbedingung geprueft (Core-Uptime 1d18h, - `caps joined=0`). Hebel 1 (`ft-over-ds=no`) war widerlegt (6,4 h allein → 124× + 2 Ausfaelle) und ist - am 09-01 zurueckgebaut; Hebel 2–4 entfallen, **kein Geraete-Write** (Soll stand: `ft=yes ft-over-ds=yes`, - **Compliance 140/140**). **Am 2026-09-04 hat Christoph `ft=no` auf `sec-main` als Dauerzustand - entschieden** (Hebel 2 doch gezogen: Template + bf-golden-config + Nautobot-Sync, `wifi-security` gruen). - ⚠️ **Rueckgenommen am 2026-09-14 (Terrasse-In-situ-Session): `ft=yes ft-over-ds=yes` zurueck** — mit - `ft=no` war jeder Roam eine Voll-Reassoziation, darin starben 2 Anrufe; Rueckkehrstand = Fund-E-Known-Good - von 09-02 (ruhig auf 7.24.1). Weg: Template + `push-gc-template.py` + `gc-configplan.py - --refresh-compliance` + `gc-manual-plan.py --deploy` (Plan `b1135f3e`, Completed) → **140/140, 0 Drifts**; - CAP-Abnahme: **keine** Inline-Overrides auf ap1/ap2/ap3. Commands: - `docs/transaktionen/TP25-ft-yes-secmain.commands.rsc`, s. Handoff 2026-09-14. - 🔴 **Nachtrag 09-04: `ft=no` war NICHT uniform.** ap3 trug einen Werks-Inline `security.ft=yes` auf - `wifi1/wifi2` (Bring-up unset `!ft-over-ds`, aber nicht `!ft`; Abnahme zählt nur benannte - security-Objekte → Inline-Wert unsichtbar). Gemischtes FT-ESS → Strong-Signal-Abbrüche (−43/−45 dBm) - **nur auf ap3** bei 3/3 Strichen. Fix: `!security.ft` auf ap3 (uniform `ft=no`), Quelle - `tp28-ap3-phase2-cap.rsc` korrigiert. **Merkregel: nach jeder `sec-main`-FT-Änderung jeden CAP einzeln - `get wifiN security.ft` prüfen — CAP-lokale Inline-Overrides sind compliance-unsichtbar.** - Der TP25-Kanalplan ist am 09-04 committed (DFS-Frequenzlisten + ap1-Provisioning, **140/140, 0 Drifts**). - Offen in TP25 bleibt nur **ap2 unterperformt** (2 m/−80 dBm). Vorstufen: - [Befund](docs/handoff-20260831-tp27-upgrade-und-hebel1-befund.md) · - [Eingriff](docs/handoff-20260831-fund-e-hebel1-gezogen.md) · - [Rueckbau](docs/handoff-20260901-fund-e-hebel1-zurueckgebaut.md). -6. ap3-Bring-up 2026-08-30: [`docs/handoff-20260830-ap3.md`](docs/handoff-20260830-ap3.md) — sechs Funde zu Werks-MikroTiks, CAPsMAN und Golden-Config-Lücken. -7. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift. +3. Bei Live-Writes/Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/gotchas.md`** (ausführliche Diagnose-Fallen), **`docs/teilprojekte-archiv.md`**. + +**Aktueller Fokus:** TP45 (IP-Schichten A/B/E, C4→**TP34.2**), TP34.2 (interne DNS-Namen als Daten, Gitea #73) und TP46 (CF-/Tunnel-SSoT) offen — Status im Gitea-Issue, Detail in den `### TPnn`-Abschnitten von `teilprojekte.md`. ## SSoT & Branch-Konvention (TP36-Cutover 2026-09-07) @@ -67,15 +29,15 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der ## Was du sofort wissen musst (Inventur 2026-08-27) -- **`bf-infra`** ist aktiver Master **mit Gitea-Remote**: `origin` = `https://gitea.emsenhuber.net/admin/bf-infra.git` (live verifiziert 2026-08-30). ⚠️ **Die frühere Zeile „lokal, kein Gitea-Remote → verloren bei Festplattencrash" ist überholt** — sie stammt aus der Inventur vom 2026-08-27 und war damals richtig; das Remote existiert seit spätestens 2026-08-28 (`origin/main` = `b917691`). **Die Backup-Lücke ist damit keine Struktur-, sondern eine Disziplinfrage: `git push`.** Am 2026-08-30 lagen vier Commits nur lokal. 🔴 **Das Remote ist nur so viel wert wie der letzte Push** — der Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5) bricht Commits ab, solange kein Backup markiert ist; **das ist die Erinnerung ans Pushen, kein Hindernis.** Nicht mit `--no-verify` umgehen. 🔴 **Merkregel:** diese Zeile hat eine Session dazu gebracht, „kein Remote" zu behaupten, statt `git remote -v` zu tippen — vor jeder Aussage über Remotes/Backups **live prüfen**. -- **`admin/bf-network-r6`** (Gitea) ist Archiv, letzte aktive Commits 2026-08-25 → read-only-Header. -- **`admin/bf-golden-config`** (Gitea) ist Bot-Master, nicht manuell. 🔴 **Korrigiert 2026-09-12:** die frühere Angabe „2×/Tag" war nie real — es gab **keinen** Scheduler, Backup/Intended/Compliance liefen nur manuell aus Sessions (zuletzt 09-04, 8 Tage Stillstand). Jetzt **täglich 03:30 UTC** per ScheduledJob **„GC Backup+Compliance – taeglich"** (Nautobot-Job *Execute All Golden Configuration Jobs - Multiple Device*, Scope `platform=RouterOS`, Reihenfolge Backup→Intended→Compliance). Stand prüfen: `backup_last_success_date` am Gerät bzw. `scheduled-jobs` in Nautobot. +- **`bf-infra`** = aktiver Master mit Gitea-Remote: `origin` = `https://gitea.emsenhuber.net/admin/bf-infra.git`. **Das Remote ist nur so viel wert wie der letzte Push** — Backup ist eine Disziplinfrage: `git push`. Der Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5) bricht Commits ab, solange kein Backup markiert ist — **das ist die Erinnerung ans Pushen, kein Hindernis.** Nicht mit `--no-verify` umgehen. 🔴 **Vor jeder Aussage über Remotes/Backups `git remote -v` live prüfen** (eine Session hat „kein Remote" behauptet, statt zu tippen). +- **`admin/bf-network-r6`** (Gitea) ist Archiv → read-only-Header. +- **`admin/bf-golden-config`** (Gitea) ist Bot-Master, nicht manuell. Täglich **03:30 UTC** per ScheduledJob **„GC Backup+Compliance – taeglich"** (Nautobot-Job *Execute All Golden Configuration Jobs - Multiple Device*, Scope `platform=RouterOS`, Reihenfolge Backup→Intended→Compliance). Stand prüfen: `backup_last_success_date` am Gerät bzw. `scheduled-jobs` in Nautobot. - **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo. -- **`inventory/` ist tot** (Pipeline seit 2025-06 stale). Live-Daten aus MikroMCP/CF/Portainer. +- **`inventory/` ist tot** (Pipeline stale). Live-Daten aus MikroMCP/CF/Portainer. - **`mcp/INVENTORY.md`** ist BF-Netz-SSoT; `~/.claude.json`/`opencode.jsonc` sind globalere Werkzeugkästen. -- **Terpenfinder = `420.teilzeitladen.xyz`** (CF-Worker, Repo `admin/blaueblueten-n8n`, hinter CF Access), **kein Caddy, kein Mac mini im Pfad**. `terpene.*` **existiert nicht und soll es nicht** (live geprüft 2026-09-10). Einziger terpene-benannter CF-Gegenstand: der **interne** Tunnel `terpene-db` (`f2d2fc8a`, ohne öffentlichen Hostname, Hyperdrive→VPC→Postgres, s. `docs/cf-tunnel-mapping.md` §17). ⚠️ **Grenze:** blaueblüten ist ein eigenes Projekt — nicht anfassen. *(Die frühere „terpene.teilzeitladen.xyz:8890 via Tunnel"-Zeile war veraltet — bf-mm ist gelöscht.)* -- **shelly-mcp / portainer-mcp bewusst NICHT als MCP-Client** (User-Direktive 2026-08-28): shelly-mcp-Container auf DS923 wird via **Portainer-API** (Skript-Aufrufe, `mcp/portainer/.env` als Token-Quelle) deployt, nicht via MCP-Protokoll. Inventar-Eintrag spiegelt das. `mcp-host-policy.md` ist SSoT für die Host-/IP-Konvention. -- **Mac mini `10.40.20.167`** ist der **n8n-Host** (n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — **NICHT** der zentrale MCP-Host. Performance-Überlegung: MCP-Clients würden auf dem macmini (User-Workstation) laufen und die Box zusätzlich belasten; deshalb liegt der MCP-Host auf der **DS923** (`nas-bf`, Portainer Endpoint 14). gitea-mcp läuft aktuell noch auf dem macmini (historisch, eigene Container-Definition dort) — gehört **bei Gelegenheit** auf die DS923 umgezogen (separater Punkt, **nicht** Teil dieser Session). Container-Definitionen für n8n leben auf dem macmini (Docker Desktop); FQDN→Backend-Mapping im `caddy/Caddyfile`. +- **Terpenfinder = `420.teilzeitladen.xyz`** (CF-Worker, Repo `admin/blaueblueten-n8n`, hinter CF Access), **kein Caddy, kein Mac mini im Pfad**. `terpene.*` **existiert nicht und soll es nicht** (live geprüft 2026-09-10). Einziger terpene-benannter CF-Gegenstand: der interne Tunnel `terpene-db` (s. `docs/cf-tunnel-mapping.md` §17). ⚠️ **Grenze:** blaueblüten ist ein eigenes Projekt — nicht anfassen. +- **shelly-mcp / portainer-mcp bewusst NICHT als MCP-Client** (User-Direktive 2026-08-28): shelly-mcp-Container auf DS923 wird via **Portainer-API** (Skript-Aufrufe, `mcp/portainer/.env` als Token-Quelle) deployt, nicht via MCP-Protokoll. `mcp-host-policy.md` ist SSoT für die Host-/IP-Konvention. +- **Mac mini `10.40.20.167`** ist der **n8n-Host** (n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — **NICHT** der zentrale MCP-Host. Der MCP-Host liegt auf der **DS923** (`nas-bf`, Portainer Endpoint 14). gitea-mcp läuft noch auf dem macmini (historisch) — gehört **bei Gelegenheit** auf die DS923 umgezogen. Container-Definitionen für n8n leben auf dem macmini (Docker Desktop); FQDN→Backend-Mapping im `caddy/Caddyfile`. ## TP-Management (2026-08-26) @@ -89,7 +51,7 @@ Wenn während einer Session ein neues Teilprojekt, eine Abhängigkeit oder ein Q in eine Quick-Index-Tabelle oder einen Ist-Stand-Block — die gibt es seit dem TP36-Cutover (2026-09-07) nicht mehr in der Datei. -**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis) und ihr Gitea-Issue ist `closed` (Label `status:✅`/`❌`). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt** (Issue dann `reopen`). Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — nur noch der betroffene `### TPnn`-Prosa-Abschnitt wird angefasst. +**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis) und ihr Gitea-Issue ist `closed` (Label `status:✅`/`❌`). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt** (Issue dann `reopen`). Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`). --- @@ -112,264 +74,86 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`, ## Hardware & Zugang | Gerät | IP | Zugang | |---|---|---| -| ~~ALLNET Core-Switch (sw1)~~ | — | ❌ **außer Betrieb (2026-08-08)** — durch `crs326-core` (L3-Router + Core-Switch, Technik UG) ersetzt; physisch abgelöst. (Telnet-Runner `switch/` = nur noch historisch.) | -| ~~Omada Access-Switch (sw2)~~ | — | ❌ **abgebaut 2026-07-23** (TP15 Branch-Tausch) — durch `crs326-branch` in Technik OG ersetzt, physisch entfernt | -| **crs326-branch** (CRS326, Technik OG) | **10.40.99.2** (VLAN99) · Bootstrap Rev4 2026-08-08 | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.2/rest` · MikroMCP `crs326-branch` ✅ **live** (2026-08-16 verifiziert) · **TP15 ✅: ersetzt sw2** (L2-Switch), volle Portbelegung (ether1 Uplink-Trunk, 2/8 NAS, 3 Drucker, 4 Dock, 5/6 Sonos, 7 macmini, 24 ap3) · sw-id RGZJ-UK0T, MAC 04:F4:1C:B4:15:58 | -| **crs326-core** (CRS326, Technik UG) | **10.40.99.1** (VLAN99-SVI = L3-GW; same-subnet-Fallback `10.40.10.1`) · Identity `crs326-core` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_CORE_*`) · MikroMCP routerId `crs326-core` ✅ **live** (2026-08-16 verifiziert) · **TP15/TP21.1 ✅ 2026-08-08: PRODUKTIVER L3-Router** (ersetzt ap1) — Routing/DHCP (7 Server + 54 Leases)/DNS (**DoH zum CF-Gateway mit `verify-doh-cert=yes`**, TP23 Stufe 1 ✅ 2026-08-18; `servers=1.1.1.1` nur noch Bootstrap) · **NTP-Server für den Standort**/NAT/WG-Hub (fm+dw live); WAN `vlan100`=Liwest `90.146.33.177` · sw-id 4W04-URHT, MAC 04:F4:1C:B4:17:76 | -| hAP ax² #1 (ap1) | **10.40.99.11** (VLAN99) · Identity `ap1` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP1_*`) · REST `http://10.40.99.11/rest` · MikroMCP `ap1` ✅ live · **TP28 ✅ 2026-08-17: reiner CAP** (Manager = `crs326-core`) · ✅ **aktiv am Sideboard** (Wohnzimmer; Uplink `ether1` → core `ether3`, PoE-Port `poe-out=off`) — Kanäle `5260/Ce` (Band B, DFS, Fallback 5280) + `2412`/ch1, vom Manager · MAC D0:EA:11:4D:40:E0 · Config-Referenz `routeros/.secrets/ap1-prov-base.rsc` | -| hAP ax² #2 (ap2) | **10.40.99.12** (VLAN99) · Identity `ap2` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP2_*`) · REST `http://10.40.99.12/rest` · MikroMCP `ap2` · **TP28 ✅ 2026-08-17: reiner CAP** — **Technik UG**, Uplink `ether1` (PoE) an core `ether21`; Kanäle `5300/Ce` (Band B, DFS, Fallback 5320) + `2462`/ch11 · MAC D0:EA:11:4D:40:39 (ether1; Bridge/Mgmt `:3A`) · Config `routeros/.secrets/ap2-prov-base.rsc` | -| **cAP ax (ap3)** | **10.40.99.13** (VLAN99) · Identity `ap3` | ✅ **LIVE seit 2026-08-30** (TP28-Nachtrag) · `cAPGi-5HaxD2HaxD`, RouterOS **7.24.1** + RouterBOOT 7.24.1 (**seit 2026-08-31 ist die ganze Flotte auf 7.24.1**, TP27-Angleichung erledigt) · reiner CAP, Manager `crs326-core` · **Technik OG**, Uplink `ether1` → **branch `ether24`** (Trunk) · Kanäle **`5180/ax`** (Band A, non-DFS, 5 GHz) + **`2437`** (ch6) nach TP25-Plan · 🔴 **nur 2 Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out, **kein** Bridge-Port) → `ap.extra_ports=[]` · `device-mode=basic` (scheduler/fetch/romon **erlaubt**, anders als ap1/ap2) · Dateien `routeros/tp28-ap3-phase{1,2}-*.rsc` + `-service-accounts.rsc` + `-werksreste.rsc` | -| **🔑 Einheitliches Admin-PW (2026-08-08)** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). Werks-Defaults ap1/ap2 damit obsolet. | +| **crs326-branch** (CRS326, Technik OG) | **10.40.99.2** (VLAN99) · Bootstrap Rev4 2026-08-08 | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.2/rest` · MikroMCP `crs326-branch` ✅ **live** · **TP15 ✅: ersetzt sw2** (L2-Switch), Portbelegung (ether1 Uplink-Trunk, 2/8 NAS, 3 Drucker, 4 Dock, 5/6 Sonos, 7 macmini, 24 ap3) · sw-id RGZJ-UK0T, MAC 04:F4:1C:B4:15:58 | +| **crs326-core** (CRS326, Technik UG) | **10.40.99.1** (VLAN99-SVI = L3-GW; same-subnet-Fallback `10.40.10.1`) · Identity `crs326-core` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_CORE_*`) · MikroMCP routerId `crs326-core` ✅ **live** · **PRODUKTIVER L3-Router** — Routing/DHCP (7 Server + 54 Leases)/DNS (**DoH zum CF-Gateway mit `verify-doh-cert=yes`**, TP23 ✅)/**NTP-Server**/NAT/WG-Hub (fm+dw live); WAN `vlan100`=Liwest `90.146.33.177` · sw-id 4W04-URHT, MAC 04:F4:1C:B4:17:76 | +| hAP ax² #1 (ap1) | **10.40.99.11** (VLAN99) · Identity `ap1` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP1_*`) · REST `http://10.40.99.11/rest` · MikroMCP `ap1` ✅ live · **reiner CAP** (Manager = `crs326-core`) · **aktiv am Sideboard** (Wohnzimmer; Uplink `ether1` → core `ether3`, PoE-Port `poe-out=off`) — Kanäle `5260/Ce` (Band B, DFS, Fallback 5280) + `2412`/ch1 · MAC D0:EA:11:4D:40:E0 · Config `routeros/.secrets/ap1-prov-base.rsc` | +| hAP ax² #2 (ap2) | **10.40.99.12** (VLAN99) · Identity `ap2` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP2_*`) · REST `http://10.40.99.12/rest` · MikroMCP `ap2` · **reiner CAP** — **Technik UG**, Uplink `ether1` (PoE) an core `ether21`; Kanäle `5300/Ce` (Band B, DFS, Fallback 5320) + `2462`/ch11 · MAC D0:EA:11:4D:40:39 (ether1; Bridge/Mgmt `:3A`) · Config `routeros/.secrets/ap2-prov-base.rsc` | +| **cAP ax (ap3)** | **10.40.99.13** (VLAN99) · Identity `ap3` | ✅ **LIVE seit 2026-08-30** · `cAPGi-5HaxD2HaxD`, RouterOS **7.24.1** + RouterBOOT 7.24.1 · reiner CAP, Manager `crs326-core` · **Technik OG**, Uplink `ether1` → **branch `ether24`** (Trunk) · Kanäle **`5180/ax`** (Band A, non-DFS) + **`2437`** (ch6) · 🔴 **nur 2 Ethernet-Ports** → `ap.extra_ports=[]` · `device-mode=basic` (scheduler/fetch/romon erlaubt) · Dateien `routeros/tp28-ap3-phase{1,2}-*.rsc` | +| **🔑 Einheitliches Admin-PW** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). | | NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) | -| platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅ 2026-07-16: PG16, DB `nautobot`, Creds `postgres/.secrets`** | -| Nautobot (Portainer-Stack, NAS) | **10.40.20.154** (TP45 Schicht A 2026-09-21: `.30/.31/.32/.34` → `.154/.155/.156/.158`) | Portainer ep14, Stack Id 203 · **TP21 ✅ 2026-07-16: `ghcr.io/nautobot/nautobot:3.1.7`, DB=platform-pg, Creds in `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored)** · **TP22 ✅ 2026-08-13: Golden-Config-App + Device-Adoption DURCH** — alle 4 MikroTik adoptiert, Deploy-Pipeline scharf (Image `3.1.7-gc9`), **68/68 Compliance-Checks grün / 0 Drifts** (2026-08-16) | -| ~~Omada-Controller~~ | — | ❌ **entfernt 2026-07-16** (User hat den Container gelöscht — overkill für 1 Switch auf der Abschussliste, s. TP2.1/TP15) | -| ~~omada-mcp~~ | — | ❌ **entfernt 2026-07-16** — mit dem Controller gelöscht. ✅ **Aufräumen erledigt/gegenstandslos (geprüft 2026-08-16):** nicht mehr in `mcpServers` registriert (nur noch als „disabled" in ein paar Projekt-Scopes = harmloser Merkposten), und **kein `omada`-Hostname mehr im CF-Ingress** | -| pve-mcp | 10.40.20.173 | **StreamableHTTP `http://10.40.20.173:3000/` (Root-Pfad, NICHT /sse)** · NAS-Stack ep14 (=nas-bf `.20.10`, NICHT LXC .126!) id 202 (vlan20-only, TP14.1 2026-07-10) · **mcp-pve v0.6.1 (Upgrade 2026-07-10: Single-Session-Bug 0.4.8 „Server already initialized" gefixt, TP14.2a)** · `~/.claude.json` ✓ | -| Proxmox (pve) | 10.40.20.100 | `ssh root@10.40.20.100` (SSH-Key ✓) · **TP14 2026-07-13: Host-Mgmt VLAN1→VLAN20** (vmbr0.20 + Default-GW-Flip); `.41.100` bleibt interim ohne Route | -| Docker-LXC (auf pve) | 10.40.20.126 | Portainer CE 2.40.0 (`https://10.40.20.126:9443`) · **TP14.2 2026-07-10: LXC 400 net0 tag=20/.20.126 (iface heißt jetzt `veth20`, NICHT eth0); Docker-Netz `vlan20`=ipvlan parent `veth20`; adguard_bf1 .20.16, adguard_fm2 .20.17** | -| Mac mini | **10.40.20.167** (VLAN20) | `ssh macmini` · ⚠️ die früher hier stehende `10.40.41.167` war **stale** (VLAN1) — live korrigiert 2026-08-16 (ARP `reachable` auf `vlan20`) | -| Arris-Kabelmodem | (Bridge-Mode, keine Mgmt-IP im BF-LAN) | Liwest-Edge · speist **`crs326-core vlan100`** (VLAN100-Transfer, WAN-IP `90.146.33.177`) — seit Core-Cutover 2026-08-08, davor via sw1 gi16 → ap1 · **FritzBox 6660 seit 2026-07-01 komplett ausgebaut** (Liwest-Tausch, kein Zurück); Telefonie nicht weitergeführt | +| platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅: PG16, DB `nautobot`, Creds `postgres/.secrets`** | +| Nautobot (Portainer-Stack, NAS) | **10.40.20.154** | Portainer ep14, Stack Id 203 · **TP21 ✅: `ghcr.io/nautobot/nautobot:3.1.7`, DB=platform-pg, Creds `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored)** · **TP22 ✅: Golden-Config-App + Device-Adoption DURCH** (alle 4 MikroTik, Image `3.1.7-gc9`) | +| pve-mcp | 10.40.20.173 | **StreamableHTTP `http://10.40.20.173:3000/` (Root-Pfad, NICHT /sse)** · NAS-Stack ep14 (=nas-bf `.20.10`, NICHT LXC .126!) id 202 (vlan20-only, TP14.1) · **mcp-pve v0.6.1** (TP14.2a) · `~/.claude.json` ✓ | +| Proxmox (pve) | 10.40.20.100 | `ssh root@10.40.20.100` (SSH-Key ✓) · **TP14: Host-Mgmt VLAN1→VLAN20** (vmbr0.20 + Default-GW-Flip) | +| Docker-LXC (auf pve) | 10.40.20.126 | Portainer CE 2.40.0 (`https://10.40.20.126:9443`) · **TP14.2: LXC 400 net0 tag=20/.20.126 (iface heißt `veth20`, NICHT eth0); Docker-Netz `vlan20`=ipvlan parent `veth20`; adguard_bf1 .20.16, adguard_fm2 .20.17** | +| Mac mini | **10.40.20.167** (VLAN20) | `ssh macmini` · n8n-Host (siehe oben) | +| Arris-Kabelmodem | (Bridge-Mode, keine Mgmt-IP im BF-LAN) | Liwest-Edge · speist **`crs326-core vlan100`** (VLAN100-Transfer, WAN-IP `90.146.33.177`) · **FritzBox 6660 seit 2026-07-01 ausgebaut** (Liwest-Tausch, kein Zurück); Telefonie nicht weitergeführt | + +*Ausgemusterte Geräte (ALLNET sw1, Omada sw2/Controller/ES208G, omada-mcp): Historie in TP15/TP2.1-Archiv + `switch/README.md`.* --- ## Tooling - **MikroTik (core/branch/ap1/ap2):** MikroMCP-Server live → `mcp__mikromcp__*`-Tools (routerId `crs326-core`/`crs326-branch`/`ap1`/`ap2`). - Reads immer safe; Writes mit `dryRun:true` previewen, dann `apply_plan`. **Passwort setzen via `manage_user set-password`** (`/user set*` ist per Deny-Policy blockiert). ⚠️ **Reboot geht auf ROS 7.24 gar nicht ueber MikroMCP:** das `reboot`-Tool sendet `delay`, die REST-API kennt den Parameter nicht (`unknown parameter delay`), und `run_command` ist fuer `/system reboot*` gesperrt — Weg ist `SSHPASS="$PW" sshpass -e ssh admin@ "/system reboot"` (PW aus `~/.secrets`, im Output - maskieren). 🔴 **Niemals `sshpass -p`** — das schreibt das Passwort in die Kommandozeile, wo es in - `ps` und jedem argv-Trace steht; **genau so ist am 2026-08-22 das Core-Passwort in ein - Sitzungstranskript geraten** und musste rotiert werden. `-e` hält es in der Umgebung des Kindprozesses. - ✅ **Alle sechs Skripte im Repo sind am 2026-08-31 umgestellt** (`nautobot/gc-transaktion.py`, - `routeros/import-core-rsc.py` und die vier inzwischen nach - `routeros/archiv-tp15-cutover/` archivierten Cutover-Skripte) — gemessen - mit `ps`-Stichproben plus Negativkontrolle, nicht nur gelesen. **Neue Skripte erben die Regel nicht - von selbst**: wer einen sshpass-Aufruf schreibt, baut `env=dict(os.environ, SSHPASS=pw)` mit ein. `bridge/vlan/set` nur auf `[find … and !dynamic]`. - 🔴 **Zweiter Leak-Weg, gleiche Klasse: `list_wifi_interfaces` gibt `security.passphrase` ALLER - Profile im Klartext zurück** — am 2026-08-24 hat ein Read, der nur die `cap-wifiN`-Zuordnung finden - sollte, damit **fünf WLAN-PSKs** (`main`/`iot`/`media`/`untrusted`/`guest`) in ein Transkript - geschrieben. Kein Werkzeugfehler — das falsche Werkzeug für die Frage. **Für Kanäle - `/interface wifi channel print detail`, für die CAP-Zuordnung `… print where master=yes` mit - Feldauswahl.** Der Scan selbst (`/interface wifi scan`, **nur am Manager**) ist unbedenklich. - Merkregel dahinter: **vor jedem Read fragen, was das Tool AUSSER dem Gesuchten noch ausgibt.** -- **~~ALLNET-Switch (sw1)~~:** ❌ **Gerät ausgebaut 2026-08-08** → der Telnet-Runner unter `switch/` ist **Archiv, nicht mehr ausführbar** (kein Endpunkt). Siehe `switch/README.md`. Nicht mehr als Werkzeug anbieten. -- **~~Omada-Switch (sw2)~~:** ❌ abgebaut 2026-07-23 (TP15, durch `crs326-branch` ersetzt). Controller + omada-mcp komplett gelöscht — **Omada spielt keine Rolle mehr, nie wieder aufgreifen** (User-Entscheid 2026-07-22). Aufräum-Reste ✅ geprüft 2026-08-16 (s. Tabelle oben). + Reads immer safe; Writes mit `dryRun:true` previewen, dann `apply_plan`. **Passwort setzen via `manage_user set-password`** (`/user set*` ist per Deny-Policy blockiert). ⚠️ **Reboot geht auf ROS 7.24 gar nicht über MikroMCP** — Weg ist `SSHPASS="$PW" sshpass -e ssh admin@ "/system reboot"` (PW aus `~/.secrets`, im Output maskieren). + 🔴 **Niemals `sshpass -p`** — das schreibt das Passwort in die Kommandozeile (`ps`/argv-Trace; so ist am 2026-08-22 das Core-PW in ein Transkript geraten). `-e` hält es in der Umgebung des Kindprozesses. **Neue Skripte erben die Regel nicht von selbst**: `env=dict(os.environ, SSHPASS=pw)` einbauen. `bridge/vlan/set` nur auf `[find … and !dynamic]`. + 🔴 **`list_wifi_interfaces` gibt `security.passphrase` ALLER Profile im Klartext zurück** (2026-08-24: fünf WLAN-PSKs ins Transkript). **Für Kanäle `/interface wifi channel print detail`, für die CAP-Zuordnung `… print where master=yes` mit Feldauswahl.** Der Scan (`/interface wifi scan`, nur am Manager) ist unbedenklich. Merkregel: **vor jedem Read fragen, was das Tool AUSSER dem Gesuchten noch ausgibt.** - **Nautobot Golden Config (= der einzige legitime Config-Weg für Netzgeräte):** `nautobot/gc-configplan.py` (Token = `NAUTOBOT_SUPERUSER_API_TOKEN` aus `docs/nas-vlan20/stacks/210-nautobot.yml`, gitignored). `--job ` legt manuelle Pläne an (Dry-Run default), `--deploy-ids …` deployt gestaffelt, `--refresh-compliance` fährt **Backup → Intended → Compliance** in der einzig richtigen Reihenfolge. - ⚠️ **`POST /api/plugins/golden-config/config-plan/` ist `405` — das ViewSet ist read-only.** ConfigPlans - entstehen nur über den Job **`Generate Config Plans`** (`plan_type=manual` + `commands`-TextVar). + ⚠️ **`POST /api/plugins/golden-config/config-plan/` ist `405` — ViewSet read-only.** ConfigPlans entstehen nur über den Job **`Generate Config Plans`** (`plan_type=manual` + `commands`-TextVar). - **Docker-Management:** Portainer API `https://10.40.20.126:9443`, Key in `mcp/portainer/.env`. - 🔴 **`pct push` meldet Exit 0, auch wenn es die Quelldatei NICHT lesen kann** (gemessen 2026-09-21). - `pct push 130 /etc/caddy/Caddyfile` → `failed to open … for reading`, **`rc=0`**. - Ein `&&`/`||` hinter dem Push **beweist nichts**, und ein anschließendes `validate`/`reload` prüft - dann die **alte** Datei → **grüne Abnahme bei unverändertem Zustand**. Genau so sah der - Nautobot-Umzug am 2026-09-21 „gelandet" aus, während live noch die alte Backend-IP stand - (Symptom: 502 bei gesundem Backend). **Regel: Push-Werkzeug wird geprüft, nicht geglaubt** — nach - jedem `pct push` den Inhalt vergleichen (`md5sum` diesseits/jenseits in *einem* Remote-Aufruf, - Abbruch bei Abweichung). Muster: `push_verified()` in `caddy/deploy.sh`. Dieselbe Klasse wie - `nc -z` (09-01) und das `grep|sed`-Maskierungsmuster (08-31): **erst das eigene Werkzeug - verdächtigen, dann das Netz.** + `rc=0` trotz `failed to open … for reading`; ein `&&`/`||` dahinter beweist nichts, und `validate`/`reload` prüft dann die **alte** Datei → **grüne Abnahme bei unverändertem Zustand** (so sah der Nautobot-Umzug „gelandet" aus, live stand noch die alte Backend-IP). **Regel: Push-Werkzeug wird geprüft, nicht geglaubt** — nach jedem `pct push` den Inhalt vergleichen (`md5sum` diesseits/jenseits in *einem* Remote-Aufruf). Muster: `push_verified()` in `caddy/deploy.sh`. **Erst das eigene Werkzeug verdächtigen, dann das Netz.** - 🔴 **Die Core-Adressliste `nautobot` wird von Golden Config NIRGENDS gerendert** (Fund 2026-09-21). - Der Mgmt-Zugang des Cores für die Deploy-Pipeline läuft über `src-address-list=nautobot` - (Regel `*28`) — die Liste selbst stammt aber aus dem **handgelaufenen** - `routeros/tp5-etappe2-whitelists.rsc` und steht in **keinem** Template (`grep address-list` im - Template findet nur Kommentare). 🔴 **Ein GC-Recovery von null erzeugt damit einen Core, in den - Nautobot nicht mehr per SSH kommt** — die Pipeline wäre tot und könnte sich nicht selbst - reparieren. Gleiche Lücke wie die DHCP-Reservierungen. **Merkregel: was auf dem Gerät existiert, - aber nicht im Template, ist eine Recovery-Lücke, auch wenn Compliance 140/140 grün meldet** — - die Zahl prüft nur, was gerendert wird. - Ablösung durch Komodo (native MCP) geplant. + Der Mgmt-Zugang für die Deploy-Pipeline läuft über `src-address-list=nautobot` (Regel `*28`), die Liste + stammt aber aus dem handgelaufenen `routeros/tp5-etappe2-whitelists.rsc` — **steht in keinem Template**. + 🔴 **Ein GC-Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per SSH kommt** — die + Pipeline wäre tot. Gleiche Lücke wie die DHCP-Reservierungen. **Merkregel: was auf dem Gerät existiert, + aber nicht im Template, ist eine Recovery-Lücke, auch wenn Compliance 140/140 grün meldet** — die Zahl + prüft nur, was gerendert wird. Ablösung durch Komodo (native MCP) geplant. --- ## Kritische Gotchas -- 📜 **~~ALLNET-Gotchas (1 Telnet-VTY, `save` nicht vergessen)~~ — gegenstandslos seit 2026-08-08** (Gerät ausgebaut). Nur noch historisch relevant, s. `switch/README.md`. -- 📝 **WLAN-Logs sind seit 2026-08-30 persistent — und der Topic heißt `wireless`, nicht `wifi`.** - Auch auf dem neuen `wifi-qcom`-Stack tragen Roaming-/Connect-Events `wireless;info` bzw. - `wireless,debug`. Ein geratenes `topics=wifi` wäre wirkungslos geblieben. Zwei Regeln auf dem - **Core** (nicht auf den CAPs — unter CAPsMAN führt der Manager die Events) schicken `wireless` - und `caps` in denselben Kollektor wie TP5 (`10.40.20.18:514`, Tagesdateien unter - `/opt/tp5-syslog/log/10.40.20.1/`). ⚠️ **RouterOS UND-verknüpft Topics innerhalb EINER Regel** - (`topics=info,!firewall` = info UND NICHT firewall) — `topics=wireless,caps` hätte **nichts** - gematcht, es braucht zwei Regeln. Vorher lief alles nur in einen ~75-Zeilen-Ringpuffer, der von - einer Stunde Arbeit überschrieben war; ein seit Wochen gemeldetes Client-Problem war deshalb - **rückwirkend nicht diagnostizierbar**. `/system logging` steht jetzt unter der 27. ComplianceRule - `system-logging` — vorher unter keiner. **Merkregel: bevor man ein sporadisches Problem sucht, - erst prüfen, ob es überhaupt irgendwo mitgeschrieben wird.** -- 🧮 **Ein Log-Burst über VIELE MACs ist Infrastruktur, über EINE MAC ein Client** (Fund 2026-08-31). - Am 31.08. sahen 13 × `caps,info … joined`, 4 × `activity timeout` und zwei Massenbursts - `association rejected, no cached PMK` (74 Zeilen, **38 verschiedene MACs**) wie eine CAPWAP- - Instabilität aus, die das ganze Netz trifft. Es waren die **Reboots des TP27-Upgrades**. Jedes - Ereignis fiel auf einen Neustart. 🔴 **Vor jeder Client-Diagnose erst `grep " caps,"` und - `/system resource get uptime` lesen** — sonst sucht man einen Gerätefehler, der ein geplanter - Reboot war. Der Umkehrschluss gilt auch: 93 von 93 `reauthenticating` von **einer** MAC auf - **einer** SSID sind ein echter Client-/SSID-Befund. -- 🗺️ **Die `cap-wifiN`-Karte verschiebt sich bei JEDEM CAP-Rejoin, nicht nur beim Provisionieren.** - Am 2026-08-31 wanderte sie zweimal an einem Tag (08:09 Re-Provisionierung, 14:36 Core-Reboot); - eine im Handoff festgehaltene Tabelle war nach sechs Stunden falsch. ⇒ **Interface-Namen in - Logzeilen sind ohne einen zeitnahen Karten-Snapshot NICHT zuschreibbar.** Vor jeder Auswertung - `/interface wifi radio print terse proplist=cap,radio-mac,interface` am Manager lesen — und wenn - zwischen Ereignis und Auswertung ein Rejoin liegt, die Zuordnung als **unbekannt** behandeln - statt zu raten. -- 🔑 **CAPsMAN verteilt KEINE Login-Passwörter — nur WLAN-PSKs.** Zwei verschiedene Dinge heißen - „Passwort": die **WPA-Passphrase** (`/interface wifi security`) liegt am Manager und wird an die - CAPs verteilt ✅; der **Geräte-Login** (`/user`) ist eine **lokale Benutzer-DB auf jedem Gerät** ❌. - Beleg: ap1 trägt `admin`, `ha`, `nautobot` lokal, mit gerätelokalen `last-logged-in`-Zeitstempeln. - Das einheitliche Admin-PW ist eine **von Hand angewendete Konvention**, keine Automatik — und - logisch kann es nicht anders sein: ein Gerät *wird* erst zum CAP, nachdem sich jemand eingeloggt hat. -- 🔴 **Ein neuer CAP braucht `configuration.manager=capsman` auf JEDEM Master-Radio.** - `/interface wifi cap set enabled=yes` allein reicht nicht — ohne das Feld bietet der CAP seine - Radios dem Manager gar nicht erst an. **Symptom (2026-08-30, ap3):** `remote-cap print` zeigt - `state=Ok`, aber `/interface wifi radio print` am Manager listet für dieses Gerät **nichts**, und - weil die Provisioning-Regel dann nicht matcht, sucht man den Fehler in der `radio-mac`. Im - Werkszustand ist das Feld leer (= `local`). -- 🔌 **Werks-MikroTik am Trunk: `ether1` ist WAN, und das sieht aus wie ein Netzproblem.** Beim - cAP ax (und den hAPs) steht `ether1` per defconf in der Interface-Liste `WAN`. Folge: Discovery - aus (**kein MNDP → in `/ip neighbor` unsichtbar**), Input-TCP verworfen (22/80/8291/8728 **zu**), - **ICMP aber erlaubt** — es *pingt*, was sich wie „erreichbar" liest. Kein Registry-Eintrag und - kein MikroMCP hilft, denn Port 80 ist zu. **Der Weg rein ist die LAN-Seite** (`ether2` oder das - Werks-WLAN); dort `/interface list member remove [find where interface=ether1]` — das ist der Fix. -- 🔀 **`pvid` ist INGRESS, `tagged`/`untagged` ist EGRESS.** Ein Port mit `pvid=99`, der in der - *tagged*-Liste von VLAN99 steht, nimmt untagged an und antwortet **getaggt**. Symptom - (2026-08-30): ein DHCP-Lease hing ewig auf `offered` und ARP auf `failed` — der DISCOVER kam an, - der ACK nie. Für ein Werksgerät gehört der Port **untagged** in die VLAN-Zeile. -- 🕳️ **Ein neues Gerät ohne `secrets_group` erzeugt eine LÜCKE, keine Fehlermeldung.** Ohne - Secrets Group scheitert der Golden-Config-**Backup**-Job still; die *intended* config wird - trotzdem sauber gerendert, aber ohne Backup entstehen **keine Compliance-Einträge** — und die - Gesamtzahl bleibt unauffällig (bei uns 104/104), weil ein Gerät ohne Ergebnisse auch keine Drift - meldet. 🔴 **Merkregel: eine gleichbleibende Compliance-Zahl nach dem Hinzufügen eines Geräts ist - ein BEFUND, kein Erfolg.** Erst `130/130` bewies, dass ap3 wirklich geprüft wird. Zweite - Voraussetzung: die dedizierten Service-Accounts `nautobot`/`ha` (TP-PWD) existieren auf einem - Werksgerät nicht. -- 🔢 **Firewall-Regelnummern aus `print where` sind Handles, keine Positionen.** Nach einem - `place-before` zeigte `/ip firewall filter print terse where comment~"..."` die Nummer **60**, - während `drop` auf **59** stand — die Regel lag trotzdem davor. Beweis ist der **Zähler**: eine - Regel hinter dem `drop` kann nicht zählen. Wer nach der Zahl geht statt nach `print stats`, - „repariert" eine funktionierende Regel kaputt. -- 🔌 **`crs326-core` hat KEIN PoE-out** (`/interface ethernet poe` existiert dort gar nicht, - CRS326-24G-2S+). Ein AP an einem Core-Port braucht eigene Stromversorgung. Der **cAP ax** bringt - dafür **Netzteil UND Gigabit-PoE-Injector im Karton** mit (Produktseite, 2026-08-30) — die - TP25-Notiz „kein Netzteil im Lieferumfang üblich" gilt für dieses Modell **nicht**. Symptom, wenn - man das übersieht: der Core-Port steht auf `no-link` und man sucht den Fehler im VLAN-Tagging. -- 🧩 **Die AP-Flotte ist seit ap3 heterogen — Literal-Listen im GC-Template sind dadurch gefährlich - geworden.** Der AP-Zweig hatte `ether3/ether4/ether5` als Bridge-Ports **hartkodiert**; das war die - stille Annahme *„AP == hAP ax² mit 5 Ports"*. Der **cAP ax hat zwei**. Seit 2026-08-30 kommt die - Liste aus **`ap.extra_ports`** (ConfigContext `ap-common`, Default = hAP-ax²-Bestand; ap3 - überschreibt sie per `local_config_context_data` mit `[]`). ⚠️ **Nautobot merged Dicts tief, Listen - ERSETZEND** — für diesen Fall genau richtig, bei `zones`/`radios` wäre es eine Falle. - Merkregel: **bevor ein zweites Modell derselben Rolle dazukommt, die Literal-Blöcke im Template - daraufhin lesen, welche Zeile eigentlich „Gerätetyp" meint und nicht „Rolle".** -- 🔵 **Container auf MikroTik — korrigiert 2026-08-25, die alte Pauschalregel war nie nachgemessen.** - Es hiess hier „hAP ax² hat 128 MB Flash, kein USB → Container NICHT auf dem Router". Live gelesen: - **ap1/ap2 sind `arm64` mit 95 MB FREIEM Flash und 690 MB freiem RAM** — das reicht für **einen** - kleinen Container (USB ist laut MikroTik-Doku nur für schnellere Extraktion, nicht Pflicht). - Faustregel: **~70 MB nutzbar je AP**, weil RouterOS Reserve für sein eigenes `.npk` braucht. - 🔴 **`crs326-core` dagegen NIE:** `arm` **32-bit**, **2,9 MB frei von 16 MB**. Der Core ist der - Netz-SPOF, aber **kein Anwendungs-Host** — das ist eine Hardware-Tatsache, keine Politik. - 🔑 **Und die TP16.1-Aussage „Connector als RouterOS-Container ❌ nicht möglich" gilt nur für den - WARP-Connector**, weil der ein **TUN-Device** braucht (RouterOS reicht keines durch). - **`cloudflared` braucht kein TUN** — Userspace-Proxy. Die Schlussfolgerung überträgt sich nicht. - Merkregel: **bei einer „strukturellen" Einschränkung erst prüfen, ob sie ein Default ist oder ein - Gesetz** — und ob sie dem konkreten Dienst überhaupt gilt. -- **Schwere Container laufen weiterhin auf NAS/pve**, nicht am Router (Caddy, AdGuard: die eine Config - ändert sich staendig, die andere schreibt dauernd Logs auf NAND). -- ⏱️ **Ein konstanter 10-Sekunden-Hänger ist ein 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 in - `/etc/network/interfaces`** (Ursache gefunden + behoben 2026-09-01). In vier LXCs standen dort 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. -- 🧪 **Ein Messwerkzeug, das etwas anderes beantwortet als die Frage, kostet mehr Zeit als der Fehler** - (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`). -- 🔌 **`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. -- **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. -- 📜 **~~ALLNET VLAN-DB (TP1)~~ — historisch.** Die VLAN-Semantik gilt weiter (**40=MEDIA, 50=UNTRUSTED**), die Portangaben (GE1/GE2/GE8) beziehen sich auf ein ausgebautes Gerät. -- **ap3 hängt an `crs326-branch ether24`** (Trunk, native VLAN1 + tagged 10-99); **branch-Uplink `ether1` → `crs326-core ether8`** (korrigiert 2026-08-16 — die frühere Angabe „→ sw1 GE8" stammt aus der Zeit vor dem Core-Tausch; MAC-hart belegt in TP22 Schritt 5). ap2 hängt seit dem TP25-Tausch (2026-09-15) an `core ether21` (Technik UG), ap1 an `core ether3` (Sideboard). -- **Mesh = NEIN** (beide APs haben wired Trunk-Uplink → Loop-Risiko). -- **Beide APs = CAPs, Manager ist `crs326-core` (TP28 ✅ 2026-08-17).** Der Entscheid „kein CAPsMAN" von 2026-07-22 ist **revidiert**: er galt der Konstellation ap1=Manager+AP, in der ein Manager seine eigenen Radios nicht provisionieren kann — mit dem Core als Manager sind beide APs symmetrisch. Der Core braucht dafür **kein** `wifi-qcom`, `capsman` steckt im Basispaket. - - 🔴 **`/interface wifi scan` geht am CAP NICHT — nur am Manager** (Fund 2026-08-23). Das Gerät - antwortet `failure: not allowed (/interface/wifi/scan)`; das ist **weder device-mode noch eine - Policy-Lücke** (Gegentest: `/tool fetch` sagt auf demselben Gerät explizit „not allowed **by - device-mode**", und die Gruppe `full` hat `test`+`sniff`). Ein CAP besitzt sein Radio nicht. - Richtig ist `/interface wifi scan cap-wifiN duration=…` **am Core** — dort läuft es einwandfrei. - ⚠️ Die `cap-wifiN`-Zuordnung vorher über `radio-mac` bestimmen, sie ist nicht stabil. - - 🔴 **Messfalle: unter CAPsMAN führt der MANAGER die Tabellen.** `/interface/wifi/registration-table` und die `wireless`-Logs sind auf ap1/ap2 **leer**, obwohl das WLAN läuft. **Immer am Manager zählen.** - - 🔴 **Die CAPs finden den Manager per Layer-2-Discovery** (`discovery-interfaces=vlan99`, `current-caps-man-address` ist eine **MAC**, keine IP). Deshalb müssen **MNDP (udp/5678)** und **CAPWAP (udp/5246-5247)** in der `input`-Kette des Core erlaubt bleiben — sonst kommt das WLAN nach dem nächsten AP-Reboot nicht zurück, während laufende Sessions unauffällig weiterlaufen (Fund 2026-08-18, TP5 Etappe 4). - - 🔁 **Slave-VAPs aendern heisst zweimal arbeiten (2026-08-22).** `slave-configurations` in einer - `provisioning`-Zeile zu setzen bewirkt am Funk **nichts**, solange nicht - `/interface wifi capsman remote-cap provision [find where identity="apX"]` nachgezogen wird — - und das gilt in **beide** Richtungen: auch beim Entfernen senden die VAPs sonst munter weiter. - 🔴 **Und die Abnahme nicht am Kommentar festmachen:** beim Rueckbau wird der Interface-Kommentar - geleert, waehrend der VAP steht. Wer auf `comment~"ssid"` prueft, sieht "weg" und liegt falsch. - Richtig ist `datapath.vlan-id` bzw. die Interface-Zahl **am CAP** (`/interface wifi print` dort). - Beim Interface-*Zustand* ist ueberhaupt der CAP die Wahrheit: der Manager zeigt nach einer - Re-Provisionierung `running=false` fuer alle `cap-*`, waehrend am CAP alles laeuft — das - Gegenstueck zur Regel, dass die *Registration-Table* am Manager zu lesen ist. - - **Local Forwarding ist keine Einstellung mehr:** im neuen `/interface/wifi` gibt es kein zentrales Forwarding, `datapath` existiert dort nicht. - - 🔴🔴 **`/interface wifi cap slaves-datapath` MUSS auf beiden APs gesetzt sein** (`dp-caps-slaves`, ein lokales datapath-Objekt mit `bridge=bridge` und **ohne** `vlan-id`). Es ist das Einzige, was die vom Manager erzeugten **Slave-VAPs** lokal in die Bridge haengt: der Manager schickt `datapath.bridge=bridge` mit, **der CAP uebernimmt davon nur die `vlan-id`**. Fehlt es, assoziieren Clients auf `-iot`/`-media`/`-untrusted`/`-guest` sauber und bekommen **nie DHCP**, waehrend `dirtyvader-main` weiterlaeuft — dessen Master-Radios `wifi1`/`wifi2` sind statische Bridge-Ports. Genau so lagen IOT+MEDIA am 2026-08-18 rund 14 Stunden (Ursache: CAPsMAN-Cutover 08-17, nicht die TP5-Haertung). **Abnahme fuer jeden WLAN-Umbau ist eine frische DHCP-Lease pro SSID — NICHT die Registration-Table**, die zeigte waehrend des Ausfalls 25 zufriedene IOT-Clients. - - 🔎 **Merkregel „connect ok, kein DHCP":** zuerst `torch` auf dem betroffenen VLAN am Core UND den TP5-Drop-Log lesen. Sind **beide leer**, wird nichts verworfen — es kommt nichts an, und die Firewall ist entlastet. Dann liegt es an L2 (Bridge-Mitgliedschaft/Tagging), nicht am Regelwerk. -- mgmt-VLAN-Wechsel am Switch = Lockout-Risiko: VLAN muss existieren + Port muss es tragen, in einem Schritt. -- 🔥 **Die Firewall ist seit 2026-08-18 deny-by-default — in BEIDEN Ketten.** Ein neuer Dienst über eine VLAN-Grenze braucht ab jetzt eine explizite Regel, sonst funktioniert er einfach nicht. Diagnosewerkzeug ist die **Log-Regel direkt vor dem Drop** (`TP5-WOULD-DROP` / `TP5-INPUT-WOULD-DROP`), die weiterläuft und in den Kollektor `10.40.20.18:514` schreibt (Dateien auf dem Docker-LXC unter `/opt/tp5-syslog/log/`). **Erst dort nachsehen, bevor irgendwo anders gesucht wird.** **MAIN→INFRA/MEDIA/MGMT/IOT stehen dauerhaft offen** (IOT seit TP5 E5, 2026-08-19 — Drucker + Shellys); **IOT→MAIN bleibt bewusst zu**, dort sitzt der Wert der Segmentierung. Ad-hoc-Zugriff aus MAIN in ein **noch geschlossenes** VLAN (UNTRUSTED, GUEST) läuft über die selbstverfallende Liste: `/ip firewall address-list add list=jit-admin address= timeout=1h`. *(Die früher hier genannten Beispiele „Scannen, IoT anlernen" brauchen jit-admin seit E5 nicht mehr.)* - - 🖨️ **Merkregel aus dem E5-Drop-Log:** der Drucker fällt mit **udp/161 (SNMP)** auf, nicht mit 631 — macOS fragt zuerst den SNMP-Status ab und bricht ab, bevor es zu IPP kommt. **Eine Whitelist auf 631 hätte nichts repariert.** Bei „Gerät antwortet nicht" also das Log lesen, statt den erwarteten Port zu raten. -- ⏰ **Zeitquelle ist der Core** (`/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. -- 📡 **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 `: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 "`) 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. -- 🏠 **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 ` 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 `/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). -- 📜 **~~Omada ES208G = Easy Managed~~ — gegenstandslos** (Gerät abgebaut 2026-07-23, Controller gelöscht). -- **L3-Router = `crs326-core` (TP21.1 ✅ 2026-08-08, ersetzt ap1).** Bridge vlan-filtering=yes; VLAN-SVIs `.1` je VLAN mit DHCP (7 Server / 54 Leases); DNS über **DoH** zum CF-Gateway, `verify-doh-cert=yes` (TP23 Stufe 1 ✅ 2026-08-18). Die alte Notiz „core hat kein CA" war zu pessimistisch: es braucht kein Bundle, sondern **genau einen Root** (GTS Root R4, ~1 KB) — die Kette ist Leaf → GTS WE1 → R4. **Beide Firewall-Ketten enden seit 2026-08-18 mit `drop`** (TP5 E3/E4); `TP10-seal-lan` ist `disabled`, Mgmt-Input kommt nur noch aus VLAN10, von HA (`8728`), von Nautobot (`22`) und über `cf-mesh`. **ap1/ap2 sind NUR noch APs** — kein DHCP/DNS/NAT/Routing/WG dort. (Die frühere „TP4: ap1 ist L3-Router"-Zeile ist überholt.) -- **✅✅ Edge-Cutover (2026-07-01; Router-Umzug 2026-08-08): FritzBox ausgebaut, Arris im Bridge-Mode.** Liwest hat die FB6660 dauerhaft gegen ein Arris getauscht (physisch weg). WAN terminiert auf **`crs326-core vlan100`** (VLAN100-Transfer), core zieht die öffentliche Liwest-IP `90.146.33.177` → kein Double-NAT. **core = Public-Edge + WG-Hub** (fm/dw live), NICHT mehr ap1. Telefonie nicht weitergeführt. Runbook `routeros/wan-cutover-runbook.md` (ap1-Teile historisch). -- **hAP `device-mode=home` (ap1/ap2): scheduler + fetch blockiert** (`not allowed by device-mode`) → AP-Provisionierung idempotent **ohne** Scheduler. **`[/ping]` gibt nil in 7.23**. REST je AP `http://10.40.99.11|12/rest` (Creds = `ROUTER_AP{1,2}` in `~/.claude.json`). **Der L3-Router crs326-core ist KEIN device-mode=home** → dort gehen scheduler/fetch (z.B. DDNS-Updater). -- **Erstzugriff Werks-hAP am CRS-Fabric:** Uplink `ether1` = Werks-WAN → `192.168.88.1` (LAN) darüber NICHT erreichbar. Weg: Gerät auf **LAN-Port (ether2)** patchen ODER **Core-Temp-Pfad** `192.168.88.2/24` auf core-`bridge` + srcnat masq `192.168.88.0/24`, dann scp+`/import` (danach Temp-Pfad + stale ARP `.88.1` entfernen). Winbox-Neighbors zeigt die APs NICHT (MNDP pro-VLAN, Mgmt auf vlan99) — kein L2-Fehler. -- 🖨️ **ZWEI Drucker, nicht verwechseln (geklärt 2026-08-23):** der **Brother MFC** ist der - *eigentliche* Drucker und hängt auf **VLAN10** (`10.40.10.6`, MAC `30:05:5C:78:86:20`, **statisch**, - keine DHCP-Reservierung) — er steht damit im *selben* VLAN wie die Macs, die `forward`-Kette greift - dort gar nicht. Der **P750W ist der Etikettendrucker** auf **VLAN30** (`10.40.30.5`) — nur für ihn - gilt der VLAN-übergreifende SNMP-Fall unten. -- **P750W (Brother, VLAN30) läuft auf `dirtyvader-iot`** (User-bestätigt 2026-08-08) — **kein** Access-List-Pin nötig; die alte „nur am co-located L3-GW-AP erreichbar"-Logik ist mit L3-GW auf dem Core hinfällig. Coverage-Kanten/Terrasse grenzwertig (−76..−89 dBm); TX am AT-Max (18/15 dBm); echter Fix = Platzierung. SSoT `docs/wifi-p750w-coverage-and-map.md`. ⚠️ **Power-Save ist KEIN Thema dieses Netzes** — die früher hier stehende Merkregel „DHCP-geht-aber-Ping-nicht = ARP/Power-Save" war eine Fehldiagnose (2026-08-13 korrigiert: der Drucker war schlicht ausgeschaltet, das Ping-Symptom war WARP, s.u.). Nicht wieder einführen. -- **Ping auf JEDE lokale IP tot, aber Web/App/Print (TCP) geht überall = WARP-Client lokal versehentlich noch aktiv** (2026-08-13 live beobachtet, BF vor Ort): `cloudflared`/WARP ist QUIC-Proxy, kann **kein ICMP**, reicht TCP aber durch — auch wenn man physisch im BF-LAN steht, nimmt der Client bei aktivem WARP trotzdem den Tunnel-Umweg. **Fix: WARP trennen**, kein Router-Eingriff nötig. **Vor jeder Firewall-/Mangle-/VLAN-Suche zuerst prüfen**, wenn das Symptom „Ping auf ALLES tot, TCP geht" lautet — das ist ein Client-, kein Netzproblem. Strukturell schließt **TP16.1** (natives WireGuard-CF-Mesh auf `crs326-core` statt cloudflared-QUIC) genau diese ICMP-Lücke. Memory `ref_warp_local_icmp_trap`. + +- 🔥 **Die Firewall ist seit 2026-08-18 deny-by-default — in BEIDEN Ketten.** Ein neuer Dienst über eine VLAN-Grenze braucht eine explizite Regel, sonst funktioniert er nicht. Diagnosewerkzeug = **Log-Regel direkt vor dem Drop** (`TP5-WOULD-DROP` / `TP5-INPUT-WOULD-DROP`, Kollektor `10.40.20.18:514`, Dateien Docker-LXC `/opt/tp5-syslog/log/`). **Erst dort nachsehen, bevor irgendwo anders gesucht wird.** **MAIN→INFRA/MEDIA/MGMT/IOT offen** (IOT seit TP5 E5, 2026-08-19); **IOT→MAIN bewusst zu.** Ad-hoc-Zugriff aus MAIN in geschlossenes VLAN (UNTRUSTED, GUEST): `/ip firewall address-list add list=jit-admin address= timeout=1h`. + - 🖨️ **Drucker fällt mit udp/161 (SNMP) auf, nicht 631** — macOS fragt zuerst SNMP, bricht dann ab. Bei „Gerät antwortet nicht" das Log lesen, statt den erwarteten Port zu raten. +- **Beide APs = CAPs, Manager ist `crs326-core` (TP28 ✅).** Mesh = **NEIN** (beide APs haben wired Trunk-Uplink → Loop-Risiko). Der Core braucht dafür **kein** `wifi-qcom` (capsman steckt im Basispaket). + - 🔴🔴 **`/interface wifi cap slaves-datapath` MUSS auf beiden APs gesetzt sein** (`dp-caps-slaves`, `bridge=bridge`, **ohne** `vlan-id`). Fehlt es, assoziieren Clients auf `-iot`/`-media`/`-untrusted`/`-guest` sauber und bekommen **nie DHCP**, während `dirtyvader-main` weiterläuft (so lagen IOT+MEDIA am 2026-08-18 ~14 h). **Abnahme für jeden WLAN-Umbau = frische DHCP-Lease pro SSID — NICHT die Registration-Table.** + - 🔴 **`/interface wifi scan` geht am CAP NICHT — nur am Manager.** `cap-wifiN`-Zuordnung vorher über `radio-mac` bestimmen, sie ist nicht stabil. + - 🔴 **CAPs finden den Manager per L2-Discovery** (`discovery-interfaces=vlan99`, `current-caps-man-address` = MAC). Deshalb **MNDP (udp/5678)** + **CAPWAP (udp/5246-5247)** in der input-Kette des Core offen lassen. + - 🔁 **Slave-VAPs ändern = zweimal arbeiten:** nach `slave-configurations`-Änderung `/interface wifi capsman remote-cap provision [find where identity="apX"]` nachziehen (beide Richtungen). Abnahme nicht am Kommentar (`comment~"ssid"`), sondern `datapath.vlan-id` bzw. Interface-Zahl **am CAP**. + - 🔎 **„connect ok, kein DHCP":** zuerst `torch` auf dem VLAN am Core UND den TP5-Drop-Log lesen. Beide leer ⇒ nichts wird verworfen, es kommt nichts an → L2 (Bridge-Mitgliedschaft/Tagging), nicht Regelwerk. +- 📝 **WLAN-Log-Topic heißt `wireless`, nicht `wifi`.** Zwei Regeln auf dem **Core** (nicht den CAPs) schicken `wireless` + `caps` zum Kollektor `10.40.20.18:514`. ⚠️ **RouterOS UND-verknüpft Topics in EINER Regel** — `topics=wireless,caps` matcht **nichts**, es braucht zwei Regeln. `/system logging` steht unter der ComplianceRule `system-logging`. **Merkregel: bevor man ein sporadisches Problem sucht, erst prüfen, ob es überhaupt mitgeschrieben wird.** +- 🧮 **Log-Burst über VIELE MACs = Infrastruktur, über EINE MAC = Client.** Vor jeder Client-Diagnose erst `grep " caps,"` + `/system resource get uptime` lesen (ein Massenburst war oft ein geplanter Reboot). Umkehrschluss: 93/93 `reauthenticating` von einer MAC auf einer SSID = echter Client-/SSID-Befund. +- 🗺️ **Die `cap-wifiN`-Karte verschiebt sich bei JEDEM CAP-Rejoin.** Vor jeder Auswertung `/interface wifi radio print terse proplist=cap,radio-mac,interface` am Manager lesen; liegt zwischen Ereignis und Auswertung ein Rejoin, Zuordnung als **unbekannt** behandeln statt raten. +- 🔑 **CAPsMAN verteilt KEINE Login-Passwörter — nur WLAN-PSKs.** WPA-Passphrase (`/interface wifi security`) liegt am Manager ✅; Geräte-Login (`/user`) ist eine **lokale DB je Gerät**. Das einheitliche Admin-PW ist eine **Hand-Konvention**, keine Automatik. +- 🔴 **Ein neuer CAP braucht `configuration.manager=capsman` auf JEDEM Master-Radio.** `/interface wifi cap set enabled=yes` allein reicht nicht — im Werkszustand ist das Feld leer (= `local`). +- 🔌 **Werks-MikroTik am Trunk: `ether1` ist WAN.** Interface-Liste `WAN` → Discovery aus (kein MNDP), Input-TCP (22/80/8291/8728) **zu**, ICMP **erlaubt** → es pingt, wirkt erreichbar. **Weg rein ist die LAN-Seite** (`ether2`), dort `/interface list member remove [find where interface=ether1]`. +- 🔀 **`pvid` ist INGRESS, `tagged`/`untagged` ist EGRESS.** Werksgerät-Port gehört **untagged** in die VLAN-Zeile. +- 🕳️ **Ein neues Gerät ohne `secrets_group` erzeugt eine LÜCKE, keine Fehlermeldung.** Backup-Job scheitert still, keine Compliance-Einträge. 🔴 **Eine gleichbleibende Compliance-Zahl nach dem Hinzufügen eines Geräts ist ein BEFUND, kein Erfolg.** Zweite Voraussetzung: dedizierte Service-Accounts `nautobot`/`ha` existieren auf einem Werksgerät nicht. +- 🔢 **Firewall-Regelnummern aus `print where` sind Handles, keine Positionen.** Beweis ist der **Zähler** (`print stats`), nicht die Zahl. +- 🔌 **`crs326-core` hat KEIN PoE-out** (CRS326-24G-2S+). AP am Core-Port braucht eigenes Netzteil; der **cAP ax bringt Netzteil + PoE-Injector im Karton** mit. +- 🧩 **Die AP-Flotte ist heterogen (cAP ax = 2 Ports vs hAP = 5).** Bridge-Ports kommen aus **`ap.extra_ports`** (ConfigContext `ap-common`; ap3 überschreibt per `local_config_context_data` mit `[]`). ⚠️ **Nautobot merged Dicts tief, Listen ERSETZEND.** **Bevor ein zweites Modell derselben Rolle dazukommt, Literal-Blöcke lesen** („Gerätetyp" vs „Rolle"). +- 🔵 **Container auf MikroTik:** ap1/ap2 = `arm64`, 95 MB Flash frei → **ein** kleiner Container ok (~70 MB nutzbar). **`crs326-core` dagegen NIE** (`arm` 32-bit, 2,9 MB frei) — SPOF, kein Anwendungs-Host (Hardware-Tatsache). **`cloudflared` braucht kein TUN** (Userspace-Proxy) → RouterOS-Container möglich; der WARP-Connector nicht (braucht TUN). **Schwere Container laufen weiterhin auf NAS/pve**, nicht am Router. +- **hAP `device-mode=home` (ap1/ap2): scheduler + fetch blockiert** → AP-Provisionierung idempotent **ohne** Scheduler. **`crs326-core` ist KEIN `device-mode=home`** → dort gehen scheduler/fetch (DDNS-Updater). +- **Erstzugriff Werks-hAP am CRS-Fabric:** Uplink `ether1` = Werks-WAN → `192.168.88.1` (LAN) darüber NICHT erreichbar. Weg: **LAN-Port (ether2)** patchen ODER **Core-Temp-Pfad** `192.168.88.2/24` + srcnat masq `192.168.88.0/24`, dann scp+`/import` (danach Temp-Pfad + stale ARP `.88.1` entfernen). +- 🖨️ **ZWEI Drucker, nicht verwechseln:** **Brother MFC** = eigentlicher Drucker, **VLAN10** (`10.40.10.6`, MAC `30:05:5C:78:86:20`, statisch). **P750W** = Etikettendrucker, **VLAN30** (`10.40.30.5`), läuft auf `dirtyvader-iot`. P750W-Coverage SSoT `docs/wifi-p750w-coverage-and-map.md`. ⚠️ **Power-Save ist KEIN Thema** (frühere „DHCP-geht-aber-Ping-nicht=ARP/Power-Save"-Merkregel war Fehldiagnose). +- **Ping auf JEDE lokale IP tot, aber TCP geht überall = WARP-Client lokal noch aktiv.** `cloudflared`/WARP ist QUIC-Proxy, kann **kein ICMP**. **Fix: WARP trennen**, kein Router-Eingriff. Memory `ref_warp_local_icmp_trap`. +- **L3-Router = `crs326-core` (TP21.1 ✅, ersetzt ap1).** Bridge vlan-filtering=yes; VLAN-SVIs `.1` je VLAN mit DHCP (7 Server/54 Leases); DNS über **DoH** zum CF-Gateway `verify-doh-cert=yes` (TP23 ✅). Beide Firewall-Ketten enden mit `drop`; Mgmt-Input nur aus VLAN10, von HA (`8728`), von Nautobot (`22`) und über `cf-mesh`. **ap1/ap2 sind NUR noch APs.** +- **✅✅ Edge-Cutover:** FritzBox aus, Arris Bridge-Mode. WAN terminiert auf **`crs326-core vlan100`**, öffentliche Liwest-IP `90.146.33.177`, kein Double-NAT. **core = Public-Edge + WG-Hub.** Runbook `routeros/wan-cutover-runbook.md`. +- **mgmt-VLAN-Wechsel am Switch = Lockout-Risiko:** VLAN muss existieren + Port muss es tragen, in einem Schritt. + +**Diagnose-Fallen (Detail in [`docs/gotchas.md`](docs/gotchas.md), hier nur die Merkregel):** +- ⏰ Zeitstempel zwischen Geräten widersprüchlich → `/system ntp client get status` (Zeitquelle = Core, DHCP-Option 42, ComplianceRule `system-ntp`). +- 🔌 Vor jedem geplanten LXC-Neustart `systemctl --failed` lesen + danach Ports gegen vorher vergleichen. +- 🧪 „alles gleichzeitig kaputt" → **zuerst das eigene Werkzeug verdächtigen** (`nc -z` lügt), dann das Netz. +- 📡 Sonos sucht hunderte Male ohne Verbindung → **Relay, nicht Netz** (Richtungsbilanz `grep -c "via eth1"` vs `"via eth0"`). +- ⏱️ Konstanter 10-s-Hänger = **DNS-Timeout**, nicht Last (DB-Reverse-Lookup auf toten Resolver). +- 🏠 HA-`http:`-Block in `configuration.yaml` **wirkungslos** → `.storage/http` + WS `http/config/configure`→`promote`. +- 🐳 Nautobot auf DS923: **Image-Healthcheck aus**; `cpus:` geht auf Synology nicht. --- @@ -381,43 +165,17 @@ Verhaltensregeln für nachfolgende AI-Sessions — kleiner, klarer, weniger Drif - **Eine große Fragenrunde statt vieler kleiner.** Wenn der Plan von Annahmen abhängt, sammle die Annahmen in **einer** Runde, nicht in 3–4 Mikrorunden. - **Master-Frage früh stellen.** Bei jedem Repo, jeder Datei, jedem Tooling: „Was ist die SSoT, wo lebt der Master, was ist Mirror?" — **bevor** du eine Lösung vorschlägst. - **Drift zuerst, Lösungen später.** Drift-Tabelle posten + fragen, was richtig ist. Keine Lösungen vor dem Befund. -- **MCP-Erstellung/-Stilllegung = aktiv melden** (2026-08-27, Christoph-Direktive). Wer einen neuen MCP anlegt, ändert oder abschaltet — **meldet das proaktiv** an den User, nicht stillschweigend im Commit. Begründung: jede AI-Session ist gleichzeitig **Oberhand über Container** (MCP-Hosts) und **Garbage-Collector** auf Infra-Ebene — wenn Drift unbemerkt entsteht, sammelt sich Müll. -- **MCPs brauchen Host-Verifikation** vor Edit. Wenn der MCP verändert/editiert wird: vorher per Live-Read (nc/ps/curl) prüfen, ob der Host noch antwortet. Beispiel: 2026-08-27 — `shelly-mcp` zeigte auf `10.40.20.30:3030`, aber kein Host antwortet. Konfig auf `enabled=false`, dokumentieren statt hart fehlschlagen zu lassen. **(Stand 2026-08-28: shelly-mcp wird auf DS923 `10.40.20.180` deployt — `BUILD.md` liegt bereit.)** -- **Mac mini ist n8n-Host, nicht zentraler MCP-Host** (User-Direktive 2026-08-28): MCP-Dienste gehören auf die **DS923** (`nas-bf`, Portainer Endpoint 14) — nicht auf die User-Workstation. Aktuell laufen gitea-mcp + n8n-mcp **noch** auf dem macmini (historisch), gehören **bei Gelegenheit** auf die DS923 umgezogen. -- **Tote MCP-Skripte → ins Projekt-Repo, nicht global.** Wenn ein MCP nur für ein Projekt relevant ist (`cannaleo` für terpene, `terpene-pg` für terpene): gehört in `~/dev//mcp.json` + Skript im Projekt-Repo. Globale `~/.claude.json`/`~/.config/opencode/opencode.jsonc` = nur projekt-übergreifende Tools. -- **opencode-Skills: Default-Pfad kopieren, NICHT symlinken** (TP35 Schicht 6, 2026-08-28). Claude-Skill in `~/.claude/skills//SKILL.md` ist die Quelle; für opencode gehört er nach `~/.config/opencode/skills//SKILL.md` (`cp`, nicht `ln -s`). Begründung: opencode scannt diesen Pfad als Default — **kein** `skills.paths`-Eintrag in `~/.config/opencode/opencode.jsonc` nötig, das wäre redundant. Symlink wurde in der Inventur-Session testweise angelegt, hat Christophs "opencode = eigene Pfade"-Direktive verletzt und wurde wieder entfernt. Schema-Referenz: → `Config.skills.paths`. **Lehre aus dem Fall:** opencode-CLI-Verhalten nicht raten, Doku lesen — und ein "load-bearing"-Detail (Default-Scan-Pfad) zuerst verifizieren, bevor man ihn umgeht. -- **Skills sind IMMER global, niemals pro Projekt** (Christoph-Direktive 2026-08-28). `projektstatus` und alle übrigen Skills liegen **ausschließlich** unter `~/.claude/skills/` und `~/.config/opencode/skills/`. **Kein** Repo-lokaler Skill unter `.opencode/skill/` oder `.claude/skill/`. Begründung: keine Redundanz, keine Drift zwischen Skill-Versionen, eine Quelle pro Skill. **Merkregel:** wer einen Skill „überschreiben" will, editiert **die globale Datei** oder schreibt einen zweiten Skill mit anderem Namen — niemals ein Repo-Override. -- **Kommentar in Code ≠ Verhalten.** `mcp/shelly/.env` trug den Kommentar "gitignored, NICHT committen" — es gab aber **kein** `mcp/shelly/.gitignore`. Der Kommentar war Wunsch, nicht Wirklichkeit. `routeros/cloudflared/credentials.json` (Cloudflare-Tunnel-PrivateKey base64) lag ungegittoret im Working Tree. **Merkregel:** Behauptungen in Kommentaren/Konfig-Kommentaren durch `git check-ignore -v ` (oder `git status`) verifizieren, bevor man darauf vertraut. **Lessons-Block erweitert 2026-08-28 nach `commit`-Session.** -- **opencode-TUI: führendes Leerzeichen vor `/command` manchmal nötig** (Beobachtung 2026-08-28, Skill „projektstatus" getestet; **identifiziert als bekannter Bug**). Symptom: Tippen von `/projektstatus` führt nicht zum Skill, ` /projektstatus` (führendes Leerzeichen) schon. Doku zeigt den Aufruf ohne Space. **Verifiziert:** Issue **anomalyco/opencode#27528** „ACP: prompt starting with '/' is silently dropped if command name is not recognized" — Status **Not planned (skipped)**, closed 2026-07-14. Maintainer haben es also gesehen und nicht gefixt. **Konsequenz:** Workaround (führendes Leerzeichen) ist der offizielle Pfad; Skill selbst ist nicht kaputt. Vor jedem neuen opencode-Build prüfen, ob das Verhalten weg ist. -- **Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, wenn sie lang wird.** Befund 2026-08-28: der „Nächster Schritt (2026-08-26): TP31 Schritt 5"-Satz stand zwei Tage nach seiner Formulierung noch im Ist-Stand-Block, obwohl ich in derselben Zeit drei Folge-Sessions eröffnet hatte. **Merkregel pro Edit:** drei Stellen berücksichtigen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere; wer eine ändert, **muss** die anderen mitlesen. Strukturelle Antwort = TP36 (SSoT via Nautobot, Datei wird read-only Snapshot) — eigene Session, nicht nebenbei. **Lessons-Block erweitert 2026-08-28 nach Skill-Verifikation in opencode.** -- **`sed 's/=.*/=/'` maskiert NICHT, wenn das `.*` greedy ist und der Wert Slashes/Backslashes enthält** (Befund 2026-08-28, shelly-mcp-Deploy). Ich habe `sed 's/SHELLY_DEVICE_PW=.*//'` auf die Stack-YML angewendet — der **Wert selbst** wurde in den Output geschrieben, weil mein Replacement-String nicht greedy genug war und ich die Pipe nicht mit `cat -v` oder `head -15 | sed 's|=.*|=|'` abgesichert hatte. Folge: **`SHELLY_DEVICE_PW` ist jetzt als verbrannt zu betrachten** — wandert in **TP30 Posten #3** mit Hochstufung. **Merkregel:** bei jeder Ausgabe von Secret-haltigen Dateien entweder `head -c ` (Abbruch nach Bytes), oder die Datei vor dem Read mit `tr -d '\n' | sed 's|=.*|=|'` über einen Stream-Filter schicken — **niemals** einfach `cat` und auf einen Replacement-Pattern vertrauen. Vergleichbar zum PSK-Leak 2026-08-24. - -- **Vor jeder Drift-Bewertung `python3 nautobot/gc-transaktion.py status` lesen** (Befund 2026-08-31, - zweimal am selben Tag gestolpert). **Drift aus einem laufenden Versuch sieht identisch aus wie ein - Versehen** — die Compliance-Zahl unterscheidet nicht. Konkret: `crs326-core`/`wifi-security` - (`ft-over-ds=no`) wurde dem 7.24.1-Upgrade zugeschrieben und als „braucht eine Entscheidung" - weitergereicht; tatsächlich ist es **TP25 Fund E Hebel 1**, um 08:09 bewusst gesetzt, während der - Core erst um 14:36 gehoben wurde, geführt als offene Transaktion `TP25-fund-e-ft-over-ds`. - 🔴 **Eine solche Drift grün zu machen macht den Versuch unsichtbar.** Umgekehrt gilt: was **dauerhaft** - gelten soll (z.B. das `veth`/`masquerade` aus TP16.3 §3.4), gehört **nicht** in eine Transaktion, - sondern ins **Soll** — Transaktion = befristeter Versuch, Template = Dauerzustand. - ⚠️ Das Skript löst den Token-Pfad gegen die Repo-Wurzel auf und findet ihn **aus einem Worktree - heraus nicht** (`210-nautobot.yml` liegt nur im Haupt-Checkout) — die Transaktionsliste kommt - trotzdem, nur die Compliance-Spalte fehlt dann. -- **Maskierungs-Muster auf den SCHLÜSSELNAMEN ankern, nie auf ein Trennzeichen** (Befund 2026-08-31, - zweiter Fall dieser Klasse nach `SHELLY_DEVICE_PW`). `grep -n "TOKEN" datei | sed 's/:.*TOKEN/…/'` - maskiert **nicht**: `grep -n` stellt `:` voran, und das `:.*TOKEN` matcht diesen Präfix statt - den Wert — ein nachgeschaltetes `cut -c1-60` schneidet dann genau nichts weg. So sind zwei - Nautobot-Tokens ins Transkript geraten. **Sicher ist nur** - `sed -E 's/(TOKEN|PASSWORD|SECRET|KEY|PASS)=.*/\1=/'`. Und: **Werte nie suchen, um sie zu - sehen** — den Schlüssel per `sed -n 's/^…KEY=//p'` direkt in eine Variable holen, nie über `grep` - mit Ausgabe. Vergleichbar zum PSK-Leak 2026-08-24 und zum `sed`-Fall 2026-08-28. -- **Vor jedem Flash-Budget auf MikroTik `docker export | wc -c` messen, nicht `docker image inspect - .Size`** (Befund 2026-08-31, TP16.3). `.Size` meldete für `cloudflared` 27,5 MB, entpackt waren es - **69,5 MiB** — das offizielle Image passt damit auf keinen hAP/cAP. RouterOS braucht beim Import - **Tar UND entpacktes Abbild gleichzeitig**. 🔴 **Und `docker save` schreibt seit Docker 25 - (containerd-Store) OCI statt docker-v1** → RouterOS meldet `no config found in manifest`, was nach - kaputtem Image klingt und ein Formatproblem ist. Beides in `docs/tp16.3-bf-public-ap-connector-runbook.md` §2.2. +- **MCP-Erstellung/-Stilllegung = aktiv melden** (2026-08-27, Christoph-Direktive). Wer einen neuen MCP anlegt, ändert oder abschaltet — **meldet das proaktiv** an den User, nicht stillschweigend im Commit. Jede AI-Session ist zugleich **Oberhand über Container** und **Garbage-Collector** auf Infra-Ebene. +- **MCPs brauchen Host-Verifikation** vor Edit: per Live-Read (nc/ps/curl) prüfen, ob der Host noch antwortet. Beispiel: `shelly-mcp` zeigte auf `10.40.20.30:3030`, kein Host antwortet → `enabled=false`, dokumentieren. (Stand 2026-08-28: shelly-mcp auf DS923 `10.40.20.180` deployt.) +- **Tote MCP-Skripte → ins Projekt-Repo, nicht global.** Projekt-spezifische MCPs (`cannaleo`, `terpene-pg`) → `~/dev//mcp.json` + Skript im Projekt-Repo. Globale `~/.claude.json`/`opencode.jsonc` = nur projekt-übergreifende Tools. +- **opencode-Skills: Default-Pfad kopieren, NICHT symlinken** (TP35 Schicht 6). Claude-Skill `~/.claude/skills//SKILL.md` ist die Quelle; für opencode `cp` nach `~/.config/opencode/skills//SKILL.md` (kein `skills.paths`-Eintrag nötig). Symlink verletzt Christophs „opencode = eigene Pfade"-Direktive. Lehre: opencode-Verhalten nicht raten, Doku lesen. +- **Skills sind IMMER global, niemals pro Projekt** (Christoph-Direktive). Nur `~/.claude/skills/` und `~/.config/opencode/skills/`. Kein Repo-Override. Wer überschreiben will, editiert die globale Datei oder schreibt einen zweiten Skill. +- **Kommentar in Code ≠ Verhalten.** `mcp/shelly/.env` trug „gitignored", es gab aber kein `.gitignore`. Behauptungen in Kommentaren per `git check-ignore -v ` verifizieren, bevor man darauf vertraut. +- **opencode-TUI: führendes Leerzeichen vor `/command` manchmal nötig** (bekannter Bug, anomalyco/opencode#27528, „Not planned"). Workaround (führendes Leerzeichen) ist der offizielle Pfad. +- **Manuell gepflegte Doku verliert gegen die Realität, wenn sie lang wird.** Wer eine Stelle ändert, muss die anderen mitlesen (Quick-Index, TP-Abschnitt, Ist-Stand). Strukturelle Antwort = TP36 (SSoT via Nautobot). +- **Maskierungs-Muster: auf den SCHLÜSSELNAMEN ankern, nie auf ein Trennzeichen.** Sicher ist nur `sed -E 's/(TOKEN|PASSWORD|SECRET|KEY|PASS)=.*/\1=/'` — greedy `sed 's/=.*/…/'` schneidet Slashes/Backslashes nicht. **Werte nie suchen, um sie zu sehen** — den Schlüssel per `sed -n 's/^…KEY=//p'` direkt in eine Variable holen. Drei Leaks dieser Klasse: PSK 2026-08-24, `SHELLY_DEVICE_PW` 2026-08-28, Nautobot-Tokens 2026-08-31. +- **Vor jeder Drift-Bewertung `python3 nautobot/gc-transaktion.py status` lesen** (Befund 2026-08-31, zweimal gestolpert). **Drift aus einem laufenden Versuch sieht identisch aus wie ein Versehen** — die Compliance-Zahl unterscheidet nicht. 🔴 **Eine solche Drift grün zu machen macht den Versuch unsichtbar.** Was dauerhaft gelten soll, gehört ins **Soll/Template**, nicht in eine Transaktion. ⚠️ Das Skript findet den Token aus einem Worktree heraus nicht (`210-nautobot.yml` nur im Haupt-Checkout). +- **Vor jedem Flash-Budget auf MikroTik `docker export | wc -c` messen, nicht `docker image inspect .Size`** (Befund 2026-08-31, TP16.3). `.Size` meldete für `cloudflared` 27,5 MB, entpackt **69,5 MiB**. 🔴 **`docker save` schreibt seit Docker 25 OCI statt docker-v1** → RouterOS meldet `no config found in manifest`. Beides in `docs/tp16.3-bf-public-ap-connector-runbook.md` §2.2. **Checkliste vor jedem Edit-Vorschlag:** @@ -426,4 +184,4 @@ Verhaltensregeln für nachfolgende AI-Sessions — kleiner, klarer, weniger Drif 3. Habe ich **eine** Fragenrunde mit allen offenen Annahmen gestellt? 4. Habe ich für jeden Edit **deine** Bestätigung eingeholt (nicht nur für den Plan-Output)? 5. **MCP-Änderung?** Vorher Live-Check (Port/Prozess), aktiv melden, Garbage-Collector-Pflicht. -6. **Output-Länge.** Christoph hat sich heute (2026-08-28) wiederholt in langen Outputs verirrt. **Merkregel:** eine Antwort pro Edit-Vorschlag, eine Zeile pro Bestätigung, keine Wiederholung dessen, was im Diff steht. Keine Frage-Runden, wenn keine echte Annahme kollidiert. Erklärungstexte unter 6 Zeilen, sofern nicht ausdrücklich verlangt. +6. **Output-Länge.** Christoph hat sich wiederholt in langen Outputs verirrt. **Merkregel:** eine Antwort pro Edit-Vorschlag, eine Zeile pro Bestätigung, keine Wiederholung dessen, was im Diff steht. Keine Frage-Runden, wenn keine echte Annahme kollidiert. Erklärungstexte unter 6 Zeilen, sofern nicht ausdrücklich verlangt. diff --git a/docs/gotchas.md b/docs/gotchas.md new file mode 100644 index 0000000..b9d6544 --- /dev/null +++ b/docs/gotchas.md @@ -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 `: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 "`) 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 +` 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 `/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).