23 Commits

Author SHA1 Message Date
08a1aaa80d SSoT: Status-Marker aus den TP-Headern entfernt, Sync-Werkzeug auf Anlege-Richtung festgelegt
Die `### TPnn`-Header trugen weiterhin Status-Anspruche (▸ ✅ FERTIG …, ▸ ⏳ CUTOVER …),
obwohl Index/Status seit dem TP36-Cutover in Gitea liegt. Das war eine dritte Statusquelle,
die den Labels nachlief — Header-Marker datierten teils Wochen vor dem Issue-Update.

- 75 Header (32 aktiv + 43 Archiv) auf `### TPnn · Gitea #N — <Titel> — <Caveat>` umgestellt.
  Caveats, Warnungen, Querverweise und Archiv-Links bleiben erhalten, nur der Status faellt weg.
  Status-Marker im Fliesstext (Messchronik, Unterpunkte) bleiben — sie beschreiben Historie.
- TP47 fehlte als Gitea-Issue komplett (nur in der Datei seit 808d3a1) -> als #74 angelegt.
- bin/gitea-tp-sync.py schrieb in die falsche Richtung: der Abgleich leitete den Zielzustand
  aus dem Datei-Marker ab (fest ✅) und haette #24 (TP34, bewusst status:dropped) auf status:done
  gezogen. Jetzt werden nur noch FEHLENDE Archiv-Issues angelegt, bestehende nie angefasst.
- CLAUDE.md: Header-Form + Werkzeug-Richtung festgehalten.
- Announcement 2026-10-03 für die naechste Session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 13:12:26 +02:00
808d3a168b TP47: Core egress via cf-mesh und WARP-Exit-Validierung 2026-09-27 16:10:17 +02:00
3ae6399b93 docs: WARP-Egress-Versuch dokumentiert — Cloudflare blockt Consumer-WARP-WG-Clients
Consumer-WARP über natives RouterOS-WG gebaut, verifiziert, zurückgebaut. Befund: Handshake rx=0, Cloudflare blockt inoffizielle WG-Clients (reserved/Client-ID + Abuse-Prevention). Weg: Dedicated Egress.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-26 09:04:18 +02:00
528f133264 docs: Plan WARP-Egress (nativer Consumer-WARP als WG auf crs326-core)
Routing-Regel statt Mangle: Client-VLANs -> Routing-Tabelle warpout,
Router-Infra (wg-hub/cf-mesh/DNS/NTP) bleibt auf Liwest. Nur Plan, nichts ausgeführt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-25 19:24:13 +02:00
27a9977115 CLAUDE.md: Skills sind jetzt versioniert (admin/claude-skills)
Die 22 globalen Skills waren reiner Home-Ordner-State - kein Git, kein
Backup. Master ist jetzt das Gitea-Repo admin/claude-skills, ~/.claude/skills
ist dessen Arbeitsbaum. Sync-Ablauf inkl. cp-Schritt nach opencode festgehalten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-25 09:02:39 +02:00
6601df5a1b TP30 #12: Konsumenten-Inventur des OR-Keys korrigiert (live geprüft)
Die 3P-Config 8f69f2f1 traegt nur den CCR-Gateway-Token, nicht den
OR-Key. Der OR-Key lebt in config.sqlite live + in der nur gespeicherten
e1219ea4. Rotation muss 8f69f2f1 unangetastet lassen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-25 08:55:54 +02:00
c0c63882e4 TP30: Posten #12 — OpenRouter-Key-Leak, Merkregel JSON-statt-Env
Der Key geriet ins Transkript, weil das `sed` auf den geratenen Namen
`apiKey` ankerte statt auf `inferenceGatewayApiKey`. Neue Unterklasse der
bekannten Familie: Quelle war JSON, nicht Env — das geteilte KEY=VALUE-
Snippet kann `"key": "…"` nicht treffen. Merkregel ergaenzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-24 13:58:26 +02:00
230f052a01 TP30: Posten #10/#11 — zwei Transkript-Leaks der Env-Session nachgetragen
Beim Einlesen von ~/.secrets/exports.zsh griffen zwei Feld-Maskierungen nicht:
N8N_MCP_BEARER (Alternation ohne BEARER, Schluessel endet ohne "=") und
TERPENE_RO_URL (Passwort steckt im URL-Userinfo-Teil, keine "="-Ankerstelle).
Beide als neue Zeilen im Sammelbecken, gebuendelt wie die uebrigen Posten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-22 19:46:28 +02:00
39e19e7f0f docs: Handoff — Umbau-Vorschlag für Doku-/TP-Verwaltung (Analyse-Basis)
Drei driftende Wahrheitsquellen (Gitea-Issue, teilprojekte.md, CLAUDE.md),
Standard-vs-Eigenbau, drei Vorschläge. Basis für eine separate Session,
die die Umstellung selbstständig sucht und umsetzt.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-22 14:22:31 +02:00
be51f12ab9 TP25: Roll-up — Kondensat nach teilprojekte-archiv.md (FERTIG)
TP25 ist abgeschlossen (Platzierung final, Fund E geschlossen, Gitea #17 closed).
Der aktive Stand (Kondensat, ~507 Zeilen) wandert ins Archiv als neue Sektion
## TP25 — Kondensat (Anchor #tp25); in teilprojekte.md bleibt ein Pointer.
Archiv-Index + "Dazu am Ende"-Notiz + Langfassung-Header ("nicht abgeschlossen"
→ abgeschlossen) nachgezogen.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-22 13:14:49 +02:00
769d0ac6b3 TP25: Stand-Block + Header auf FERTIG (Platzierung final, vier Offen-Punkte aufgelöst)
Die vier "Offen"-Punkte aus dem 09-15-Zuordnungs-Block waren am selben Tag
im Steering-Handoff §6 aufgelöst (ap1 bleibt, Tausch-Korrektur verworfen,
Drei-Räume verworfen, Doku-Drift geschlossen), aber nie zurückgeschrieben.
Header ⏳→✅ FERTIG, Gitea #17 geschlossen.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-22 13:11:10 +02:00
4a63c1e527 TP34.2: C4 → interne DNS-Namen als Daten (Gitea #73)
Träger für TP45 Schicht C4. TP34 (Caddyfile-Rendering) bleibt verworfen (#24);
der Daten→Config-Gedanke lebt als TP34.2 weiter. Prosa-Abschnitt + Anker angelegt,
Schicht-C-Tabelle / „Offen in C" / Abh. / Follow-up / TP37-Abgrenzung auf TP34.2
umgestellt, Archiv-Zeiger ergänzt.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-22 13:06:39 +02:00
52a469185b 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>
2026-09-22 13:05:56 +02:00
2f3d44d4be docs: Handoff für CLAUDE.md-Ausdünnen (strukturelle Hygiene)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-22 12:48:11 +02:00
fe11ae2a5c TP25: Merkregel — no cached PMK ≠ FT-Problem
Tritt auch auf ft=no-SSIDs (IOT) auf, harmloser PMK-Cache-Miss.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-22 12:48:11 +02:00
7b365e1586 TP45 Doku: Schicht C (FQDN) als durch dokumentiert, Pflichtlektuere nachgezogen
- docs/teilprojekte.md: Schicht-C-Zeile auf C1-C3 durch; neuer Abschnitt mit
  C1 (27 Core-Statics, Job tp45-c-dns), C2 (AGH entdoppelt), C3 (ds-bf ueber
  Caddy), WARP-Abnahme, sowie den zwei offen gelassenen Befunden (AAAA-Leak,
  AGH-Doppeladresse) und der dig-Messfalle. ap3.bf-Bullet korrigiert.
- CLAUDE.md: Pflichtlektuere Block 0 beschrieb noch das ungeloeste FQDN-Problem.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-22 12:31:59 +02:00
6d1d56e463 TP45 Schicht C: FQDN-Wiederherstellung (C1-C3)
C1 - Der Core haelt jetzt 27 /ip dns static (vorher 10). Die 12 toten Namen
loesen wieder auf: 9 Caddy-Frontends, tv.bf/mfc.bf, das bisher nirgends
existierende ap3.bf, dazu pve.bf und pve-mcp.bf von der Host-IP auf .40 und
die 5 bare Namen. Neuer GC-Job tp45-c-dns; er liest die Zeilen aus dem
Template selbst und macht daraus einen idempotenten Upsert.

C2 - AGH entdoppelt: upstream_dns nur noch 10.40.20.1, alle 26 Rewrites raus.
Der Core ist damit die einzige Quelle. /admin ist lokal wieder erreichbar
(header-frei 200, mit CF-Connecting-IP weiter 404 - der Guard bleibt zu).

C3 - ds-bf-Ingress-Origin auf https://10.40.20.40:443 + SNI (CF-Write).
Oeffentlich 200 in 0,24 s statt 000 nach 20 s.

Abnahme: Geraete-Read (27 Statics), Compliance 145/145 / 0 Drifts, dig vom
Mac fuer alle Namen, /admin lokal 200, ds-bf oeffentlich 200, Filter
(doubleclick -> 0.0.0.0) intakt, vw/hass/n8n/gitea unveraendert.

Offener Befund: der Core reicht AAAA weiter und liefert damit fuer die bare
Namen die CF-IPv6. Heute wirkungslos (kein globales IPv6 im Netz), aber
latent - bewusst dokumentiert statt geaendert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-22 12:16:56 +02:00
4720b5d223 Redaktionsschaden beheben: private_networks in beiden AGH-YAMLs
Commit 3eb03ae (2026-09-14, "Plaintext-Secrets entfernen") hat in beiden
Dateien `private_networks: []` durch `private_<redacted>: []` ersetzt -- ein
Redaktionswerkzeug hat den Schluesselnamen fuer einen Secret-Wert gehalten.

Folge: `adguard/AdGuardHome.yaml` ist die SSoT, die `agh-sync.py push` in beide
Live-Instanzen schreibt -- der naechste Push haette einen unbekannten Schluessel
in die AGH-Config gebracht (private_networks faellt dann auf Default zurueck,
oder der Parser lehnt die Config ab).

Wiederhergestellt in
  adguard/AdGuardHome.yaml:74
  docs/transaktionen/agh-bf1-AdGuardHome.snapshot.yaml:87

Abnahme: Repo-SSoT vs. live .36 -> 498/498 Zeilen identisch (ohne password-Zeilen).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-21 23:06:30 +02:00
3f774b7b17 TP45 Schicht B2 Ruecknahme: VLAN30 vollstaendig auf den Stand vor dem Umzug
Zwei Christoph-Direktiven am 2026-09-21, in dieser Reihenfolge:
1. "mach die shellys sofort rueckgaengig" -- HA bindet sie per IP ein.
2. "wir sollten alle vlan30-aenderungen rueckgaengig machen" -- Sperr-Risiko
   Nuki: ein Fehlgriff haette aussperren koennen.

Neu in tp45-leases.py: die Tabellen SHELLY/REVERT und das Kommando `revert`
(nur die 26 Shellys) sowie `restore` (alles zurueck auf vor B2, inkl.
NSPanelPro-Reservierung und wiederangelegter CCU-Zeile). In gc-configplan.py
die Jobs tp45-v30-revert und tp45-v30-restore; _lease_move nimmt jetzt ein
tag, damit die :log-Zeilen den Job unterscheidbar machen.

Abnahme: Geraete-Read (alle 36 Adressen zurueck, NSPanelPro ohne Reservierung,
CCU .232 wieder da) und --refresh-compliance 145/145, 0 Drifts.

Zwei Funde, die als Merkregel bleiben: `make-dynamic` gibt es in RouterOS 7.24
nicht (der Rueckweg ist `remove`), und der GC-Plan-Status beweist nichts --
beide Laeufe meldeten Failed, waehrend der Job-:log alle 36 Statements
protokollierte.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-21 22:22:22 +02:00
180c5b6c57 TP45 Schicht E: Umzug laeuft ueber den Core, nicht ueber Doppeladresse am Host
Neue IPs .110/.121/.130 werden zuerst am Core generiert (Sekundaer-IP auf
vlan20 + 1:1-NAT aufs Geraet), gemessen wird ab Tag 1 mit zwei Zaehlern
(neue IP = wer ist umgestellt, Alt-IP = ist noch was los). Das Geraet wird
erst angefasst, wenn seine Alt-IP still ist ("siedeln"). Alles ins Soll,
nicht als Transaktion. NAS siedelt zuletzt und gemeinsam.

Warum NAT statt Routing: im selben Subnetz antwortet das Geraet dem Client
direkt per L2 -- er verwirft die Antwort von .10 auf eine Verbindung zu .110.

Vorabcheck: NAS-NFS/iSCSI sind IP-ACL'd, aber ohne lebende Nutzer (pve hat
null NAS-Mounts) -- der src-nat-Umweg bricht nichts. Nebenbefund: die
Exports stehen auf totem 10.40.41.x plus drei 10.0.0.0/8-Freigaben.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-21 21:29:38 +02:00
7b03e01fab TP30 #4: "faellig" -> "Anlass" — kein Vorgezogen-Signal mehr
Nach dem Streichen der Ausnahme durfte die Wortwahl nicht mehr nach
sofortiger Einzelrotation klingen. Posten #4 ist anlass- aber weiterhin
gebunden; beide Stellen sagen das jetzt explizit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-21 17:43:37 +02:00
4e61c63ae7 TP30: "harte Ausnahme fuer neue Leaks" gestrichen
Die Klausel zog bei einem neuen Leak eine sofortige Einzelrotation vor und
widersprach damit dem Buendelungszweck des TPs (Kostentreiber sind die
Konsumenten, nicht das Setzen) und der gelebten Praxis: auch die
leak-getriebenen Posten #1 und #5-#9 blieben gebuendelt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-21 17:43:21 +02:00
08bbe1830f TP30 #4: CF_API_TOKEN-Leak 2026-09-21 nachgezogen + falsches Ablaufdatum korrigiert
Der Posten hing an einem harten Termin ("laeuft am 2027-01-09 ab"), der nicht
mehr existiert: dieses Datum gehoerte zum laengst revokten Alt-Token
9984078812bc..., waehrend das live CF_API_TOKEN (id 493ed8bd...) gar kein
Ablaufdatum hat (expires_on: null, verifiziert ueber /accounts/{id}/tokens/verify).

Damit ist die Rotation anlass- statt termingetrieben — Ausloeser ist jetzt der
Leak vom 2026-09-21 (Wert in einer Session-Ausgabe sichtbar geworden).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-21 16:58:53 +02:00
16 changed files with 1784 additions and 1073 deletions

429
CLAUDE.md
View File

@@ -1,58 +1,34 @@
# 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 = aktuell dringendster Posten (2026-09-21, Gitea #71):** interne `*.bf.emsenhuber.net`
lösen im Browser **nicht** auf (AGH wertet Rewrites im **Filter-Pfad** aus → für unfiltrierte
Clients übersprungen → Fallback auf den Core, der nur 10 von 26 Namen hält), und `ds-bf` ist
**öffentlich kaputt** (einziger Ingress-Eintrag, der Caddy umgeht → TP5-Drop). Reihenfolge:
VLAN20 ins TP32-Schema → VLAN30-Pool-Grenze → DNS/FQDN. Ausführlich: `### TP45`.
**`### 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)
- **SSoT-Dreiteilung:** **Index/Status = Gitea-Issues** (`admin/bf-infra`, Labels `status:*`/`thema:*`) ·
**Prosa/Detail = Repo** (`docs/teilprojekte.md` aktiv + `teilprojekte-archiv.md`, `### TPnn`-Abschnitte) ·
**Live-Gerätedaten = Nautobot** (Compliance-Zahl, RouterOS-Version …). Ein Status ändert sich **in Gitea**,
nicht mehr in einer Repo-Datei. Sync-/Abgleich-Werkzeug fürs Archiv: `bin/gitea-tp-sync.py` (Dry-Run default,
nicht mehr in einer Repo-Datei. Archiv-Backfill-Werkzeug `bin/gitea-tp-sync.py` (Dry-Run default,
Token aus `~/.hermes/.env`, **nie ausgeben**).
- 🔴 **Die TP-Header in beiden Dateien tragen KEINEN Status mehr** (seit 2026-10-03). Form:
`### TP25 · Gitea #17 — <Titel> — <Caveat>`. Der `· Gitea #N`-Zeiger ist die Brücke zum Issue.
Wer einen Status sucht, liest das **Label im Board** — nicht den Datei-Kopf. Der alte `▸ ✅ FERTIG …`-Anspruch
war eine dritte Statusquelle und ist entfernt; die Header-Marker waren teils älter als die Issues.
⚠️ **Status-Marker im Fließtext** (in `└─`-Zeilen, Unterpunkten, Messchronik) bleiben erlaubt — sie
beschreiben historische Ereignisse, keinen aktuellen Stand.
- 🔴 **`bin/gitea-tp-sync.py` schreibt nur noch NEUE Issues** für Archiv-TPs, die im Board fehlen.
Existierende Issues werden **nie** angefasst — kein Label-, kein State-Patch. Die Vorgängerversion
glich bestehende Issues gegen den Datei-Marker ab und zog so #24 (`status:dropped` → `done`) und
#32 auf falsche Labels. **Richtung Datei → Gitea ist nur fürs *Anlegen* zulässig, nie für *Status*.**
- **Branch-Konvention:** Branch `tpNN-kurz` **jederzeit** mitten in der Session möglich (`git switch -c`,
uncommittete Arbeit wandert mit); für Multi-Session-TPs der Normalfall. **Merge nach `main`, sobald der
Code/Config des TP steht.** Weil Steuerungs-Updates nach **Gitea** gehen (nicht in `teilprojekte.md`),
@@ -63,15 +39,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)
@@ -85,7 +61,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`).
---
@@ -108,264 +84,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@<ip> "/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@<ip> "/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 <name>` 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 <fehlende-datei> /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=<deine-IP> 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 `<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.
- 🏠 **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).
- 📜 **~~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=<deine-IP> 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.
---
@@ -377,43 +175,18 @@ 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/<projekt>/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/<name>/SKILL.md` ist die Quelle; für opencode gehört er nach `~/.config/opencode/skills/<name>/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: <https://opencode.ai/config.json> → `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 <datei>` (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 <https://opencode.ai/docs/commands> 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/=.*/=<set>/'` 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=.*/<set>/'` 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|=.*|=<set>|'` 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 <n>` (Abbruch nach Bytes), oder die Datei vor dem Read mit `tr -d '\n' | sed 's|=.*|=<set>|'` ü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 `<zeile>:` 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=<redacted>/'`. 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/<projekt>/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/<name>/SKILL.md` ist die Quelle; für opencode `cp` nach `~/.config/opencode/skills/<name>/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.
- **Skills sind seit 2026-09-25 versioniert** (Befund: sie waren reiner Home-Ordner-State, kein Git, kein Backup — bei 22 Skills über mehrere Monate gewachsen). Master = Gitea **`admin/claude-skills`** (privat), `~/.claude/skills/` ist dessen **Arbeitsbaum**. **Ablauf ab jetzt: editieren → committen → pushen → `cp -R ~/.claude/skills/<name> ~/.config/opencode/skills/<name>`.** ⚠️ Der `cp`-Schritt ist der, den man vergisst — genau so entsteht stille Drift zwischen den beiden Kopien. Secret-Gebot gilt auch hier: keine Tokens/Keys in Skills, Platzhalter (`REPLACE_WITH_…`) sind okay.
- **Kommentar in Code ≠ Verhalten.** `mcp/shelly/.env` trug „gitignored", es gab aber kein `.gitignore`. Behauptungen in Kommentaren per `git check-ignore -v <datei>` 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=<redacted>/'` — 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:**
@@ -422,4 +195,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.

View File

@@ -31,10 +31,11 @@ dns:
ratelimit_subnet_len_ipv6: 56
ratelimit_whitelist: []
refuse_any: true
# TP45 C2 (2026-09-22): der Core ist der EINZIGE Upstream — AGH macht nur noch
# Filtering. Die interne Zone und die bare Namen kommen damit aus derselben Quelle
# wie fuer jeden anderen Client. DoH nach draussen macht allein der Core.
upstream_dns:
- https://px35ynlol3.cloudflare-gateway.com/dns-query
- '[/bf.emsenhuber.net/]10.40.20.1'
- '[/40.10.in-addr.arpa/]10.40.20.1'
- 10.40.20.1
upstream_dns_file: ""
bootstrap_dns:
- 1.1.1.1
@@ -71,7 +72,7 @@ dns:
ipset_file: ""
bootstrap_prefer_ipv6: false
upstream_timeout: 10s
private_<redacted>: []
private_networks: []
use_private_ptr_resolvers: true
local_ptr_upstreams: []
use_dns64: false
@@ -158,85 +159,9 @@ filtering:
blocking_mode: default
parental_block_host: family-block.dns.adguard.com
safebrowsing_block_host: standard-block.dns.adguard.com
rewrites:
- domain: pbs.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: portainer.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: pve.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: gitea-mcp.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: n8n-mcp.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: ha-mcp.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: pve-mcp.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: z2m25.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: iobroker.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: ccu.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: nautobot.bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: vw.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: hass.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: n8n.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: gitea.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: ds-bf.emsenhuber.net
answer: 10.40.20.40
enabled: true
- domain: nas.bf.emsenhuber.net
answer: 10.40.20.10
enabled: true
- domain: macmini.bf.emsenhuber.net
answer: 10.40.20.167
enabled: true
- domain: mfc.bf.emsenhuber.net
answer: 10.40.10.6
enabled: true
- domain: tv.bf.emsenhuber.net
answer: 10.40.40.11
enabled: true
- domain: docker.bf.emsenhuber.net
answer: 10.40.20.126
enabled: true
- domain: platform-pg.bf.emsenhuber.net
answer: 10.40.20.20
enabled: true
- domain: core.bf.emsenhuber.net
answer: 10.40.99.1
enabled: true
- domain: ap1.bf.emsenhuber.net
answer: 10.40.99.11
enabled: true
- domain: ap2.bf.emsenhuber.net
answer: 10.40.99.12
enabled: true
- domain: branch.bf.emsenhuber.net
answer: 10.40.99.2
enabled: true
# TP45 C2 (2026-09-22): alle 26 Rewrites entfernt — sie waren die zweite, stille
# Definitionsquelle. Der Core haelt jeden dieser Namen als /ip dns static.
rewrites: []
safe_fs_patterns:
- /opt/AdGuardHome/userfilters/*
max_http_size: 256MB

View File

@@ -1,22 +1,28 @@
#!/usr/bin/env python3
"""
TP36 (reframed) — teilprojekte.md Index/Status → Gitea Issues (admin/bf-infra).
TP36 (reframed) — Archiv-Backfill: fehlende Archiv-TPs als Gitea-Issues anlegen.
Idempotenter Sync (get-or-create, Match ueber "TPnn" im Titel):
- Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die in Gitea
NICHT offen sind; ein reopentes TP mit Archiv-Zeiger bleibt offen).
- Labels status:* (Marker) + thema:* (Gruppierung) werden vorab get-or-create.
🔴 RICHTUNG (seit 2026-10-03): **Gitea ist die SSoT fuer Index/Status.** Dieses Werkzeug
liest die Datei NUR, um zu pruefen, ob jedes archivierte TP ein Issue hat. Es schreibt
**ausschliesslich** NEUE Issues fuer noch nicht existierende Archiv-TPs — existierende
Issues werden NIE angefasst (kein Label-, kein State-Patch).
Warum das wichtig ist: die Vorgaenger-Version glich auch bestehende Issues ab und leitete
den Zielzustand aus dem Datei-Marker ab (fest `✅`). Damit konnte sie ein bewusst gesetztes
Label ueberschreiben — so wurde #24 (TP34, `status:dropped`) auf `status:done` und #32
(TP43) auf ein falsches `thema:` gezogen. Die Richtung Datei → Gitea fuer *Status* ist
seit dem TP36-Cutover (2026-09-07) falsch; sie bleibt hier nur fuer das *Anlegen* erhalten.
Abgrenzung:
- Aktive TPs: werden DIREKT als Gitea-Issue gepflegt (nicht hier).
- Status/Thema/State bestehender Issues: aendert nur der Mensch im Board.
- Per-TP-Prosa: bleibt in docs/teilprojekte.md / teilprojekte-archiv.md.
SSoT-Modell (seit TP36-Cutover 2026-09-07): Gitea fuehrt Index+Status — aktive TPs werden
DIREKT als Gitea-Issue angelegt/gepflegt, NICHT mehr aus einer Datei-Tabelle abgeleitet
(die Quick-Index-Tabelle wurde aus teilprojekte.md entfernt). Dieses Werkzeug haelt nur noch
die ARCHIVIERTEN (closed) TPs mit teilprojekte-archiv.md idempotent abgeglichen und fasst
offene Issues nie an. Die per-TP-Prosa bleibt in docs/teilprojekte.md / teilprojekte-archiv.md.
Token: ~/.hermes/.env (GITEA_ACCESS_TOKEN, GITEA_HOST) — wird NIE ausgegeben.
Aufruf:
python3 bin/gitea-tp-sync.py # Dry-Run (default) — zeigt, was passieren wuerde
python3 bin/gitea-tp-sync.py --commit # legt Labels + Issues an / gleicht sie ab
python3 bin/gitea-tp-sync.py # Dry-Run (default) — zeigt, was fehlt
python3 bin/gitea-tp-sync.py --commit # legt fehlende Archiv-Issues an
"""
import json
import os
@@ -33,16 +39,6 @@ HERMES = Path.home() / ".hermes" / ".env"
OWNER_REPO = "admin/bf-infra"
COMMIT = "--commit" in sys.argv
# Marker -> (status-label, gitea-state)
MARKER = {
"🔜": ("status:todo", "open"),
"⏳": ("status:wip", "open"),
"⏸️": ("status:paused", "open"),
"💡": ("status:idea", "open"),
"🟢": ("status:concept", "open"),
"✅": ("status:done", "closed"),
"❌": ("status:dropped", "closed"),
}
LABEL_COLORS = {
"status:todo": "#ededed", "status:wip": "#fbca04", "status:paused": "#c5def5",
"status:idea": "#d4c5f9", "status:concept": "#bfdadc", "status:done": "#0e8a16",
@@ -105,23 +101,31 @@ def api(method, path, body=None):
# --------------------------------------------------------------------------- parse
def parse_archiv(exclude_nums):
"""Archiv-Header -> archivierte TPs. exclude_nums = TP-Nummern, die in Gitea OFFEN sind
(reopente TPs mit Archiv-Zeiger) -> die bleiben offen, nicht als closed abgleichen."""
"""Archiv-Header -> archivierte TPs, die ein Issue bekommen SOLLEN.
Header-Form seit 2026-10-03: `### TP25 · Gitea #17 — WLAN-Stabilisierung — …`
Der Status-Anspruch (frueher `▸ ✅ FERTIG …`) ist entfernt; die Datei traegt keinen
Status mehr. Der Gitea-Zeiger `· Gitea #N` ist die Bruecke, falls er schon da ist.
exclude_nums = TP-Nummern, die in Gitea OFFEN sind -> die brauchen kein neues Issue.
"""
out = []
hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\s*—\s*(.+?)\s*▸", )
hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\b\s*(?:·\s*Gitea\s*#(\d+)\s*)?—\s*(.+?)\s*$")
seen = set()
for line in ARCHIV.read_text().splitlines():
m = hdr.match(line)
if not m:
continue
tp, title = m.groups()
tp, issue_no, title = m.groups()
num = tp[2:]
if num in exclude_nums or num in seen:
continue
seen.add(num)
short = re.sub(r"\*\*|`", "", title).strip()[:80]
out.append({"tp": tp, "num": num, "marker": "✅",
"title": short, "date": "", "src": "archiv"})
# Caveat hinter dem zweiten ` — ` abschneiden; nur der Kurztitel gehoert ins Issue.
short = re.split(r"\s+—\s+", title)[0]
short = re.sub(r"\*\*|`", "", short).strip()[:80]
out.append({"tp": tp, "num": num, "issue": issue_no,
"title": short, "src": "archiv"})
return out
@@ -173,43 +177,44 @@ def main():
print(f"Geparst: {len(items)} archivierte TP (aktive TPs = SSoT in Gitea: "
f"{len(open_nums)} offen, nicht angetastet)\n")
created = updated = 0
created = skipped = 0
for it in items:
status_label, state = MARKER.get(it["marker"], ("status:todo", "open"))
# Archiv-TPs sind abgeschlossen -> neu angelegte Issues gehen direkt auf closed.
status_label, state = "status:done", "closed"
labels = [status_label]
th = THEME.get(it["num"])
if th:
labels.append(f"thema:{th}")
title = f"{it['tp']} — {it['title']}"
body = (f"Steuerungs-SSoT: Index/Status hier in Gitea; **Prosa/Detail** im Repo unter "
f"`docs/{'teilprojekte-archiv.md' if it['src']=='archiv' else 'teilprojekte.md'}` "
f"(Abschnitt `{it['tp']}`).\n\n"
f"- Status: {it['marker']} ({status_label})\n"
+ (f"- Abschluss: {it['date']}\n" if it["date"] else ""))
f"`docs/teilprojekte-archiv.md` (Abschnitt `{it['tp']}`).\n\n"
f"- Status: ✅ ({status_label})\n"
f"- Angelegt aus dem Archiv-Backfill (`bin/gitea-tp-sync.py`).\n")
want_ids = [label_id[l] for l in labels if l in label_id]
cur = by_tp.get(it["tp"])
if cur is None:
print(f" [{'+' if COMMIT else 'DRY'}] NEW {title} {labels} state={state}")
if COMMIT:
sc, res = api("POST", f"/repos/{OWNER_REPO}/issues",
{"title": title, "body": body, "labels": want_ids})
if state == "closed" and isinstance(res, dict) and res.get("number"):
api("PATCH", f"/repos/{OWNER_REPO}/issues/{res['number']}", {"state": "closed"})
created += 1
else:
have = {l["name"] for l in cur.get("labels", [])}
drift = (have != set(labels)) or (cur.get("state") != state)
if drift:
print(f" [{'~' if COMMIT else 'DRY'}] UPD #{cur['number']} {title} "
f"{sorted(have)}->{labels} state {cur.get('state')}->{state}")
if COMMIT:
api("PATCH", f"/repos/{OWNER_REPO}/issues/{cur['number']}",
{"labels": want_ids, "state": state})
updated += 1
if cur is not None:
# 🔴 Existierendes Issue: NICHT anfassen. Weder Label noch State noch Body.
# Status/Thema aendert nur der Mensch im Board (Gitea = SSoT seit TP36-Cutover).
skipped += 1
continue
# Fehlt das Issue, obwohl der Header schon einen Zeiger traegt? -> Zeiger ist stale.
hint = f" ⚠ Datei-Header nennt bereits Gitea #{it['issue']} — Issue fehlt!" if it.get("issue") else ""
print(f" [{'+' if COMMIT else 'DRY'}] NEW {title} {labels} state={state}{hint}")
if COMMIT:
sc, res = api("POST", f"/repos/{OWNER_REPO}/issues",
{"title": title, "body": body, "labels": want_ids})
if isinstance(res, dict) and res.get("number"):
api("PATCH", f"/repos/{OWNER_REPO}/issues/{res['number']}", {"state": "closed"})
print(f" -> #{res['number']} angelegt (closed). Zeiger in "
f"docs/teilprojekte-archiv.md nachtragen: `· Gitea #{res['number']}`")
else:
print(f" !! Anlegen fehlgeschlagen: {res}")
created += 1
print(f"\n{'Angelegt' if COMMIT else 'Wuerde anlegen'}: {created} · "
f"{'Abgeglichen' if COMMIT else 'Wuerde abgleichen'}: {updated}")
f"unveraendert gelassen (SSoT = Gitea): {skipped}")
if not COMMIT:
print("Dry-Run — nichts geschrieben. Mit --commit ausfuehren.")

View File

@@ -13,6 +13,24 @@ zeigt die Einträge, die seit dem letzten `/news`-Lauf dazugekommen sind (Rest a
---
## 2026-10-03 — TP-Header tragen keinen Status mehr (SSoT-Drift geschlossen)
**Betrifft:** alle, die `docs/teilprojekte.md` oder `teilprojekte-archiv.md` lesen oder editieren, und
alle, die ein TP anlegen/abschließen.
**Aktion:** Die `### TPnn`-Header haben die Form `### TP25 · Gitea #17 — <Titel> — <Caveat>`.
Der Status-Anspruch (`▸ ✅ FERTIG (2026-09-15)`, `▸ ⏳ CUTOVER …`) ist **entfernt** — er war eine
dritte Statusquelle neben Issue und Prosa und lief den Labels nachweislich hinterher. **Wer einen
Status braucht, liest das `status:*`-Label im Gitea-Board**, nicht den Datei-Kopf — `· Gitea #N`
ist die Brücke. Status-Marker **im Fließtext** (Messchronik, `└─`-Zeilen) bleiben: sie beschreiben
Historie, keinen Stand.
Dazu zwei Befunde, die dabei behoben wurden:
- **TP47 existierte nur in der Datei**, ohne Gitea-Issue (seit Commit `808d3a1`, 2026-09-27) — jetzt
**#74**. Das war ein echter SSoT-Bruch, kein fehlender Zeiger.
- **`bin/gitea-tp-sync.py` schrieb in die falsche Richtung**: der Abgleich leitete den Zielzustand
aus dem Datei-Marker ab (fest `✅`) und hätte #24 (TP34, bewusst `status:dropped`) auf `status:done`
gezogen. Das Werkzeug schreibt jetzt **nur noch fehlende** Archiv-Issues neu und fasst bestehende
**nie** an.
## 2026-09-12 — Golden-Config-Backups laufen jetzt täglich (vorher KEIN Scheduler)
**Betrifft:** alle, die sich auf GC-Backups/Compliance oder `gc-snapshot.py`-Werte verlassen.
**Aktion:** Es gab **nie** einen automatischen GC-Backup-Zeitplan — Backup/Intended/Compliance liefen

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).

View File

@@ -0,0 +1,75 @@
# Handoff 2026-09-22 — CLAUDE.md ausdünnen (strukturelle Hygiene, kein Bug)
**Anlass:** Christoph stellt fest, dass CLAUDE.md „eh schon so groß" ist. Das ist kein kosmetischer
Punkt — es ist eine strukturelle Drift, die jede künftige Session belastet. Dieser Handoff beschreibt
das Problem aus der Innenperspektive einer Session (die CLAUDE.md bei jedem Start komplett lädt) und
gibt einen Scope für eine eigene Cleanup-Session.
## Das Problem, aus Sicht der Session
Jede Session lädt CLAUDE.md **vollständig** in den Kontext. Was dabei konkret wehtut:
1. **Reine Kontext-Kosten.** Die Datei ist >58 KB. Ein großer Teil davon ist nicht „Kontext für die
anstehende Arbeit", sondern Historie und abgeschlossene TPs. Der Token-Preis wird bei jeder Session
fällig, egal worum es geht.
2. **Signal-to-noise im Gotcha-Block.** Der „Kritische Gotchas"-Abschnitt mischt **aktive Fallen** mit
**erledigten/gegenstandslosen** Einträgen. Beispiele direkt aus dem Bestand: `~~ALLNET~~` und
`~~Omada~~` sind mehrfach als „gegenstandslos/historisch" markiert, stehen aber weiterhin voll im
Text. Mehrere Einträge tragen „korrigiert am X" — die **korrigierte** Fassung steht neben der
**falschen**, die damit weiter mitgelesen wird. Beim Suchen nach *einer* relevanten Falle scannt
die Session dutzende Zeilen, von denen ein guter Teil totes Gewicht ist.
3. **Redundanz über drei Orte.** Dieselbe Information (z. B. die AP-Positionen, der FT-Verlauf) steht
in CLAUDE.md **und** in `teilprojekte.md` **und** teils in Handoffs. Eine Korrektur an einer Stelle
lässt die anderen stumm veralten — genau die Drift, vor der die eigene CLAUDE.md warnt
(„Manuell gepflegte Doku verliert gegen die Realität, wenn sie lang wird").
4. **Der gefährlichste Effekt: die Bloat untergräbt „live prüfen".** Die Projektregel lautet „nicht
raten, live prüfen oder fragen". Eine Riesen-CLAUDE.md mit eingestreuten Stale-Angaben verleitet
genau dazu, sich auf die Datei zu verlassen statt nachzusehen — die Datei ist Teil des Problems,
das sie zu lösen vorgibt.
## Was wohin gehört (SSoT-Dreiteilung, TP36)
| Inhalt | Soll-Ort |
|---|---|
| VLAN-Modell, Hardware-Tabelle (aktuell), Tooling-Pointer | CLAUDE.md (bleibt) |
| **Aktive, querschnittliche** Fallen (Firewall deny-by-default, sshpass-Regel, cap-wifiN-Karte …) | CLAUDE.md (bleibt, komprimiert) |
| TP-spezifische Gotchas & Detail-Verläufe | `teilprojekte.md` `### TPnn` |
| Erledigte/historische Geräte & Entscheidungen | `teilprojekte-archiv.md` |
| „korrigiert am X"-Notizen | Original-Eintrag bereinigen (Stale-Wert raus), Korrektur **ersetzt** den Irrtum, statt neben ihm zu stehen |
## Konkrete Kandidaten zum Ausdünnen (Startpunkte, nicht abschließend)
- **„Kritische Gotchas":** alle `~~durchgestrichenen~~`/„gegenstandslos"-Einträge (ALLNET, Omada,
sw1, ES208G, „ALLNET VLAN-DB") → Archiv oder löschen.
- **„korrigiert"-Notizen:** die falsche Angabe entfernen, nur die korrigierte behalten (z. B.
Nautobot-IP, „2×/Tag war nie real", „`ft=no` war NICHT uniform").
- **„Pflichtlektüre":** ist von „1–7" auf effektiv „0–7" angewachsen; die ✅-GESCHLOSSEN-Punkte
(TP16.3 §5, Fund E) sind abgeschlossene Historie → aus dem Pflichtteil in einen Verweis.
- **„Lessons for Sessions":** mehrere Meta-Lektionen überlappen sich (Maskierungs-Muster kommt 3×,
„erst das Werkzeug verdächtigen" 2×) → auf je eine Fassung verdichten.
- **Hardware-Tabelle:** ausgemusterte Geräte (Omada, ALLNET) sind noch als Zeilen da, teils nur mit
❌ — prüfen, ob sie ganz raus können (Archiv) statt als Toten in der aktuellen Tabelle zu stehen.
## Vorgehen für die Cleanup-Session
1. **Bestandsaufnahme zuerst:** CLAUDE.md abschnittsweise sichten und je Eintrag klassifizieren
(aktiv-querschnittlich / TP-spezifisch / historisch / redundant). Drift-Tabelle vorlegen, **fragen**
was wohin soll — nicht stillschweigend umsortieren.
2. **Nichts löschen ohne Zielort:** TP-spezifisches wandert nach `teilprojekte.md`, Historisches nach
`teilprojekte-archiv.md`. Erst wenn der Zielort steht, aus CLAUDE.md entfernen.
3. **Zielbild:** CLAUDE.md zurück auf einen **schlanken Index** (VLAN-Modell, Hardware, Tooling, die
~10 wirklich querschnittlichen aktiven Fallen). Detail gehört nach `teilprojekte.md`.
4. **Abnahme:** CLAUDE.md-Wortzahl/Zeilen vorher/nachher notieren; sicherstellen, dass kein
noch-aktiver Gotcha (Firewall, sshpass, cap-wifiN, slaves-datapath, WLAN-Logs-Topic) verloren geht.
## Übergabeprompt (nächste Session)
> CLAUDE.md ausdünnen: die Datei ist >58 KB und mischt aktive Fallen mit erledigter Historie
> (durchgestrichene „gegenstandslos"-Einträge, „korrigiert"-Notizen neben der falschen Angabe,
> Redundanz mit teilprojekte.md). Aufgabe: Bestandsaufnahme + Drift-Tabelle, dann TP-spezifisches
> nach teilprojekte.md, Historisches ins Archiv, CLAUDE.md zurück auf einen schlanken Index
> (~10 querschnittliche aktive Gotchas behalten). Nicht stillschweigend umsortieren — erst klassifizieren
> und fragen. Siehe [[handoff-20260922-claude-md-ausduennen]].

View File

@@ -0,0 +1,45 @@
# Handoff 2026-09-22 — Doku-/TP-Verwaltung: Umbau-Vorschlag (Basis für eigene Session)
**Anlass:** Christoph will eine separate Session, die die Doku-/TP-Verwaltung strafft und die
Vorschläge selbstständig umsetzt. Diese Notiz ist die Analyse-Basis aus der Ausdünnen-Session
(2026-09-22), in der die Drift-Kosten konkret sichtbar wurden. **Vorschläge, kein Mandat.**
## Diagnose (woran es hakt)
Drei parallele Wahrheitsquellen driften gegeneinander:
1. **Gitea-Issue** (Index + Label `status:*`/`thema:*`)
2. **`docs/teilprojekte.md`** (Prosa, kuratiert, `### TPnn`-Abschnitte)
3. **CLAUDE.md** (Quick-Index/Fokus — bis zur Ausdünnen-Session ein Ist-Stand-Block)
Jede Status-/Fakten-Änderung muss an 2–3 Stellen nachgezogen werden. Die komplette
Ausdünnen-Session plus der TP25-Roll-up (507 Zeilen verschieben, Index-Zeile, zwei
„nicht abgeschlossen"-Notizen, Pointer in `teilprojekte.md`) war reine Drift-Bekämpfung —
Arbeit, die ein Gitea-Issue-Kommentar gratis ersetzt.
## Standard vs. Eigenbau (kurz)
Der Claude-Code-Standard ist schmal: **CLAUDE.md** (kurz, statisch, nur das Nicht-Offensichtliche)
+ **git** + **Auto-Memory** + ein Issue-Tracker (Gitea/GitHub/Linear). Ein „TP-System" ist **kein**
Standard — die TP-Verwaltung hier ist Eigenbau auf Gitea. Der Teil „Gitea-Issue als Index + Label
als Status" ist der richtige, standardnahe Kern.
## Drei Vorschläge (Reihenfolge = Wert)
1. **CLAUDE.md = nur statische Konventionen.** Nach der Ausdünnen-Session 28 KB und gut so.
Nie wieder „aktueller Stand"/Ist-Stand reinschreiben — der gehört nach Gitea.
2. **Status → nur Gitea, Prosa → nur eine Stelle.** Der Kuratier-Aufwand (Index-Tabellen,
„Dazu am Ende"-Notizen, Archiv-Roll-ups) kostet pro geschlossenem TP mehr als er wert ist.
Kandidat: Prosa/Detail in den Gitea-Issue-Body/Kommentar verlagern, `teilprojekte.md`
auf ein append-only Log reduzieren oder ganz durch Issues ersetzen.
3. **Mehr git, weniger kuratieren.** `handoff-*.md` pro Session ist gut. Archiv/Roll-up liefert
wenig über `git log` + geschlossenes Issue hinaus.
## Abgrenzung / Tradeoffs
- Das System hat echten Wert: auf Christoph zugeschnitten, die Drift-Warnungen haben real etwas
gefangen. **Nicht ersatzlos streichen.**
- Teuerster Teil ist die **zweite, kuratierte Prosa-Ebene** (`teilprojekte.md` + Archiv) — nicht die
Gitea-Ebene und nicht CLAUDE.md.
- Die separate Session sucht selbstständig Vorschläge UND setzt um; diese Notiz ist die Ausgangslage,
kein Plan.

View File

@@ -0,0 +1,52 @@
# Handoff 2026-09-28 — Core erreicht FM/DW-Spoke-Netze nicht aus eigener Kraft
## Kontext / Auslöser
Christoph konnte von Notebook + macmini immer `10.101.0.254` (FM-Standort-GW) pingen, eine
überforderte Vorgänger-Session hatte behauptet, der Core „komme dort nicht hin". Befund
sollte jetzt geprüft und ausgeräumt werden.
## Befund (alles live verifiziert 2026-09-28, crs326-core)
| Quelle | → 10.101.0.254 (FM-GW) | → 10.101.0.10 (FM-Host) |
|---|---|---|
| macmini (VLAN20, gw 10.40.20.1) | ✅ 0 % Loss | ✅ 0 % Loss |
| Notebook (VLAN10, gw 10.40.10.1) | ✅ 0 % Loss | ✅ 0 % Loss |
| **Core default** (`/ping` ohne src) | ❌ **100 % Loss** | ❌ **100 % Loss** |
| **Core mit `src-address=10.40.10.1`** | ✅ **0 % Loss** (ttl 64) | ✅ **0 % Loss** (ttl 63) |
## Root Cause
`wg-hub` (TP10 Multi-Site VPN Hub) hat **keine IP-Adresse** — weder live
(`/ip address print where interface="wg-hub"` → leer) noch im GC-Template
(`mikrotik_routeros.j2:379`, nur listen-port/mtu/name/private-key).
FM-seitig erlaubt die FritzBox als AllowedIPs Richtung Hub `10.40.0.0/16`
(`routeros/fritzbox-wg-lanlan-klickanleitung.md`). Weitergeleiteter Client-Traffic
(Quell-IP 10.40.x) liegt in diesem Raum → läuft. **Core-eigener** Traffic ohne
explizite Quell-IP: RouterOS wählt mangels IP auf wg-hub eine Quell-IP **außerhalb**
von 10.40.0.0/16 (vermutlich WAN `90.146.33.177`) → FM routet die Antwort nicht zurück
→ Timeout. Der Tunnel selbst ist up (FM-Peer last-handshake ~1m45s, rx 140.8 GiB).
## SSoT / Governance
Die Core-Config liegt im GC-Template `nautobot/mikrotik_routeros.j2`. Dort existieren:
- `:375` `/ip route add ... dst-address=10.101.0.0/24 gateway=wg-hub comment="TP10: FM spoke"`
- `:376` `/ip route add ... dst-address=10.95.10.0/24 gateway=wg-hub comment="TP10: DW spoke"`
- `:379-381` wg-hub Interface + fm/dw Peers (Privkey redacted)
Ein Fix gehört **ins Template**, nicht als Direkt-Write aufs Gerät (GC-Compliance-Regel).
Deploy via `nautobot/gc-configplan.py` / GC-Job „Backup → Intended → Compliance".
## Fix-Optionen (noch nicht entschieden)
1. **`preferred-source` auf die zwei TP10-Routen** (Zeile 375/376, z. B. `10.40.10.1`).
Kein FM-Eingriff nötig (FM erlaubt 10.40.0.0/16 bereits). Minimal-invasiv. **Empfohlen.**
2. wg-hub eine IP geben (z. B. dedizierte Adresse aus 10.40.0.0/16) — mehr Risiko, IP-Raum-
Eingriff, FM-Abgleich nötig.
3. FM-seitig AllowedIPs um die Core-WAN-IP erweitern — FritzBox-WG-Config eingeschränkt, nicht sauber.
## Offene Entscheidungen
- Welche `preferred-source`-IP? (10.40.10.1 = MAIN-SVI ist naheliegend.)
- TP10 ist im Archiv „KERN DURCH, offen nur DDNS-Updater". Neuer Befund → TP10-Issue
`reopen` + Prosa zurück nach `teilprojekte.md`, oder als eigenes kleines TP? (Christoph-Entscheid.)
- Fix jetzt ausführen oder nur dokumentieren?
## Aufwandsschätzung (Core-Ping-Fix)
Klein. 2 Zeilen Template-Edit + ein GC-Deploy-Zyklus (~30–60 min inkl. Backup/Intended/Compliance)
+ Ping-Verifikation. Kein Hardware-/FM-Eingriff bei Option 1.

View File

@@ -58,25 +58,26 @@ teuerste Eigenschaft der Datei zerstört, um ihr billigstes Problem zu lösen. B
| [TP21](#tp21) | Nautobot Golden Config (config-as-code Control-Plane) | 🟢 Plattform + Datenmodell + Templates live |
| [TP21.1](#tp21-1) | Core-Cutover-Recovery (ap1 → crs326-core als L3-Router) | ✅ NETZ WIEDER LIVE (2026-08-08) — Aufräum-Todos **alle erledigt** (Stand 2026-08-23) |
| [TP22](#tp22) | Device-Adoption + Golden-Config Compliance-Push (RouterOS-Fleet) | ✅ DURCH (2026-08-13) — Compliance heute **104/104, 0 Drifts** (live 2026-08-23) |
| [TP25](#tp25) | WLAN-Stabilisierung | ✅ FERTIG (2026-09-15) — Platzierung final (ap1 Sideboard · ap2 UG · ap3 OG), Fund E geschlossen |
| [TP26](#tp26) | `/ip firewall filter` unter Drift-Kontrolle | ✅ DURCH (2026-08-17) |
| [TP27](#tp27) | RouterBOOT-Firmware auf 7.24 nachziehen | ✅ DURCH (2026-08-17/18) |
| [TP28](#tp28) | WLAN über CAPsMAN auf `crs326-core` | ↩️ **wieder geöffnet 2026-08-30** (ap3) — Volltext in [teilprojekte.md](teilprojekte.md#tp28) |
| [TP-R](#tp-r) | Router-Capabilities-Sweep | FERTIG (docs/research/routeros-capabilities.md) |
| [TP39](#tp39) | LXC-Hygiene pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup | ✅ ABGESCHLOSSEN (2026-09-01) — pve+NAS-Sweep grün |
| [TP34](#tp34) | Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) | ❌ VERWORFEN (2026-09-08) — Caddyfile bleibt manuell |
| [TP34](#tp34) | Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) | ❌ VERWORFEN (2026-09-08) — Caddyfile bleibt manuell · **Fortsetzung: TP34.2 (2026-09-22)** |
| [TP44](#tp44) | Cloudflare-Estate als Dokumentations-SSoT in Nautobot | ✅ DURCH (2026-09-10) |
| [TP41](#tp41) | CF-Access-Reparatur + Zero-Trust-Konsolidierung | ✅ ABGESCHLOSSEN (2026-09-10) — Gelis WARP via Android eingerichtet, letzter Blocker weg |
| [TP43](#tp43) | dw-Standort-DNS-Uplink (site-spezifische Overrides) | ♻️ REOPENED (2026-09-11) — Volltext zurück in teilprojekte.md; interne BF-Namen über WG am dw offen |
| [TP42](#tp42) | Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net | ✅ DURCH (2026-09-02) — `.bf.lan` raus, `.16` abgebaut, Compliance 140/140 |
Dazu am Ende: **[TP25 (Langfassung)](#tp25-lang)** — die Messchronik und die Funde A–E im Volltext.
TP25 selbst ist **nicht** abgeschlossen und steht weiter in `teilprojekte.md`; nur seine 394 Zeilen
Herleitung liegen hier.
Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)** — TP25 ist
✅ abgeschlossen (2026-09-15); der aktive Stand (Kondensat) und die 394-Zeilen-Herleitung
(Messchronik A–G) liegen beide hier.
---
<a id="tp1"></a>
### TP1 — ALLNET Core-Switch finalisieren ▸ ✅ FERTIG (2026-06-15) → ❌ Gerät AUSSER BETRIEB (2026-08-08, durch crs326-core ersetzt)
### TP1 · Gitea #34 — ALLNET Core-Switch finalisieren — → ❌ Gerät AUSSER BETRIEB (2026-08-08, durch crs326-core ersetzt)
- ✅ GE8 Trunk (native vlan1, tagged 10/20/30/40/50/90/99/100) — war bereits aktiv.
- ✅ VLAN 40 umbenannt: UNTRUSTED → **MEDIA** (`switch/cli/04-vlan50-media-untrusted.cli`).
- ✅ VLAN 50 angelegt + benannt: **UNTRUSTED**.
@@ -85,7 +86,7 @@ Herleitung liegen hier.
<a id="tp2"></a>
### TP2 — Omada Access-Switch + Controller ▸ ✅ FERTIG (2026-06-15)
### TP2 · Gitea #35 — Omada Access-Switch + Controller
- ✅ Omada Controller 6.2.10.17 auf DS923 (macvlan `.175`): `omada/docker-compose.yml`, Stack via Portainer endpointId=14. **Wizard abgeschlossen** (configured=true), Creds in `mcp/omada/.env`.
- ✅ pve-mcp (samik081) in Claude Code eingetragen (`~/.claude/settings.json`, SSE `.173:3000`).
- ✅ omada-mcp **live** auf **pve-LXC (`.126`, endpointId=2), ipvlan-IP `.176`**, Stack Id=200, URL in `~/.claude/settings.json`.
@@ -121,7 +122,7 @@ Herleitung liegen hier.
<a id="tp2-1"></a>
### TP2.1 — Omada-Switch (sw2/ES208G) → Standalone-Recovery (kein Controller) ▸ ✅ FERTIG (2026-07-16, User solo)
### TP2.1 · Gitea #36 — Omada-Switch (sw2/ES208G) → Standalone-Recovery (kein Controller) — User solo)
- **Root Cause:** beim VLAN20-Umzug des Omada-Controllers (TP14.1, 2026-07-10) ist die Controller↔sw2-Verbindung zerbrochen (sw2 seither „Disconnected", ursprünglich als reiner Mgmt-Loss vertagt). Beim Rettungsversuch hat der User versehentlich einen **Werks-Reset** am Switch ausgelöst. Ein anschließender Wiederherstellungsversuch (durch Claude) hat Teile der Config **unbemerkt halluziniert** — der damalige „wiederhergestellte" Stand war **nicht vertrauenswürdig**.
- **Entscheidung (User 2026-07-15): Controller-Zustand als invalid betrachtet, sw2 komplett neu — diesmal OHNE Managementsoftware.** Kein Omada-Controller-Re-Adopt, kein Omada-Ecosystem-Anschluss (s. TP15-Korrektur — Omada bekommt generell keine Rolle mehr, auch nicht WLAN). sw2 direkt über eigenes Web-UI konfiguriert, wie ein eigenständiger Switch.
- **✅ 2026-07-16 (User-Aussage): „voll durch, Funktionsweise erfolgreich getestet."** Nach dem gescheiterten Claude-Versuch hat der User sw2 **komplett allein** neu aufgesetzt (Runbook `docs/tp2.1-omada-switch-standalone-runbook.md` als Playbook-Referenz, nicht Schritt-für-Schritt-Log). **Keine formale Einzelschritt-Verifikation dieser Session** (kein Port-für-Port-Protokoll, keine Live-Prüfung durch Claude) — Status beruht auf User-Bestätigung „es geht einfach", nicht auf eigener Nachkontrolle.
@@ -131,7 +132,7 @@ Herleitung liegen hier.
<a id="tp3"></a>
### TP3 — WLAN ▸ 📜 **VOLLSTÄNDIG HISTORISCH — der Ist-Stand steht in TP28, nicht hier**
### TP3 · Gitea #37 — WLAN
> 🔴 **Lesehinweis (überarbeitet 2026-08-19): dieser gesamte TP ist Geschichte, von der ersten bis zur
> letzten Zeile.** Er beschreibt zwei überholte Zustände nacheinander — die **CAPsMAN-Ära (Juni/Juli
> 2026)** und danach den **Standalone-Rebuild vom 2026-08-08**. Beides gilt nicht mehr:
@@ -169,7 +170,7 @@ Herleitung liegen hier.
<a id="tp3-1"></a>
### TP3.1 — ap2 VLAN-Trunk-Fix ▸ ✅ Config live (2026-07-01) — `routeros/tp3.1-ap2-vlan.rsc`
### TP3.1 · Gitea #38 — ap2 VLAN-Trunk-Fix — `routeros/tp3.1-ap2-vlan.rsc`
- **Root Cause:** ap2 (CAP, local forwarding) hatte `bridge vlan-filtering=no` → `datapath.vlan-id` wurde nie
auf die Leitung geschrieben → **ALLE** WLAN-Clients auf ap2 (main/media/iot/…) egressten **ungetaggt in VLAN1**
(defconf/`10.40.41.x`) statt ins Ziel-VLAN. Symptom: iPhone (VLAN10) im OG „verbunden, aber sporadisch kein
@@ -207,7 +208,7 @@ Herleitung liegen hier.
<a id="tp3-2"></a>
### TP3.2 — WLAN Coverage / P750W-Discovery ▸ ✅ **ABGESCHLOSSEN (2026-08-17, User-Test erfolgreich)** — `docs/wifi-p750w-coverage-and-map.md`
### TP3.2 · Gitea #39 — WLAN Coverage / P750W-Discovery — `docs/wifi-p750w-coverage-and-map.md`
- ✅ **Brother P750W (VLAN30) gelöst:** DHCP-`client-id`-Fix (matcht über MAC → feste `10.40.30.5`); **an ap1 gepinnt**
(geordnete wifi-access-list `accept w2-iot` → blanket `reject`, überlebt ap2-Re-Provision) — dozende Legacy-Clients
nur am co-located-GW ap1 per ARP erreichbar, CAP ap2 nicht; mDNS `vlan30` in `mdns-repeat-ifaces`+5353 → in MAIN
@@ -220,7 +221,7 @@ Herleitung liegen hier.
<a id="tp3-3"></a>
### TP3.3 — WLAN Coverage-Kur ▸ 📜 **IN TP25 AUFGEGANGEN (2026-08-17)** — offene Arbeit steht dort, nicht hier
### TP3.3 · Gitea #40 — WLAN Coverage-Kur — offene Arbeit steht dort, nicht hier
> ⚠️ Der Header sagte bis 2026-08-19 „🔜 TODO (Fortsetzung, neue Session)" — dieselbe Falle wie bei TP3:
> ein Status, der nicht mitgezogen wurde. Der Re-Walk und die Dead-Spot-Kartierung sind **Schritt 5 von
> TP25** und zwar bewusst **nach** den Funkparameter-Fixes; die 802.11k/v-Reparatur ist am 2026-08-13
@@ -259,7 +260,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp4"></a>
### TP4 — Router-Cutover (hAP#1 = Router + WAN-Edge) ▸ ✅ FERTIG (2026-07-01) — ⚠️ **Rolle seit 2026-08-08 auf `crs326-core`** (TP21.1); alles unten beschreibt den damaligen ap1-Aufbau und gilt als **Historie/Referenz**, nicht als Ist
### TP4 · Gitea #41 — Router-Cutover (hAP#1 = Router + WAN-Edge) — ⚠️ **Rolle seit 2026-08-08 auf `crs326-core`** (TP21.1); alles unten beschreibt den damaligen ap1-Aufbau und gilt als **Historie/Referenz**, nicht als Ist
- ✅ **Bridge VLAN-Filtering aktiviert** (`vlan-filtering=yes`); VLAN-Tabelle: VLAN1 untagged bridge+ether2, VLANs 10/20/30/40/50/90/99/100 tagged bridge+ether2.
- ✅ **VLAN-Subinterfaces:** vlan10/20/30/40/50/90/99 auf bridge; je GW `.1` (10.40.10.1 … 10.40.99.1).
- ✅ **DHCP:** dhcp-v10…dhcp-v99 je auf eigenem VLAN-Interface, pools .100-.200, GW+DNS = VLAN-GW.
@@ -274,7 +275,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp5"></a>
### TP5 — Firewall & Inter-VLAN-Policy ▸ ✅ **DURCH (2026-08-18): deny-by-default in BEIDEN Ketten scharf** · **+ E5 (2026-08-19)** — 📋 `docs/tp5-deny-by-default-runbook.md`
### TP5 · Gitea #42 — Firewall & Inter-VLAN-Policy — + E5 (2026-08-19)** — 📋 `docs/tp5-deny-by-default-runbook.md`
> 📌 **„DURCH" heißt nicht „abgeschlossen".** E5 (MAIN→IOT) kam einen Tag nach dem Drop — unter deny-by-default
> braucht **jeder** neue Dienst über eine VLAN-Grenze seine Regel. Weitere Etappen sind normaler Betrieb,
> kein Rückschritt. Der Weg dahin ist immer derselbe: **erst den Drop-Log lesen**, dann die Regel schreiben.
@@ -410,7 +411,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp6"></a>
### TP6 — QoS/CAKE + Call-Quality-A/B-Test ▸ ✅ **GESCHLOSSEN 2026-08-23 — CAKE gebaut, gemessen, wieder ausgebaut.** ⚠️ **Die Erklaerung darin ist von TP6.1 ueberholt: nicht DOCSIS, sondern fehlende Klassentrennung.**
### TP6 · Gitea #43 — QoS/CAKE + Call-Quality-A/B-Test — ⚠️ **Die Erklaerung darin ist von TP6.1 ueberholt: nicht DOCSIS, sondern fehlende Klassentrennung.**
> 🔴 **ZUERST LESEN — diese Analyse ist teilweise widerlegt (2026-08-23, durch TP6.1).**
> Unten steht, die ~30 ms entstuenden im DOCSIS-Uplink und seien unabaenderlich. **Sie sind es
> nicht.** TP6.1 holt dieselbe Strecke unter derselben Saettigung auf **11,0 ms** — allein durch
@@ -549,7 +550,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp6-1"></a>
### TP6.1 — Saettigung vermeiden statt Bufferbloat bekaempfen ▸ ✅ **LIVE UND GEMESSEN (2026-08-23): 40,8 → 11,0 ms unter Last**
### TP6.1 · Gitea #44 — Saettigung vermeiden statt Bufferbloat bekaempfen
> **Der Gedanke kam von Christoph** beim TP6-Abschluss („dann koennten wir zumindest den Sync
> von der NAS zurueckhalten“) — und er hat mein TP6-Urteil zu Recht angezweifelt.
>
@@ -600,7 +601,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp7"></a>
### TP7 — Media-Discovery (mDNS) für VLAN 40 ▸ ✅ FERTIG (2026-07-02) — `docs/tp7-mdns.md`
### TP7 · Gitea #45 — Media-Discovery (mDNS) für VLAN 40 — `docs/tp7-mdns.md`
- ✅ **Built-in mDNS-Repeater** aktiv — **Live-Stand `crs326-core` (verifiziert 2026-08-14):**
`mdns-repeat-ifaces=vlan10,vlan20,vlan30,vlan40,vlan90` → MAIN/INFRA/IOT/GUEST ↔ MEDIA. Reversibel (Feld leeren).
📌 **`bridge` (=VLAN1) ist nicht mehr dabei** — mit dem VLAN1-Retirement erwartbar und korrekt, war aber nie nachgezogen.
@@ -617,7 +618,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp7-1"></a>
### TP7.1 — SSDP-Relay (macOS-Sonos-App über VLAN-Grenze) ▸ ✅ **LÄUFT (2026-08-18)** — User-bestätigt: „sonos ist da!"
### TP7.1 · Gitea #46 — SSDP-Relay (macOS-Sonos-App über VLAN-Grenze) — User-bestätigt: „sonos ist da!"
- ✅ **Umsetzung:** eigener LXC **108 `mcast-relay`** auf pve (Ubuntu 24.04, 1 Kern, 256 MB, `onboot`),
zweibeinig: `eth0` = **10.40.10.21** (VLAN10), `eth1` = **10.40.40.21** (VLAN40), Default-Route nur
über VLAN10, **kein IP-Forwarding**. Dazu `core ether13` (pve-Port) um `tagged vlan10, vlan40`
@@ -658,7 +659,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp7-2"></a>
### TP7.2 — HA↔Sonos static-host binding + VLAN1/GUEST-Discovery ▸ ✅ FERTIG (2026-07-03) — `docs/sonos-inventar.md`
### TP7.2 · Gitea #47 — HA↔Sonos static-host binding + VLAN1/GUEST-Discovery — `docs/sonos-inventar.md`
- **Root Cause (widerlegt frühere Doku):** HA-`media_player.*`-Sonos alle `unavailable`. HA sitzt **nicht** auf
VLAN 20, sondern noch auf `10.40.41.105` (VLAN-1-Transfernetz, IP-Migration steht aus). Der mDNS-Repeater
deckte VLAN 1 **nicht** ab, und die HA-Sonos-Integration ist **SSDP-abhängig** (`manifest dependencies:[ssdp]`,
@@ -681,7 +682,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp9"></a>
### TP9 — Gateway-/Router-HA ▸ ❌ OBSOLET (User-Entscheid 2026-08-08)
### TP9 · Gitea #48 — Gateway-/Router-HA
- ❌ **Verworfen (User-Begründung 2026-08-08, „ganz klar nein"):** am `crs326-core` führt ohnehin **nie ein Weg vorbei** — er ist der unumgängliche L3-Durchleitungspunkt. Ein zweites Gerät / VRRP **beseitigt diesen SPOF nicht**, sondern baut nur **zusätzliche Sollbruchstellen** ein (2× Config-Sync für DHCP/DNS/Firewall/NAT, Split-Brain-Risiko, VRRP-Eigenfehler) → netto ein Zuverlässigkeits-**Verlust**, kein -Gewinn. Die Frage ist nicht „Aufwand vs. Nutzen", sondern „mehr Fehlerquellen einbauen?" → nein. DR-Weg = Config-Backup-Restore (+ ggf. AP-Reprovisionierung). VRRP-Skizze unten = nur noch historische Recherche.
- **„Was, wenn ap1 ausfällt?"** → **VRRP** löst Gateway-Failover je VLAN ~gratis (kreuzweise Master/Backup).
⚠️ **Haken (nicht Last):** DHCP/Firewall/NAT/DNS müssen auf **BEIDEN** APs **identisch** gepflegt sein, sonst „Gateway up, Internet tot".
@@ -690,7 +691,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp10"></a>
### TP10 — Multi-Site VPN ▸ ✅ KERN DURCH (2026-06-27) — beide WG live auf ap1-Hub (`docs/tp10-vpn-migration.md`)
### TP10 · Gitea #49 — Multi-Site VPN — beide WG live auf ap1-Hub (`docs/tp10-vpn-migration.md`)
> **Details/Cutover/Gotchas/Eckdaten → `docs/tp10-vpn-migration.md` (Single Source of Truth).** Research: `docs/research/vpn-comparison.md`.
- **✅ KERN DURCH (2026-06-27) + EDGE-CUTOVER (2026-07-01):** beide AVM-Tunnel → **2× WireGuard mit Hub auf ap1** migriert. Live: `peer fm`
(`10.101.0.0/24`, bidirektional, myfritz-Endpoint) **und** `peer dw` (`10.95.10.0/24`, CGNAT-Dial-in) mit frischem
@@ -710,7 +711,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14"></a>
### TP14 — Host/Container-IP-Migration VLAN1→VLAN20 (Docker-Cutover NAS+pve) ▸ ✅ DURCH (2026-07-13; live re-verifiziert 2026-08-08) — `docs/nas-vlan20/RUNBOOK.md`, Handoff `docs/nas-vlan20/handoff/`
### TP14 · Gitea #50 — Host/Container-IP-Migration VLAN1→VLAN20 (Docker-Cutover NAS+pve) — live re-verifiziert 2026-08-08) — `docs/nas-vlan20/RUNBOOK.md`, Handoff `docs/nas-vlan20/handoff/`
- ✅ **ABGESCHLOSSEN — Migration komplett** (Live-Check 2026-08-08 vom core: NAS `.20.10`, pve `.20.100`, Docker-LXC `.20.126`, platform-pg `.20.20`, Nautobot `.20.30` alle 0% Loss). Alle Hosts + Container auf VLAN20. **TP14.4** (CF-Access-Token-Rotation) = kleiner, nicht-blockierender Standalone-Follow-up (CF/TP11-Umfeld); **TP14.5** war nie ein eigener Change (nur ein Reihenfolge-Gate) → erfüllt, TP5 damit entblockt.
- ✅ **NAS (DS923, ep14) migriert (2026-07-09/10):** Docker von `default_external` (macvlan .41) auf **ein** `vlan20`-Macvlan (10.40.20.0/24, gw .1, parent `ovs_eth0`); DSM-Cutover `.20.10`; Portainer-Endpoint `.20.10:9001`. **Root-Cause der Dauerfehler** = Portainer **Config-Only-Macvlan-Falle** (`Driver:null`, kein Container-Attach) + Macvlan-Parent-Exklusivität (`default_external` muss weg für `vlan20`) + `default_internal` (172.18) weg. Container laufen auf `.20.x`.
- ✅ **CF-Ingress v116 (2026-07-10):** 6 NAS-Origins .41→.20 (SSoT `docs/cf-tunnel-mapping.md` §8); `vw` 200 via Tunnel = pve→VLAN20-Routing steht (502 direkt nach Ingress-PUT = transient).
@@ -722,7 +723,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-1"></a>
### TP14.1 — Session A: NAS fertigstellen ▸ ✅ DURCH (2026-07-10); Omada-Inform/sw2 VERTAGT
### TP14.1 · Gitea #51 — Session A: NAS fertigstellen — Omada-Inform/sw2 VERTAGT
- ✅ **Alle ep14-Container auf vlan20 redeployed + verifiziert running/.20.x** (via Portainer-Stack-Redeploy, Env preserved, Volumes intakt): oc-stack(112 → prometheus `.151`/node-exporter `.154`/grafana `.109`), default(72 → hms-mqtt `.124`), nginx(101 → dashboard-web `.148`), oc-central(160 → qdrant `.159`), oc-charlie(158 → `.162`+cf), oc-johnny2(150 → `.161`+cf), gitea(111 → `.157`, DB `.20.23:3306` erreichbar ✓), drawio(107 → `.139`, intern; CF-CNAME war eh gelöscht).
- ✅ **pve-mcp NEU** als Stack (id 202, vlan20-only `.20.173`), verbunden mit Proxmox `.41.100` (v9.0.11) — ⚠️ **StreamableHTTP am Root-Pfad `/`, NICHT `/sse`** (mcp-pve v0.4.8, `MCP_TRANSPORT=http`). `~/.claude.json` `mcpServers.pve-mcp` NEU angelegt `{type:http, url:http://10.40.20.173:3000/}` (war vorher gar nicht drin) → wirkt nach Claude-Code-Restart.
- ✅ **Tunnel-Smoke** (CF-Ingress v116 zeigte schon auf `.20`): gitea 200, grafana 302, vw 200 — Origins existieren jetzt real.
@@ -733,7 +734,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-2"></a>
### TP14.2 — Session B: pve-Docker-LXC (.126) → VLAN20 ▸ ✅ DURCH (2026-07-10) (ipvlan)
### TP14.2 · Gitea #52 — Session B: pve-Docker-LXC (.126) → VLAN20
- ✅ ep2-Container auf `vlan20`: **omada-mcp `.20.176`** (OMADA_BASE_URL→`.20.175`), **adguard_bf1 `.20.16`**, **adguard_fm2 `.20.17`** — alle running/reachable (health 200, adguard 302). Transf. Files `docs/nas-vlan20/stacks/{195-adguard,200-omada-mcp}.yml`.
- ✅ LXC 400 (`docker`) net0 → `tag=20, ip=10.40.20.126/24, gw=10.40.20.1` (⚠️ **Retag brauchte Christoph** — Token `claude@pam!claude` hat kein `VM.Config.Network`; UI-Edit, dann Reboot via API mit `VM.PowerMgmt`). Portainer wandert auf `https://10.40.20.126:9443`.
- 🐞 **Gotcha: in-Container-Iface heißt jetzt `veth20`, NICHT `eth0`** (UI setzte Name analog net1=veth16) → Docker `vlan20`=ipvlan **`parent=veth20`** (nicht eth0 wie im Runbook). cloudflared(host)/portainer(bridge) unberührt.
@@ -743,13 +744,13 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-2a"></a>
### TP14.2a — pve-mcp Single-Session-Fix + VLAN20-Host-DNS ▸ ✅ (2026-07-10)
### TP14.2a · Gitea #53 — pve-mcp Single-Session-Fix + VLAN20-Host-DNS
- **pve-mcp „Failed to connect":** Ursache = **mcp-pve v0.4.8** kann im HTTP-Transport nur **eine** MCP-Session (ein globaler `StreamableHTTPServerTransport`, `sessionIdGenerator` gesetzt) → erster Client nach Boot belegt den Slot, jeder weitere Init (neue Claude-Code-Session, `claude mcp list`-Healthcheck, Reconnect) = `400 "Server already initialized"`. **omada-mcp NICHT betroffen** (frische Session-ID pro Init → Per-Session-Transport). **Fix = Upgrade auf 0.6.1** (Per-Session-Map, `serverFactory()` je Session, gleiche Env-Vars = Drop-in); Stack 202 neu deployed, Container `.20.173` = 0.6.1, `Connected to Proxmox VE 9.0.11`, `tools/list` liefert `pve_*`, **Claude Code ✔ Connected**. 3× init hintereinander = 3× 200 mit verschiedenen session-ids (verifiziert).
- **Kollateral-Fund — VLAN20-Host-DNS kaputt (beide Docker-Hosts):** nach dem VLAN20-Umzug zeigten **NAS** *und* **pve-LXC** `/etc/resolv.conf` noch auf totes `.41.254` → `docker pull` ghcr.io scheitert (`EAI_AGAIN` / 5s-Resolver-Timeout; Egress zu ghcr-IP `140.82.x:443` = OK 31 ms, also **nur Resolver tot**, nicht Egress). Fixes: **LXC** `.20.126` → `pct set 400 --nameserver 10.40.20.1` (Christoph). **NAS** → DSM-„Allgemein"-DNS war `.20.1`, ABER die **Netzwerkschnittstelle zog DNS per DHCP** und überschrieb `resolv.conf`; Fix = Schnittstelle „DNS manuell" `10.40.20.1` (Christoph) → NAS pullt wieder direkt. **Merkregel Synology:** GUI-DNS ≠ was `dockerd` liest (Quelle `/etc/resolv.conf`, per DHCP überschreibbar). **Workaround solange NAS-DNS tot:** `docker save`(LXC, hat DNS) → **gzip** (191-MB-tar reißt am Portainer-Agent-Socket-Proxy „context canceled"; ~70 MB gz geht) → Portainer `POST /endpoints/14/docker/images/load` → Stack-Redeploy `pullImage:false`. Gotcha: laufende Container frieren den `127.0.0.11`-Upstream zum Erstellzeitpunkt ein (Recreate nötig; für pve-mcp egal — Proxmox per IP `.41.100`). Memory `ref_pve_mcp_singlesession_and_vlan20_dns`.
<a id="tp14-3"></a>
### TP14.3 — Session C: Dormante Stacks vlan20-tauglich + Referenz-Sweep ▸ ✅ Referenz-Sweep durch (2026-07-16); dormante Stacks weiter bewusst nicht deployt
### TP14.3 · Gitea #54 — Session C: Dormante Stacks vlan20-tauglich + Referenz-Sweep — dormante Stacks weiter bewusst nicht deployt
- ✅ **Dormante Stacks bereitgestellt (2026-07-11):** `transform.py` auf core(43)/openclaw(92)/openclaw2(97)/oc20260307(119)/openspeedtest(74)/speedtest_tracker(80)/technikscheiss(42) → **`.new.yml` + `.orig.yml`** in `docs/nas-vlan20/stacks/` (gitignored). **Nicht deployt** (User: „im Bedarfsfall später", Container-Zukunft ungewiss). 42/43 waren `default_internal` → jetzt vlan20-only (DHCP-.20.x). ⚠️ **Deploy-Caveats:** externe Refs bewusst NICHT umgeschrieben (`DB_HOST=.41.122` [pre-existing tote openclaw-DB, in 80+119], `OLLAMA_BASE_URL=.41.129`, `REDIS_URL=.41.111` in 119); **Kollision** `43-core` will `.20.23` = gitea-DB-IP (nicht gleichzeitig deploybar).
- ✅ **Referenz-Sweep `.41→.20` (2026-07-16, im Rahmen der TP19-Backup-Vorbereitung live gefunden + gefixt):** ap1 `/ip dns static` hatte noch `nas.bf.lan→.41.10` und `docker.bf.lan→.41.126` **stale** — korrigiert auf `.20.10`/`.20.126` (MikroMCP `manage_dns_entry`, dry-run+apply). Zusätzlich per User-Entscheid **Omada-Controller + omada-mcp komplett gelöscht** (overkill für 1 Switch auf der Abschussliste) → DNS-Einträge `omada.bf.lan`/`omada-mcp.bf.lan` entfernt statt migriert. `fritzbox.bf.lan` (totes Gerät, physisch weg seit 2026-07-01) ebenfalls entfernt. `docs/ip-plan.md` + CLAUDE.md nachgezogen.
- ✅ **ERLEDIGT/GEGENSTANDSLOS (live geprüft 2026-08-16):** ~~`~/.claude.json` (omada-mcp-Eintrag verwaist), CF-Ingress-Rest (`omada`/`omada-mcp`-Hostnames, ungeprüft)~~
@@ -759,7 +760,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-5"></a>
### TP14.5 — Session E: Freigabe-Gate → TP5-Firewall ▸ ✅ ERFÜLLT (2026-08-08) — kein eigener Change
### TP14.5 · Gitea #55 — Session E: Freigabe-Gate → TP5-Firewall — kein eigener Change
- Gate „TP14.1–14.3 durch (alle Origins/Connectoren in VLAN20)" ist erfüllt → **TP5 (Deny-by-default) ist entblockt.** Reines Reihenfolge-Gate, kein eigener Change.
<a id="tp15"></a>
@@ -770,7 +771,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
> auf tote Adressen — `backup-ap1.py` allerdings auf eine **lebende falsche** (`10.40.10.1` ist die
> VLAN10-SVI des Core, nicht ap1) und hätte still das falsche Gerät gesichert.
>
### TP15 — HW-Refresh: Switching + Router ▸ ✅ **ABGESCHLOSSEN (2026-08-08)** — Bring-up beider CRS326 + Branch-Tausch (2026-07-23) **und** Core-Tausch/L3-Umzug (2026-08-08, Durchführung s. TP21.1) sind durch — `docs/hw-refresh-requirements.md`, Memory `project_mikrotik_commit_nautobot_gc`
### TP15 · Gitea #56 — HW-Refresh: Switching + Router — Bring-up beider CRS326 + Branch-Tausch (2026-07-23) **und** Core-Tausch/L3-Umzug (2026-08-08, Durchführung s. TP21.1) sind durch — `docs/hw-refresh-requirements.md`, Memory `project_mikrotik_commit_nautobot_gc`
> 🔌 **Scope-Änderung 2026-08-16 (User-Entscheid): die DC-USV ist aus diesem TP herausgelöst und heißt jetzt **TP24**.** Sie war der letzte offene Rest hier, hat mit Switching/Routing sachlich nichts zu tun und trägt eigene Entscheidungen (Elektrik, Batterie, Standort). TP15 ist damit inhaltlich zu.
> *Vorherige Header-Fassung (Stand 2026-08-05, überholt): „🟢 CRS326-Bring-up + Branch-Tausch DURCH (2026-07-23); Core-Tausch/L3-Umzug **vorbereitet, Cutover-Config materialisiert (2026-08-05)**, Cutover = nächstes Wartungsfenster."*
- ✅ **BRING-UP BEIDER CRS326 + BRANCH-TAUSCH gegen sw2 — DURCH (2026-07-23, Claude via REST unter Governance-Ausnahme #2).** Ablauf/As-built:
@@ -824,7 +825,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp16-1"></a>
### TP16.1 — Private Access via native Cloudflare Mesh (WireGuard) auf `crs326-core` ▸ ✅ **ABGESCHLOSSEN (2026-08-17)** — `docs/tp16.1-warp-mesh-native-crs326.md`
### TP16.1 · Gitea #57 — Private Access via native Cloudflare Mesh (WireGuard) auf `crs326-core` — `docs/tp16.1-warp-mesh-native-crs326.md`
- ✅ **Kernergebnis: ICMP ist repariert.** Privater Zugang unterwegs läuft über **natives WireGuard auf `crs326-core`** statt cloudflared-QUIC — **0 % Ping-Loss auf alle BF-VLANs, vorher 100 %**. Genau das Symptom aus dem Incident 2026-07-16, das dieses TP ausgelöst hat. Öffentliche FQDNs unberührt auf dem 3-Tunnel-HA (TP16). Compliance **68/68**.
- ✅ **§4.0 erledigt 2026-08-17: Mesh-Node angelegt.** `bf-core-mesh` = **`d1da7c8a-2736-4cbb-8af1-32db5393fb9c`**, Register-Token in `mcp/cloudflare/.env` (`CF_MESH_REGISTER_TOKEN`, gitignored). ➡️ **Die seit 2026-07-17 offene Free-Plan-Frage ist beantwortet: der Free-ZT-Plan erlaubt Cloudflare Mesh — der LXC-Fallback ist vom Tisch.**
- 🔑 **Der Weg dahin war ein Token-Problem, kein Plan-Problem (kostete 2 Anläufe).** Der alte User-Token hatte **zwei** fehlende Permissions (§4.0 *und* §4.1 — `devices/settings` war schon lesend `403`). Ersetzt durch einen **account-owned** Token (`9984078812bc…`, ⏰ **gültig bis 2027-01-09**). Zwei Fallen, beide sehen aus wie „Token kaputt": (1) Account-Tokens verifizieren an `/accounts/{id}/tokens/verify` — am alten `/user/tokens/verify` melden sie `1000 "Invalid API Token"`, **während alle echten Endpoints 200 liefern**; (2) Dashboard-Fehler `"Account tag in access policy must match tag in request uri"` = eine **Zone**-Permission (`DNS`) steckt in der Policy-Zeile mit **Account**-Ressourcen → braucht eine eigene Zeile. Verifiziertes Soll-Set + Permission-IDs in `docs/tp16.1-…md` §4.0. **Merke: Dashboard sagt „Edit", API sagt „Write".**
@@ -864,7 +865,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp17"></a>
### TP17 — Shelly-Übersiedelung VLAN1 → VLAN30 (IOT) + `shelly-mcp` ▸ ✅ **KOMPLETT (User-Bestätigt 2026-08-13): alle Shellys auf VLAN30** — Geräte-PW *(war TP17.2, seit 2026-08-24 **TP30** Posten #3)* — `mcp/shelly/`, Runbook `docs/tp17-shelly-go-runbook.md`
### TP17 · Gitea #58 — Shelly-Übersiedelung VLAN1 → VLAN30 (IOT) + `shelly-mcp` — Geräte-PW *(war TP17.2, seit 2026-08-24 **TP30** Posten #3)* — `mcp/shelly/`, Runbook `docs/tp17-shelly-go-runbook.md`
- ✅ **Abschluss 2026-08-13 (User-Bestätigt):** auch die zuletzt offenen saisonalen Shellys (s1/s2/s3/s12) sind inzwischen umgeklemmt — **alle Shellys auf `dirtyvader-iot`/VLAN30**, kein VLAN1-Shelly-Rest mehr. Migration-Teil von TP17 damit vollständig abgeschlossen.
- ✅ **GO durchgezogen 2026-07-13 (16 Shellys VLAN1→VLAN30, 0 Fehler):** Pilot s26 + Welle A (s4–s9) + Welle B (s10/11/13/15/17/19/20/21/25) → alle auf `dirtyvader-iot`/`10.40.30.<octet>` verifiziert (SSID+IP+rssi). Mit den 6 Vorab-Migrierten (s14/16/18/22/23/24) = **22/26 auf VLAN30**; VLAN1-Sweep `.180-250` = **0 Shellys**. Restliche 4 saisonal ausgesteckte (s1/s2/s3/s12) inzwischen ebenfalls migriert (s.o.).
- 🔧 **Anker-Verfahren (User-Idee, statt sta1-Backup):** je Gerät **vor** dem sta-Write die geräteeigene **AP-Funktion** aktiviert (`WiFi.SetConfig ap.enable` — Firmware verlangt WPA2, kein offenes AP → PSK=IOT-PSK, live/kein Reboot) = Notanker, falls VLAN30-Join scheitert (Gerät bleibt über `Shelly*-<mac>`-AP erreichbar, **kein physischer Reset**). Nach Verify alle 22 AP-Anker wieder `deanchor`t. Runner: `mcp/shelly/migrate.py` (`migrate`/`deanchor`/`auth`/`table`, Dry-Run default, stoppt bei erstem Fehler, reused `discover.py`).
@@ -911,7 +912,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp19"></a>
### TP19 — cudy-Teardown + VLAN1-Client-Retirement ▸ ✅ **GESCHLOSSEN (2026-08-18)** — `docs/tp19-vlan1-cudy-teardown-runbook.md`
### TP19 · Gitea #59 — cudy-Teardown + VLAN1-Client-Retirement — `docs/tp19-vlan1-cudy-teardown-runbook.md`
- ✅ **Abschluss-Sweep 2026-08-18, alles live gegengeprueft (reine Reads):**
- **VLAN1 ist leer:** `/ip arp` auf `bridge` = **0 Eintraege, 0 reachable**. Nicht Restverkehr,
sondern gar nichts. Der Dual-DHCP-Race aus `project_vlan1_dhcp_incident` hat damit endgueltig
@@ -1008,7 +1009,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
- **Teardown-Sequenz (angepasst an den Live-Stand):** VLAN1-Client-Sweep ist **schon fast leer** (nur sw1-Mgmt + pve-Interim reachable) → `defconf`/pool-v1 ist **bereits disabled** → cudy-Abbau **entfällt** (cudy1 umgenutzt statt abgebaut, cudy2 tot) → ✅ **SwitchBot + Twinkly erledigt (2026-07-16)** → **verbleibt:** Media-Check (B1.3), pve-`.41.100` abräumen (B1.5), Interim-Krücken bewerten (ap2 `frame-types=admit-only-vlan-tagged`, `st-main rrm/wnm`), Inventar/`ip-plan`/DNS `.41→Ziel` final.
- Abh.: **TP14 ✅** (Hosts raus aus VLAN1), **TP17 ✅** (Shellys raus). Verwandt: `project_vlan1_dhcp_incident` (cudy-Dual-DHCP-Leak), TP5 (Deny-by-default danach sauberer). SSoT hier + `docs/tp19-vlan1-cudy-teardown-runbook.md`. Folge: **TP19.1**.
#### TP19.1 — VLAN99-MGMT-Cutover (sw1/sw2/ap2-Mgmt VLAN1 → VLAN99) ▸ ✅ ERLEDIGT (2026-08-14 nachgezogen) — alle lebenden Geräte auf VLAN99, die beiden Rest-Punkte gegenstandslos
#### TP19.1 · Gitea #60 — VLAN99-MGMT-Cutover (sw1/sw2/ap2-Mgmt VLAN1 → VLAN99) — alle lebenden Geräte auf VLAN99, die beiden Rest-Punkte gegenstandslos
- ✅ **Abschluss-Nachtrag 2026-08-14 (Status-Korrektur, kein neuer Change):** der Header stand noch auf „ap2 vertagt, sw1 offen" (Stand 2026-07-16) — beides ist durch spätere TPs eingelöst bzw. gegenstandslos geworden, ohne dass es hier nachgetragen wurde. Ist-Stand: **core `.99.1` · branch `.99.2` · ap1 `.99.11` · ap2 `.99.12`** — jedes lebende Netzgerät hat sein Mgmt auf VLAN99. **sw1/sw2 existieren nicht mehr** (physisch ausgebaut, in Nautobot per TP22 Schritt 5 auf *Decommissioned*). Damit ist TP19.1 vollständig; offen bleibt nur der VLAN1-**Client**-Teil, und der gehört zu TP19 (Hauptteil), nicht hierher.
- **Scope:** die einzigen Dinge, die TP19s Hauptteil bewusst in VLAN1 belässt — sw1 (`.41.11`), sw2, ap2-Mgmt (`.41.2`) — auf VLAN 99 (MGMT, `10.40.99.0/24`) umziehen. Erst danach ist VLAN1 wirklich **komplett** weg, nicht nur als Client-Netz.
- **User-Entscheid (2026-07-16):** kein eigenständiges TP-Nummer — bewusst als **Unterpunkt von TP19** geführt („VLAN1 killen"), nicht als TP22 o.ä.
@@ -1023,7 +1024,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp20"></a>
### TP20 — Platform-Datenbank (Postgres Precious-Tier + MariaDB Tier-2) ▸ ✅ **Plattform live (2026-07-16)** — Nautobot-DB angelegt, vw-Migration (TP20.1) offen — Memory `project_mikrotik_commit_nautobot_gc`
### TP20 · Gitea #61 — Platform-Datenbank (Postgres Precious-Tier + MariaDB Tier-2) — Nautobot-DB angelegt, vw-Migration (TP20.1) offen — Memory `project_mikrotik_commit_nautobot_gc`
- ✅ **Platform-Postgres live (2026-07-16):** LXC 105 auf pve, `platform-pg.bf.lan` / `10.40.20.20`, Ubuntu 24.04 (native PG16-Paket, kein PGDG-Repo nötig — bewusste Abweichung von der sonstigen Debian/community-scripts-Konvention der übrigen pve-LXCs, aus PG16-Verfügbarkeitsgründen). Cluster mit `--locale en_US.UTF-8 -e UTF8` neu initialisiert (Erstinstallation lief in `C`/`SQL_ASCII`, für Nautobot ungeeignet, korrigiert). `wal_level=replica` gesetzt; **`archive_mode` bewusst noch NICHT an** — ohne pgBackRest/Garage-S3 (TP18) zum Prunen würde archivierte WAL unbegrenzt wachsen und die kleine 8GB-Disk füllen; wird nachgezogen sobald TP18 steht. Rolle+DB `nautobot` angelegt, `pg_hba.conf` erlaubt `scram-sha-256` aus `10.40.20.0/24`. End-to-End-Verbindungstest (Netzwerk, nicht nur localhost) erfolgreich. Creds in `postgres/.secrets` (gitignored, `chmod 600`).
- **Auslöser:** Mehrere Dienste brauchen zentrale DB (Nautobot-SoT [TP21], Vaultwarden, HA-Recorder). Grundsatzentscheid statt Nautobot-Insellösung — „wir skalieren schneller als wir schauen können" (User).
- ⚠️ **Abh.-Korrektur (User 2026-07-16): TP18 (Garage-S3) ist KEINE Vorbedingung mehr — „hat mit dem Netz nichts zu tun", transitiv aus der TP21-Kette entfernt.** pgBackRest→Garage-S3 (s.u.) wird **nachgezogen sobald Garage existiert**, blockiert aber nicht mehr den Start. `wal_level=replica`/`archive_mode` können mit einem Platzhalter/lokalen Archive-Ziel hochgezogen werden, produktives Off-Site-PITR kommt später.
@@ -1047,7 +1048,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp21"></a>
### TP21 — Nautobot Golden Config (config-as-code Control-Plane) ▸ 🟢 **Plattform + Datenmodell + Templates live**
### TP21 · Gitea #62 — Nautobot Golden Config (config-as-code Control-Plane)
- 🔴 **BETRIEBSBEFUND 2026-08-18: der Stack hat sich zweimal in einer Nacht selbst umgebracht — Ursache
war der Image-Healthcheck, nicht Celery.** `nautobot-worker` und `-scheduler` erben aus dem Image
einen `HEALTHCHECK`, der alle **10 s** `nautobot-server health_check` startet — einen kompletten
@@ -1111,7 +1112,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp21-1"></a>
### TP21.1 — Core-Cutover-Recovery (ap1 → crs326-core als L3-Router) ▸ ✅ NETZ WIEDER LIVE (2026-08-08); ~~Aufräum-Todos offen~~ → **alle fünf erledigt, letzter live gegengeprüft 2026-08-23** (s. Nachtrag am Ende)
### TP21.1 · Gitea #63 — Core-Cutover-Recovery (ap1 → crs326-core als L3-Router) — ~~Aufräum-Todos offen~~ → **alle fünf erledigt, letzter live gegengeprüft 2026-08-23** (s. Nachtrag am Ende)
> **Kern-Erkenntnis (wichtig gegen Doku-Verwirrung):** **`crs326-core` IST jetzt der produktive L3-Router.** Der Core-Cutover (TP15) wurde faktisch vollzogen — ap1 wurde plattgemacht/getrennt, core hat Routing/DHCP/DNS/NAT/WG-Hub übernommen. core-Mgmt = **`10.40.99.1`** (vlan99-SVI) bzw. same-subnet-Fallback **`10.40.10.1`** (RouterOS-Version 7.19.5 identifiziert das Gerät zweifelsfrei als CRS326, nicht als hAP/ap1). Die CLAUDE.md-Zeile „core grundkonfiguriert, NICHT produktiv" ist damit **überholt**.
- **Verlauf:** Ausgangssymptom war ein L3-Ausfall-Eindruck (kein DHCP, „kein Routing nach .20.x"). Ursache = **unvollständiger Cutover**: core hatte L3-SVIs + Firewall, aber es fehlten DHCP-Server, WAN-Egress, WG-Hub und der DNS-Resolver-Upstream. ap1 (bisher alleiniges L3-Gerät) war bereits Werksreset + getrennt.
- ✅ **DHCP wiederhergestellt** aus letztem ap1-Backup (`routeros/ap1-pre-core-cutover-20260806-073616.rsc`): 7 Server (`dhcp-v10…v99`) + 8 Networks + **54 statische Reservierungen** + Option `dns 1.1.1.1`. Reproduzierbar in `routeros/tp21-core-dhcp-from-ap1.rsc`. Legacy VLAN1 (pool-v1/defconf) + tote omada-inform-Option bewusst weggelassen.
@@ -1152,7 +1153,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp22"></a>
### TP22 — Device-Adoption + Golden-Config Compliance-Push (RouterOS-Fleet) ▸ ✅ **DURCH (2026-08-13)**, Compliance-Backlog **abgearbeitet 2026-08-16 → 68/68, 0 Drifts** — alle 5 Schritte erledigt (Schritt 4 per Entscheid „keine Secret-Injection"); aus TP21 ausgelagert 2026-08-08
### TP22 · Gitea #64 — Device-Adoption + Golden-Config Compliance-Push (RouterOS-Fleet) — Compliance-Backlog **abgearbeitet 2026-08-16 → 68/68, 0 Drifts** — alle 5 Schritte erledigt (Schritt 4 per Entscheid „keine Secret-Injection"); aus TP21 ausgelagert 2026-08-08
- **Ziel:** die 4 Live-Geräte (crs326-core, crs326-branch, ap1, ap2) real unter **Nautobot Golden Config** nehmen — Backup → Compliance/Drift → intended → **Push (`enable_deploy`)**. Damit greift der Governance-Entscheid „Config exklusiv über Nautobot" operativ. Macht die aktuellen `routeros/.secrets/*.rsc`-Handoffs mittelfristig überflüssig.
- **Stand aus TP21:** Plattform + Datenmodell + Git-Template + intended-config für core stehen; ap1/ap2 als reine APs zurückgeschrieben (`nautobot/ingest-ap-rebuild.py`, 2026-08-08). `enable_plan`/`enable_deploy` in `nautobot_config.py` noch **bewusst aus** (pull/read-only).
- ✅ **Schritt 0 DURCH (2026-08-13) — Backup-Scrubbing + Repo-Neuanlage.** Vorgezogen aus Schritt 4, weil ohne Scrubbing jedes weitere Backup Secrets geleakt hätte.
@@ -1233,7 +1234,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp26"></a>
### TP26 — `/ip firewall filter` unter Drift-Kontrolle ▸ ✅ **DURCH (2026-08-17)**
### TP26 · Gitea #65 — `/ip firewall filter` unter Drift-Kontrolle
> **Nachgetragen 2026-08-18.** TP26 existierte bis dahin nur als zwei Commits und eine `.rsc` — ohne
> Abschnitt in dieser Datei. Genau die Lücke, die eine frische Session nicht finden kann.
@@ -1254,7 +1255,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp27"></a>
### TP27 — RouterBOOT-Firmware auf 7.24 nachziehen ▸ ✅ **DURCH (2026-08-17/18)**
### TP27 · Gitea #66 — RouterBOOT-Firmware auf 7.24 nachziehen
> **Nachgetragen 2026-08-18.** Auch TP27 hatte keinen Abschnitt — die Arbeit steckt in Christophs Commit
> `b93514a` plus `routeros/tp27-routerboot-firmware-724.rsc` und
> `docs/routerboot-firmware-klickanleitung.md`.
@@ -1273,7 +1274,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp28"></a>
### TP28 — WLAN über CAPsMAN auf `crs326-core` ▸ ↩️ **WIEDER GEÖFFNET 2026-08-30**
### TP28 · Gitea #18 — WLAN über CAPsMAN auf `crs326-core`
> Der Volltext steht seit dem 2026-08-30 wieder in
> [`teilprojekte.md` → TP28](teilprojekte.md#tp28), weil mit **ap3** (cAP ax) ein dritter
@@ -1283,7 +1284,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp-r"></a>
### TP-R — Router-Capabilities-Sweep ▸ FERTIG (`docs/research/routeros-capabilities.md`)
### TP-R · Gitea #67 — Router-Capabilities-Sweep — `docs/research/routeros-capabilities.md`)
- **Leitsatz: nativ vor Container, NAS vor Router** (128MB-Flash-Limit).
- **Sofort nativ (~0 RAM):** RoMON · Netwatch · Scheduler + Auto-Backup/Mail · SNMP→externes Monitoring · **mDNS-Repeater** · spectral-scan · **802.11r/k/v (Fast-Roaming statt Mesh)** *(die Sweep-Fassung nannte hier CAPsMAN — das ist seit 2026-08-08 abgeschafft; 11r/k/v läuft standalone weiter, s. TP3/TP3.3)* · Back-to-Home/WireGuard.
- **Auf NAS:** FreeRADIUS(+LDAP) · NPM · icinga/Grafana · CF-Tunnel.
@@ -1295,13 +1296,529 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
---
<a id="tp25"></a>
## TP25 — Kondensat (aktiver Stand bis 2026-09-15, abgeschlossen)
> ✅ **TP25 ist ABGESCHLOSSEN (2026-09-15).** Platzierung final (ap1 Sideboard · ap2 Technik UG ·
> ap3 Technik OG), Fund E geschlossen (`7.24.1` war die Ursache, nicht `ft-over-ds`), alle vier
> Handoff-Punkte aufgelöst, Gitea #17 closed. Die 394-Zeilen-Herleitung (Messchronik A–G) steht
> weiter unten unter [TP25 — Langfassung](#tp25-lang).
### TP25 · Gitea #17 — WLAN-Stabilisierung — ap2-UNTERPUNKT ✅ WIDERLEGT 2026-09-14 (In-situ, [Handoff](handoff-20260914-tp25-ap2-insitu-terrassse.md)): ap2 sendet auf 2 m mit −49…−51 dBm, stabil, 0 Roams — Hardware sauber, kein Tausch begründet. Terrasse = 5-GHz-Zellkante (−76…−81 vs. 2,4 GHz −65…−73), Thrash nur in der Band-Überlappungszone −70…−77. Hebel „Access-List-Band-Boden" in der Machbarkeitsprüfung GESCHEITERT (kein `band`-Property, cap-wifiN-Rejoin-Falle) — s. Block unten** — **FUND E ✅ GESCHLOSSEN 2026-09-02: die Abschlussmessung ist ruhig (4 reauth in 8,6 h, 0 Ausfälle, Verlauf 218→40→4) ⇒ `7.24.1` (TP27) war die Ursache, nicht `ft-over-ds`. Hebel 1 widerlegt+zurückgebaut, Hebel 2–4 entfallen, kein Geräte-Write** ([Handoff](handoff-20260902-tp25-fund-e-messung.md)) · Kern erledigt: 🟢 **(2026-08-22) Dead Spot Terrasse repariert** (43 % → 9 % Verlust, 20,4 s → 1,0 s). **Fund F ✅ aufgelöst (08-24, geräteseitig — kein Netzfehler)**, **5-GHz-Umfeldscan ✅ (08-24)**, Kanalplan für AP 3 ✅ gerechnet. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6
> 📚 **Kondensiert am 2026-08-23 (Doku-Session, Punkt F).** Dieser Abschnitt war mit 394 Zeilen
> 18 % der Datei. Die **Langfassung steht vollständig und unverändert** im Archiv:
> [`teilprojekte-archiv.md` → TP25 (Langfassung)](teilprojekte-archiv.md#tp25-lang) — Messchronik (A)–(G),
> Funde A–E im Volltext, die beantworteten Fragen (Beobachtung 5, ch149-Herkunft, `2ghz-g`,
> Deckenmontage), Umsetzungsreihenfolge mit allen Deploy-Gotchas. Hier steht: **Stand, Offenes,
> Merkregeln.** Wer an TP25 weiterarbeitet, kommt mit diesem Abschnitt aus; wer wissen will
> *warum* etwas so entschieden wurde, liest die Langfassung.
**Stand 2026-09-15 — AP-Zuordnung DEFINITIV geklärt, Tausch-Befund ap2↔ap3** ([Handoff](handoff-20260915-tp25-zuordnung-und-redundanz.md)). Positionen per IP+MAC+Switch-Port (MNDP/Bridge-Host/Port-Link) live belegt: **ap1 = Sideboard/Wohnzimmer (`core ether3`), ap2 = Technik UG (`core ether21`), ap3 = Technik OG (`branch ether24`)**. Der geplante Tausch (ap1↔ap3, cAP ax → Sideboard) hat faktisch **ap2↔ap3** gedreht — ap1 blieb stehen, der cAP ax steht jetzt in OG. MAC-Korrektur: ap2 `ether1` ist `:39`, nicht `:3A` (`:3A` = Bridge/Mgmt-MAC). Rundgang (natürlicher Zustand, 1-s-Takt): **0 Disconnects**, −45…−77 dBm, Roaming über alle 3 APs; **Terrasse −58 stabil** (jetzt cAP ax/OG, 2,4G), ap1 nur kurz gestreift. ap1-Aus-Test: Main-Client voll abgedeckt (tiefster Wert −80), **alle 27 IOT kehren zurück**, aber ~4 Wohnzimmer-IOT marginal −77…−80 ⇒ ap1 technisch redundant, nicht ausfallkritisch. ap1 wieder an (Revert). **Aufgelöst 2026-09-15 ([Steering-Handoff](handoff-20260915-tp25-steering-threshold-und-terrasse.md) §6):** (1) ap1 **bleibt** am Sideboard, (2) Tausch-Korrektur **verworfen** — die Prämisse „Sideboard-AP deckt die Terrasse" war widerlegt (ap1 = Innenraum-König, cAP ax gehört nach OG), (3) Drei-Räume-Schatten **verworfen** (Signal beim Rundgang gut), (4) Doku-Drift ✅ geschlossen. **Platzierung final: ap1 Sideboard · ap2 UG · ap3 OG.**
**Merke:** `no cached PMK` im Log ≠ FT-Problem — es tritt auch auf `ft=no`-SSIDs (IOT) auf und ist ein harmloser PMK-Cache-Miss (voller Handshake statt Fast-Reassoc). Der 09-15-Abriss war die Terrassen-Bandkante, nicht PMK/FT.
**Stand 2026-09-14, 20:05 — ap2-In-situ: Hardware entlastet, Terrasse = 5-GHz-Zellkante,
Band-Boden nicht sauber ausdrückbar** ([Handoff](handoff-20260914-tp25-ap2-insitu-terrassse.md)).
Live-Session auf der Terrasse (Christoph kooperierte physisch — der Test, der am 08-31 „nachts
nicht möglich" war): das Fund-E-iPhone flappte 18:53–19:18 ~30×, fast ausschließlich
**ap2-5G ↔ ap2-2,4G** (−68…−81), mit 3 Abwürfen (2 Call-Aussetzer). Vor ap2 (1–2 m): **−49…−51 dBm,
>2 min stabil, 0 Roams** ⇒ der offene Verdacht „ap2 RF/Antenne/HW" (23:02-Befund, 2 m/−80) ist
**in situ widerlegt** — der 23:02-Wert reproduziert sich seit dem 09-04-Kanalplan (5300/5320,
tx-power 18 live) nicht mehr. Am Terrassenplatz ist **2,4 GHz klar besser** (−65…−73 vs. 5 GHz
−76…−81) und das Telefon dort **ruhig** (36 s im 1-s-Takt, 0 Roams) — es thrasht nur in der
Überlappungszone −70…−77 beider Bänder; zusätzlich client-getrieben (floh 19:44 selbst von −52
auf 2,4/−77; Steering hat keine Schwellen, der AP drängt nichts). Mit `ft=no` ist jeder Roam ein
spürbarer Voll-Reconnect ⇒ so sterben Anrufe. **Hebel „Access-List-Boden < −76 auf 5G":
Machbarkeitsprüfung negativ** — `/interface/wifi/access-list` hat kein `band`-Property,
Interface-Scoping bricht am cap-wifiN-Rejoin, SSID-/MAC-Scope träfen 2,4 GHz mit (Schlafzimmer
−84 ⇒ Lockout). **Nicht umgesetzt.** Verbleibende Hebel: (1) ✅ **DURCH 2026-09-14, 20:30 Uhr** — `ft=yes ft-over-ds=yes`
uniform auf `sec-main` (Ruecknahme der 09-04-Entscheidung; Go per Fragenrunde nach Dry-Run). Weg:
Template-Edit + `push-gc-template.py` (GC-Commit `744a17e3`) → `--refresh-compliance` (Drift
`wifi-security` wie erwartet) → `gc-manual-plan.py --deploy` (Plan `b1135f3e`, Completed, Asserts gruen;
ein Polling-Timeout am Skript war folgenlos — Geraet stand bereits auf ft=yes) → Refresh: **140/140, 0
Drifts**. CAP-Abnahme: **keine** Inline-Overrides auf ap1/ap2/ap3. Commands-File:
[`docs/transaktionen/TP25-ft-yes-secmain.commands.rsc`](transaktionen/TP25-ft-yes-secmain.commands.rsc).
Abnahme ✅ **DURCH (2026-09-14, 20:33–20:34 Uhr, live)**: Anruf auf der Terrasse ueberstand
**fuenf Band-Wechsel binnen 60 s** (nur `roamed to`, keine `disconnected` mehr im Fenster —
Christoph: „call steht"). (2) Deckungsfix
Variante 3 (ap1-5G erreichte die Terrasse mit −64) — beendet das Springen selbst.
Nachtrag 20:37: ein **Totalverlust** anderer Klasse (Roam auf ap1-2,4 bei −89, danach iOS-Aufgeben
ohne jeden Reconnect-Versuch; identisch schon 19:01 mit ft=no gesehen) — **keine ft=yes-Regression**,
sondern der strukturelle Rest. **Wechselrichter/DTU (Hoymiles HMS-800W-2T, geht mit der Sonne an/ab,
09-14: 07:09–19:35): nachts unbeteiligt; tagsüber ~5:05-Zyklus (connect/`connection lost`/reconnect +
DHCP-Churn) auf ap2-2,4/ch11 bei −53.** Messplan „DTU vs. Terrasse" angelegt ([Issue #17, Kommentar
#225](https://gitea.emsenhuber.net/admin/bf-infra/issues/17#issuecomment-225)): Referenz-Client auf
ch11 pinnen (tp25-Methodik), 30–60 min Ping, Korrelation gegen die 5:05-Taktmarken. ⚠️ **Korrektur
09-14 abends ([Kommentar #227](https://gitea.emsenhuber.net/admin/bf-infra/issues/17#issuecomment-227)):
Der urspruenglich genannte Hebel „OpenDTU/AhoyDTU-Dongle" existiert fuer DASSES Geraet NICHT** — HMS-
**XXXXW**-2T (W = eingebaute DTU „DTUBI", protobuf-Protokoll) wird von AhoyDTU/OpenDTU nicht
unterstuetzt (lumapu/ahoy#1189 seit 09-2023 offen; Christophs eigene AhoyDTU nachweislich
inkompatibel; nur die Nicht-W-Variante HMS-800-**2T** ginge per CMT2300-Funk). Die lokale Ebene
(hms-mqtt-publisher = „hmsmqtt", dtuGateway, ha-hoymiles-wifi) redet **mit der internen DTU uebers
Netzwerk** — keines davon nimmt die DTU von der Luft, hmsmqtt pollt sogar zusaetzlich. **Einziger
Airtime-Hebel bleibt DNS-Hoheit ueber die Cloud**, von Christoph bewusst zurueckgestellt (Cloud-
Aufzeichnungen noch benoetigt, bis hmsmqtt + WLAN stabil); ob eine DTU ohne Cloud ruhiger oder
gespraechiger wird, ist selbst eine offene Messfrage. Pin auf anderen AP kontraindiziert
(−53 direkt an ap2); 5-Min-Takt ist S-Miles-Telemetrie per Design, kein Ethernet an der DTU-WLite.
✅ **Netzwerk-Mechanismus geklaert (09-14, Nacht; [Issue #17, Kommentar
#228](https://gitea.emsenhuber.net/admin/bf-infra/issues/17#issuecomment-228)):** Der **Fund-F-5:05-Zyklus
ist das Cloud-Recovery-Verhalten der DTU.** Wireshark-Analyse (hms-mqtt-publisher PR #110, HMS-800W-2T):
lokaler Poll <30 s resettet den Countdown und setzt Cloud-Updates aus; **nach 4–5 min erfolgloser
Cloud-Updates** startet die DTU eine neue TCP-Session — und dabei (unser Log-Beweis 09-14) eine
**vollstaendige WiFi-Disassoziation inkl. DHCP-Churn**. Die heute gemessenen Zyklus-Abstaende decken
sich mit dem Befund aufs Minutenkomma. Ursache war hmsmqtt mit **30 500 ms** (Grenzzone);
DTU-Firmware aelter, hmsmqtt funktioniert (⇒ #134-Key-Rotation-Falle nicht primaer).
**Hebel = Poll-Rate, nicht DNS-Hoheit:** Christoph hat **30500 → 45000 ms** gesetzt (09-14 abends;
Community-Wert, 10 Monate stabil). ✅ **Verifikation DURCH (09-15, 12:11; [Issue #17, Kommentar
#230](https://gitea.emsenhuber.net/admin/bf-infra/issues/17#issuecomment-230)): DTU online seit 06:41:49,
uptime 5 h 29 min, 0 Disconnects** (Vergleich 09-14 mit 30,5 s: fuenf Zyklen in den ersten 18 min).
Die DTU sitzt heute auf **ap3-2,4** (cap-wifi4-virtual1, −49; cap-Karte unveraendert, Attribution
belastbar) — also aktuell auch vom Terrassen-Band ch11 weg. **Fund F ist damit abgeschlossen** —
Ursache war die DTU-Cloud-Recovery, getriggert durch hmsmqtt-Polling-Kollision. Messplan #225 ist
gegenstandslos (der zu messende Effekt existiert nicht mehr); eine ch11-Tagzeitmessung bleibt nur
sinnvoll, falls die Terrasse tagsueber weiterhin Call-Probleme zeigt. 📅 **terminiert ([Issue #17,
Kommentar #232](https://gitea.emsenhuber.net/admin/bf-infra/issues/17#issuecomment-232)): Deckungsfix
Variante 3 — Umsetzung 2026-09-15/16** (Christoph: „heute oder morgen"): Deckenstrahler an den guten
Platz, verdrängter hAP in den Drei-Raeume-Schatten (Schlafzimmer −84,6/38 %, Bad −82,3/24 %,
Kueche −82,0/23 % laut 08-22-Messung). Vorher-Messung Terrasse (Referenz 09-14: 5 GHz −76…−81,
2,4 GHz −65…−73) → Umsetzung → Nachher-Messung + Anruf-Abnahme; Kanalplan (09-04) bleibt unberuehrt,
nur Platzierung aendert sich; Config-Aenderungen nur ueber den GC-Weg. Offen vor Ort: Zielraum fuer
den verdraengten hAP (PoE/Steckdose) und ap1-Uplink-Thema (Sideboard → `core ether3`) mitdenken.
Randnotizen: ap1 „FUNKSTILL" im CLAUDE.md ist überholt (dient live, Radar-Event 16:51 auf 5280
sauber abgehandelt); Template-Abschnitt [6c] („Fund E WIEDER OFFEN", Stand 08-30) ist inhaltlich
überholt — beim nächsten Template-Edit mit aktualisieren, kein separater Edit nur für den Kommentar.
**Stand 2026-08-22, 23:30 — die Terrasse ist repariert.** Erster Lauf mit beiden Ping-Strecken
([`tp25-runde-20260822-2330-auswertung.md`](tp25-runde-20260822-2330-auswertung.md)): **43 % Verlust /
20,4 s längste Lücke (08-21) → 9 % / 1,0 s** bei gleichem Gerät und gleicher Rolle. Wirksam war das
**Anheben von ap2 auf 2,5 m**; 1,0 s liegt unter der Hörbarkeitsschwelle im Call. Der Dead Spot, der
TP25 seit dem 08-20 getragen hat, ist damit weg.
🔎 **Kernbefund, der bleibt: das Netz ist gut, die Redundanz ist es nicht.** Die frei roamende Referenz
hat in **17 von 21** Raumabschnitten 0 % Verlust; der Gegen-AP liefert Schlafzimmer −84,6/38 %,
Bad −82,3/24 %, Küche −82,0/23 %, Terrasse −78,7/19 %. **Fällt ein AP aus, ist die halbe Wohnung
„verbunden, aber tot".** ⇒ Ein **dritter AP ist nach Christophs eigener Randbedingung derzeit NICHT
begründet** — er kaufte Redundanz, keine Abdeckung. Das ist eine andere Frage und Christophs.
**Beide vormals offenen Punkte sind am 2026-08-24 abgearbeitet** — Fund F ist aufgelöst, der
5-GHz-Umfeldscan gefahren und der Kanalplan für einen dritten AP gerechnet. **Was von TP25 bleibt,
ist Umsetzung bei Bedarf, keine offene Frage:**
1. ✅ **Fund F — GEMESSEN UND AUFGELÖST (2026-08-24): kein netzseitiges Phänomen.**
Kollektor am **Manager** (`/interface wifi registration-table`, 15-s-Takt, 29 Samples über 7,7 min,
28 Clients), Rohdaten `docs/messdaten/tp25-fundf-20260824.log`. Von den drei Kandidaten sind **zwei
erledigt, einer bleibt** — und die zwei fielen ohne Wartezeit:
| Kandidat | Ergebnis | woran es hängt |
|---|---|---|
| **(b)** DHCP-`lease-time` gegen die Periode | ❌ **tot** | `dhcp-v30` fährt **`lease-time=1d`** → Renew bei 12 h. Kann 5:05 nicht erzeugen. **Eine Abfrage, keine Messung.** |
| **(a)** periodischer VAP-Re-Apply | ❌ **tot** | Ein Re-Apply wirft **alle** Clients des VAP gleichzeitig. Auf `cap-wifi4-virtual1` standen zeitgleich **drei Clients bei 16 h 51 min** Uptime, 25 von 28 über 6 h. Mit einem 5-Minuten-Takt ist das unvereinbar. |
| **(c)** das Gerät selbst | 🟡 **bleibt** | Der einzige Client, der im Fenster überhaupt neu verband. |
🔑 **Der entscheidende Punkt an der Beweisführung: dafür braucht es die vier Zyklen nicht.** Ein
einziges Sample mit **16 h 51 min Uptime auf genau dem VAP**, der angeblich alle 5:05 abwirft,
widerlegt (a) vollständig. Die Fensterlänge begrenzt nur, was man über *abwesende* Geräte sagen kann.
🔴 **Und genau die sind abwesend: die beiden Fund-F-MACs gibt es gerade nicht.**
`38:A5:C9:C0:73:1E` (`10.40.30.178`) und `38:A5:C9:BC:33:31` (`10.40.30.179`) stehen in `/ip arp` auf
**`status=failed`**, haben **keine DHCP-Lease**, stehen **nicht** in der Registration-Table und sind
**auch HA unbekannt** (kein `device_tracker` zu diesen MACs). ⇒ **Das Phänomen ist an diesen Geräten
derzeit nicht reproduzierbar, weil die Geräte weg sind.**
📌 **Merkregel — dieselbe wie beim P750W:** bevor ein periodisches Trennen als Funk- oder
Konfigurationsfehler untersucht wird, **erst prüfen, ob die betroffenen Geräte überhaupt da sind.**
Beim Drucker war die Fehldiagnose „Power-Save", hier wäre sie „VAP-Re-Apply" geworden.
⇒ **Fund F wird geschlossen:** was auch immer der 5:05-Takt war, er lag **geräteseitig**, und die zwei
betroffenen Geräte sind vom Netz. Netzseitig ist nichts zu tun. **Nicht erneut aufgreifen**, solange
`.30.178`/`.179` nicht wieder da sind *und* das Muster erneut gemessen wird.
🟡 **Nebenbefund mit Substanz — der DTU ist der einzige Wackelkandidat, den es wirklich gibt.**
`54:F2:9F:12:51:F0` (DTU/Wechselrichter, auf `cap-wifi4-virtual1` = **ap1**) war der **einzige** Client
mit einem Uptime-Rücksetzer: 43 min Sitzung, dann weg für ein Sample, dann wieder da — **bei −52 dBm**,
also bei tadellosem Signal. Das ist derselbe Mechanismus, der schon die Transaktion
`TP17.1-dtu-pin-ap2` begründet hat: **ein ESP hat EIN Radio, sein eigener AP und die STA-Verbindung
teilen sich den Kanal.** Periode hier 43 min, nicht 5:05 — es ist **nicht** Fund F, aber es ist echt.
⇒ Gehört zur ohnehin vorgesehenen Neubewertung des DTU-Pins, sobald der Kanalplan steht (s.o.).
<details><summary>📜 Ursprüngliche Fund-F-Formulierung (2026-08-22)</summary>
1. 🟡 **Fund F — IoT-Clients trennen exakt alle ~5:05 bei gutem Signal** (−59 dBm, vier Messpaare,
auf 3 s reproduzierbar → **ein Timer, kein Funk**). Muss **am Manager** nachgemessen werden, die
AP-Tabellen sind unter CAPsMAN leer. Kandidaten und der Test, der sie trennt:
(a) periodischer VAP-Re-Apply (Fund D war die Ursache → prüfen, ob es nach dem Fix weg ist);
(b) DHCP-`lease-time` der beiden MACs (`38:A5:C9:C0:73:1E`, `38:A5:C9:BC:33:31`) gegen die Periode legen;
(c) das Gerät selbst mit 5-min-Duty-Cycle → dann bleibt es unverändert.
🚫 **Nicht die alte Power-Save-Merkregel wiederbeleben** — die ist am 2026-08-13 als Fehldiagnose
zurückgezogen (`ref_wifi_powersave_multicast`).
</details>
2. ⚖️ **Entscheidung 5 GHz auf 20 MHz** (statt heute 20/40) — **✅ 5-GHz-Umfeldscan durchgeführt
2026-08-24, Empfehlung: NICHT verschmälern, Entscheidung an einen Auslöser hängen.**
⚠️ **2,4 GHz NICHT vorschnell drosseln** — dort hängen ~20 IoT-Geräte.
**Messung** (`/interface wifi scan` **am Manager**, cap-wifi3 = ap1 / cap-wifi1 = ap2, 10–12 s):
| vom Radio auf | fremdes 5-GHz-Netz | Kanal | Signal | Abstand zum eigenen AP |
|---|---|---|---|---|
| **ap1** (5180/Ce) | `Drei_H288A_5G_acHG` | 5745/ac/**80 MHz** | **−86 dBm** | 19 dB darunter |
| **ap2** (5220/Ce) | `LiwestBB93-5G` | 5200/ac/**80 MHz** (Block 5180–5240) | **−89 dBm** | 24 dB darunter |
⇒ **Das 5-GHz-Band ist an beiden Standorten praktisch leer.** Genau **ein** Fremdnetz je AP, beide
rund 20 dB unter dem eigenen Pegel. Der Nachbar `LiwestBB93-5G` liegt mit 80 MHz zwar formal über
**beiden** unserer Blöcke — bei −89 dBm ist das ohne Wirkung.
🎯 **Warum das gegen 20 MHz spricht — die zwei üblichen Gründe treffen hier nicht zu:**
- **Fremdinterferenz:** existiert nicht (−86/−89). Ein schmalerer Kanal hätte nichts abzuwehren.
- **Selbstüberlappung:** existiert nicht. ap1 `5180/Ce` = ch36+40, ap2 `5220/Ce` = ch44+48 —
überlappungsfrei (Fund C, 08-17).
- **Bleibt allein das +3 dB Link-Budget** — und das ist zum guten Teil **schon vorhanden**: in einem
40-MHz-BSS sendet die Ratenanpassung an schwache Clients ohnehin auf dem Primärkanal in 20 MHz.
Den Ceiling zahlen aber **alle** Clients dauerhaft. **Der Nutzen geht an die, die ihn schon haben.**
- **Und es löst das gemessene Problem nicht:** die offenen Schwachstellen sind die
Redundanz-Lücken (Schlafzimmer −84,6 / 38 %, Bad −82,3 / 24 %) **am Gegen-AP**. +3 dB macht aus
38 % Verlust keine 0 % — das ist eine Platzierungs-/AP-Zahl-Frage, keine Kanalbreiten-Frage.
🔑 **Der echte Auslöser ist die AP-Zahl, nicht die Interferenz** — und das ist der Grund, die
Entscheidung *jetzt* zu treffen statt sie offenzulassen. Beide Kanäle stehen hart in **Band A**
(`ch-5g-ap1`=5180, `ch-5g-ap2`=5220, kein `skip-dfs-channels`), und Band A hat unter ETSI genau
**4 × 20 MHz = 2 × 40 MHz**. Mit zwei APs ist es damit **exakt aufgebraucht**:
| Szenario | bei 40 MHz | bei 20 MHz |
|---|---|---|
| 2 APs (heute) | ✅ passt genau, überlappungsfrei | ✅ passt, +3 dB, halber Ceiling |
| **3.–4. AP** | ❌ Band A voll ⇒ **nur mit DFS** (Band B ch52–64: Radar-Events, 60 s CAC) | ✅ 4 saubere Kanäle, **kein DFS** |
⇒ **Empfehlung: 20/40 MHz beibehalten.** Die Verschmälerung ist heute ein Preis ohne Gegenwert.
Sie wird **fällig, sobald ein dritter AP dazukommt** — dann lautet die Wahl nicht mehr „20 gegen 40",
sondern **„20 MHz flottenweit" gegen „DFS akzeptieren"**, und dafür ist 20 MHz die ruhigere Antwort.
Ein Zwischenweg existiert (ap1 behält 40 MHz, AP 2+3 fahren 20 MHz auf 5220/5240) und braucht
ebenfalls kein DFS.
📌 **Damit ist dieser Punkt entscheidungsreif und an einen Auslöser gebunden statt offen.**
#### 📐 Kanalplan für den dritten AP ▸ **gerechnet 2026-08-24, DFS vom User genehmigt**
> **User-Vorgaben (2026-08-24):** 20/40 MHz bleibt · **DFS ist genehmigt**, soll aber auf **ap1 oder ap2**
> liegen, nicht auf dem Neuen · der Neue ist ein **MikroTik-Deckenstrahler mit ax** · **möglicherweise
> kommt er an die Stelle eines der bisherigen APs** · der vorhandene Outdoor-AP bleibt konzeptionell außen vor.
**Das Kanalbudget unter ETSI (Indoor), als Rechnung statt als Gefühl.** Ein 40-MHz-Block darf keine
Bandgrenze überschreiten — `[48+52]` ist deshalb unmöglich, und daran hängt die ganze Knappheit:
| Band | Frequenzen | Kanäle | 40-MHz-Blöcke | DFS | EIRP |
|---|---|---|---|---|---|
| **A** | 5150–5250 | 36/40/44/48 | **2** (`5180/Ce`, `5220/Ce`) | nein | 23 dBm |
| **B** | 5250–5350 | 52/56/60/64 | **2** (`5260/Ce`, `5300/Ce`) | **ja** + TPC | 23 dBm |
| **C** | 5470–5725 | 100–140 | **5** | **ja** + TPC | 30 dBm, outdoor erlaubt |
Heute belegen zwei APs mit 40 MHz **ganz Band A**. Genau das macht den dritten AP zur Bandfrage — und
mit genehmigtem DFS ist sie lösbar, ohne irgendwo auf 20 MHz zu gehen.
**Empfohlene Zuordnung (3 APs, alle 40 MHz):**
| AP | Rolle | Kanal 5 GHz | Band | DFS | 2,4 GHz |
|---|---|---|---|---|---|
| **cAP ax (neu)** | Primär, beste Position | **`5180/Ce`** (36+40) | A | **nein** | 2437 (ch6) |
| **ap1** hAP ax² | Sekundär | **`5260/Ce`** (52+56) | B | ja | 2412 (ch1) |
| **ap2** hAP ax² | Sekundär | **`5300/Ce`** (60+64) | B | ja | 2462 (ch11) |
| — | **Ausweichspur, frei halten** | `5220/Ce` (44+48) | A | — | — |
- **Warum Band B und nicht Band C für die zwei DFS-Radios:** gleicher 23-dBm-Deckel wie Band A (kein
Leistungssprung zwischen den APs) und **CAC 60 s statt 600 s** — die 10-Minuten-Prüfung gilt im
Wetterradar-Bereich von Band C. Band C bleibt damit frei für den Outdoor-AP, wo seine 30 dBm auch
hingehören.
- **Warum der Neue das nicht-DFS-Band bekommt:** DFS kostet **60 s CAC bei jedem Boot und jeder
Provisionierung** plus einen erzwungenen Kanalwechsel bei Radar-Erkennung. Diese Kosten gehören auf
die Radios, deren Fläche von einem anderen AP mitgetragen wird — nicht auf den bestplatzierten.
Christophs Instinkt ist hier richtig.
- **2,4 GHz fällt nebenbei sauber:** ch1/6/11 ist mit drei APs erstmals vollständig überlappungsfrei
möglich (heute 1/11 mit einer Lücke in der Mitte).
🔴 **Zwei Fallen, ohne die der Plan teurer wird als er aussieht:**
1. **Eine hart gepinnte Frequenz macht DFS zum Single Point of Failure.** `ch-5g-ap1`/`-ap2` tragen heute
**je genau einen** Wert (`frequency=5180` bzw. `5220`). Bei einer Radar-Erkennung **muss** das Radio
den Kanal verlassen — hat es keinen zweiten konfiguriert, bleibt das 5-GHz-Funk dieses APs **unten**.
⇒ Für DFS gehört in das Channel-Objekt eine **Liste** (z.B. `frequency=5260,5280,5220`), damit der AP
neu wählen kann. Das ist der Unterschied zwischen „DFS ist unkritisch" und „DFS kostet einen AP".
2. **Vor dem Umzug die *erreichte* Sendeleistung messen — das ist Fund B, nochmal.** Band B/C verlangen
TPC; ohne TPC sinkt der erlaubte EIRP um 3 dB. TP25 Fund B war exakt dieses Muster: **18 dBm gesetzt,
9 dBm gesendet**, weil ch149 unter ETSI leistungsbegrenzt ist. ⇒ **Diskriminierender Test, bevor
irgendwas gekauft oder umgezogen wird:** ap2 testweise auf `5260`, dann `/interface wifi monitor`
lesen und die tatsächliche `tx-power` gegen die heutigen 18 dBm auf `5220` stellen. Kostet eine
Config-Transaktion und zehn Minuten, und entscheidet, ob der Plan trägt.
⚖️ **Zur Platzierung — die Messung widerspricht dem Aufstellen an einer bisherigen Stelle.**
Die Referenzmessung vom 08-22 sagt: **17 von 21 Raumabschnitten haben 0 % Verlust.** Zusätzliche
*Abdeckung* ist damit wertlos. Der ganze Wert liegt im **Gegen-AP**: Schlafzimmer −84,6 / 38 %,
Bad −82,3 / 24 %, Küche −82,0 / 23 %.
| Variante | was sie bringt |
|---|---|
| Neuer AP an **bisheriger** Stelle | besseres Deckenbild an einem Punkt, **der schon funktioniert**. Die Drei-Zimmer-Lücke bleibt **unverändert** — gleicher Standort, gleicher Schatten. |
| Neuer AP an **neuer** Stelle (Richtung Schlafzimmer/Bad/Küche) | schließt die Lücke, also genau die Redundanz, die gekauft werden soll. |
| 🎯 **Beides: cAP ax an die bisherige Stelle, der verdrängte hAP ax² wandert in den Schatten** | bestes Deckenbild dort, wo die Clients sind, **und** eine zweite Option in den drei Räumen. Braucht keine vierte Antenne, nur eine Steckdose/PoE im Zielraum. |
⇒ **Empfehlung: Variante 3.** Sie ist der einzige Weg, bei dem der Deckenstrahler an den guten Platz
darf, *ohne* dass die gemessene Schwäche unangetastet bleibt.
- **Voraussetzung Neuer AP:** PoE-in am Zielort (die cAP-ax-Reihe hat einen Gigabit-PoE-Port, kein
Netzteil im Lieferumfang üblich) und ein Trunk mit VLAN 10/30/40/50/90/99 wie bei ap1/ap2. Der AP
läuft auf `wifi-qcom` und tritt damit als **regulärer CAP** dem Manager bei — kein neues Modell im GC,
nur ein weiteres Gerät plus zwei `channel`-Objekte. ⚠️ **Exaktes Modell vor dem Kauf prüfen**
(`cAP ax` vs. `cAP XL ax`) — beide sind ax/wifi-qcom, unterscheiden sich in Abdeckung und Preis.
- **Wenn später doch der Outdoor-AP dazukommt:** er gehört nach **Band C** (30 dBm, outdoor zulässig);
Band A/B sind dann von den drei Indoor-APs belegt und die Ausweichspur `5220/Ce` ist aufgebraucht.
#### 🔴 Fund E — **WIEDER GEÖFFNET 2026-08-30: die Wiedervorlage-Bedingung ist eingetreten**
Fund E war „abgestuft, NICHT umsetzen" mit einer ausdrücklichen Bedingung: *„Erst wieder
aufgreifen, wenn ein Sticky-Fall erneut gemessen ist."* Am **2026-08-30** ist genau das passiert.
**Auslöser war eine unabhängige User-Meldung**, nicht die Suche danach: *„die letzten Tage/Wochen
regelmäßig mit dem iPhone den WiFi-Link verloren obwohl bester Kontakt. Ich lande auf 5G und nicht
am alten AP oder dem anderen."* 🔑 **Auf Mobilfunk zu fallen ist kein Roaming-Problem** — bei
schlechtem Roaming bliebe das Telefon im WLAN und wäre nur langsam. Es heißt: die Assoziation ist
gestorben und die Neuanmeldung gescheitert.
**Neue Messung (Manager-Log, 2026-08-30):** dasselbe iPhone `F2:62:4C:E1:AF:09` machte **16
Bandwechsel in 20 Minuten** zwischen `cap-wifi5` (ap3, 5 GHz) und `cap-wifi6` (ap3, 2,4 GHz),
zweimal im Abstand von **einer Sekunde**, bei **−53 … −81 dBm**. Kurz darauf dasselbe zwischen
`cap-wifi1`↔`cap-wifi2` — also **an ap2**. ⇒ **Nicht ap3-spezifisch, sondern an jedem AP.**
Das deckt sich mit der Ursprungsmessung vom 08-20 (sechs Flaps in 22 Minuten) und mit
`…@wifi2 disconnected, not responding, signal strength −47` — **Abbruch bei ausgezeichnetem Signal**.
⚠️ **Zwei Symptome, die NICHT gleichgesetzt werden dürfen:** die Band-Oszillation (beide Male
gemessen) und der Abbruch *„not responding"* bei gutem Signal. Christophs Beschreibung passt auf das
**zweite**. Ob dieselbe Ursache dahintersteht, ist **offen**.
**Zeitachse:** am **2026-08-17** liefen TP28 (CAPsMAN-Cutover) und TP27 (7.24) gleichzeitig. Vorher
waren ap1/ap2 Standalone **ohne Steering und ohne Neighbor-Group**, und `sec-main` fuhr
`ft=yes ft-over-ds=`**`no`**. Beides änderte sich an diesem Tag. Das Beschwerdefenster deckt sich —
**Korrelation, kein Beweis.**
**Was die MikroTik-Doku dazu hergibt (recherchiert 2026-08-30):**
- `transition-threshold` Default **−80 dBm**, `-time` **10 s**, `-request-period` **30 s**,
`-count` **3**. Unsere Steering-Objekte setzen **nichts** davon. Bei −53…−74 dBm wird das iPhone
also kaum Transition-Kandidat ⇒ **unsolicited BTM scheidet als Treiber weitgehend aus.**
- **WPA3 + 802.11r:** die Doku nennt **nur** „802.11r geht nicht über **OWE**". Eine
WPA3-PSK/FT-Inkompatibilität steht dort **nicht** — sie kursiert in Foren. **Hypothese.**
- **`2g-probe-delay`**: dokumentiertes Band-Steering (2,4-GHz-AP ignoriert die ersten 3
Probe-Requests je Client pro 60 s). Bei uns **nicht gesetzt**.
- Bei **dynamischen** Neighbor-Groups markiert RouterOS 5 GHz als *preferable*. Wir nutzen die
**statische** Gruppe `bf-main` über beide Bänder; ob die Präferenz dort greift, sagt die Doku
nicht ⇒ **offene Frage**.
**✅ Erledigt 2026-08-30: die Beweislage ist jetzt dauerhaft.** `wireless`- und `caps`-Logs gehen in
den Kollektor (`10.40.20.18:514`, dieselbe Senke wie TP5), und `/system logging` steht seit heute
unter der **27. ComplianceRule `system-logging`** — vorher war der Block unter keiner Regel und im
Template gar nicht vorhanden. Vorher lief alles nur in einen 75-Zeilen-Ringpuffer.
**🔴 NACHTRAG 2026-08-31 — die Daten sind da, und sie kippen die Reihenfolge.** Nach einer Nacht
Logging: **559 `wireless`-Zeilen, 439 davon (79 %) vom iPhone.** Entscheidend ist die Trennschärfe —
`reauthenticating` tritt **93 von 93 Mal bei genau einem Client** auf (dem iPhone) und **93 von 93
Mal auf genau einer SSID** (`dirtyvader-main`). Was `main` von den anderen fünf unterscheidet, ist
**ein einziges Attribut: `ft=yes ft-over-ds=yes`**. Dieselben APs, dieselben Radios, derselbe
Zeitraum — die SSIDs mit `ft=no` zeigen **null** Ereignisse. Das ist eine saubere Kontrollgruppe;
die WPA3+802.11r-Hypothese ist damit **durch eigene Messung gestützt**, nicht mehr Forenwissen.
Verlauf am 31.08.: 07:25 (10×) → 07:34 (21×), eskalierend, teils mehrere pro Sekunde, endend mit
der Flucht auf einen **25 dB schlechteren** AP (`-89 dBm`).
⇒ **`ft-over-ds=no` ist jetzt Hebel 1**, nicht `2g-probe-delay`: Letzteres adressiert
Band-Steering, hier liegt ein **Authentifizierungs**problem vor. Details und Messrezept im
[Handoff vom 2026-08-31](handoff-20260831-wlan-fund-e.md).
**🚫 Kein Funk-Parameter wird geändert, solange keine Daten da sind.** Vier Hebel, **einzeln** und je
als Transaktion (`gc-transaktion.py`). **Reihenfolge korrigiert am 2026-08-31** durch den Befund oben —
die alte Liste führte `2g-probe-delay` an, das aber **Band-Steering** adressiert, während hier ein
**Authentifizierungs**problem gemessen ist:
| # | Hebel | Begründung der Reihenfolge | Risiko | Stand |
|---|---|---|---|---|
| 1 | **`ft-over-ds=no`** auf `sec-main` | nimmt die Änderung vom 08-17 zurück; FT bleibt über `ft=yes` (over-the-air) erhalten. Kleinster Eingriff, der die verdächtige Achse trifft | gering | ❌ **widerlegt** — lief 6,4 h allein → 124× (≈19/h) + 2 Ausfälle, **zurückgebaut 2026-09-01** |
| 2 | `ft=no` auf `sec-main` | falls (1) nicht reicht: FT komplett aus. Kostet Roaming-*Geschwindigkeit*, nicht Roaming | mittel | ✅ **gezogen 2026-09-04** (User-Entscheid, Dauerzustand — Template + bf-golden-config + Nautobot-Sync) |
| 3 | `2g-probe-delay=yes` (2,4-GHz-Configs) | MikroTiks *dafür gebautes* Werkzeug; verzögert nur Probe-Antworten und **kann keinen Client stranden lassen**. Adressiert die **Band-Oszillation** — ein anderes Symptom | sehr gering | ⏹️ entfällt (Fund E geschlossen) |
| 4 | `signal-range`-Boden per `access-list` (Fund E im Original) | **kann Clients aussperren** — genau das war 08-22 das Gegenargument; nur mit gemessenem Wert | hoch | ⏹️ entfällt (Fund E geschlossen) |
> ✅ **Ergebnis 2026-09-02 (Abschlussmessung, [Handoff](handoff-20260902-tp25-fund-e-messung.md)):** Der Sturm ist mit
> `ft-over-ds=yes` **und** 7.24.1 verschwunden — 09-02 nur **4 reauth in 8,6 h** (alle iPhone `F2:62`, isolierte
> Einzel, 0 Ausfälle), Verlauf **218 → 40 → 4**. Vorbedingung geprüft (Core-Uptime 1d18h, `caps joined=0`).
> ⇒ **7.24.1 war die Ursache/der Fix, nicht `ft-over-ds`.** Hebel 1 widerlegt+zurückgebaut, Hebel 2–4 entfallen,
> **kein Geräte-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). Der folgende Detailblock zu
> Hebel 1 ist historisch.
#### 🔬 Hebel 1 — gezogen am 2026-08-31 08:09, Messung läuft bis 2026-09-01
**Was steht:** `sec-main` am Core auf **`ft=yes ft-over-ds=no`**. Abnahme **am Manager** über alle
Master-VAPs — `cap-wifi3/4/5/6` (= ap2 + ap3) melden `ft=true ft-over-ds=false`; `cap-wifi1/2` (= ap1)
sind leer, weil ap1 funkstill ist. Danach `remote-cap provision [find]`; alle drei CAPs kamen mit
`state=Ok` zurück, alle VAPs `running=yes` **am CAP gelesen**, und das iPhone hing 71 s später wieder
auf `dirtyvader-main` mit **`auth-type=ft-wpa3-psk`** und **frischer DHCP-Lease** `10.40.10.196` —
also die vorgeschriebene Abnahme (Lease, nicht Registration-Table). 30 Clients insgesamt zurück.
**Governance:** Transaktion `TP25-fund-e-ft-over-ds` (Scope `wifi-security`, Ablauf 2026-09-03),
Geräte-Backup `pre-tp25-funde-ftoverds.backup` auf dem Core, Snapshot
[`docs/transaktionen/TP25-fund-e-ft-over-ds.snapshot.rsc`](transaktionen/TP25-fund-e-ft-over-ds.snapshot.rsc).
Compliance danach **134/135**, und der Diff ist **exakt** `ft-over-ds=yes → no` auf `sec-main` —
kein Kollateral-Drift durch die Re-Provisionierung. Das GC-Template wird **beim Commit** nachgezogen,
nicht vorher: sonst wäre der laufende Versuch unsichtbar und `status` könnte ihn nicht mehr von einem
Versehen unterscheiden.
🔴 **Die `cap-wifiN`-Karte hat sich durch die Re-Provisionierung VERSCHOBEN** — wer die Logs vom
31.08. auswertet, muss den Schnitt kennen, sonst ordnet er Ereignisse dem falschen AP zu:
| | vor 08:09 (Log 07:xx) | ab 08:09 |
|---|---|---|
| ap1 (funkstill) | — | `cap-wifi1` / `cap-wifi2` |
| ap2 | `cap-wifi1` / `cap-wifi2` | `cap-wifi3` / `cap-wifi5` |
| ap3 | `cap-wifi5` / `cap-wifi6` | `cap-wifi4` / `cap-wifi6` |
⚠️ **Korrektur am Befund: `main` unterscheidet sich in ZWEI Attributen, nicht einem.** Neben
`ft=yes ft-over-ds=yes` fährt `sec-main` als einzige SSID **`authentication-types=wpa3-psk` allein**;
die fünf anderen sind `wpa2-psk,wpa3-psk`. Die Kontrollgruppe trennt also FT **und** WPA3-only nicht
voneinander. Das ändert nichts an der Hebelreihenfolge (die Foren-Hypothese nennt genau die
Kombination WPA3+FT), aber die Formulierung „genau ein Attribut" ist zu stark — **wenn Hebel 1 und 2
beide nichts bewegen, ist `wpa2-psk,wpa3-psk` auf `sec-main` der nächste Kandidat, nicht `signal-range`.**
**Messung (Erfolgskriterium): `reauthenticating` für `F2:62:4C:E1:AF:09` geht auf ~0, ohne dass die
anderen SSIDs neue Ereignisse zeigen. Die Zahl MUSS sich bewegen.**
```bash
ssh root@10.40.20.100 "pct exec 400 -- sh -c 'cat /opt/tp5-syslog/log/10.40.20.1/2026-0*-*.log |
grep reauthenticating | grep -oE \"([0-9A-F]{2}:){5}[0-9A-F]{2}\" | sort | uniq -c'"
```
Baseline **93** (alle iPhone, alle `main`); davon **79 am 31.08., alle in Stunde 07**, der letzte um
**07:34:43** — also **vor** dem Eingriff. Zwischen 08:00 und 08:15 kein einziges. Das ist noch **kein
Ergebnis**, nur der saubere Schnitt.
*(Der Defekt war nie gegenstandslos, nur unbelegt: Fund B/C wurden am 08-20 mit ap1 auf `ether15`
gemessen — also in genau der Stellung, in der ap1 heute wieder hängt. Er ist platzierungsunabhängig.)*
**✅ Erledigte Funde — Ergebnis und Merkregel (Volltext im Archiv):**
| Fund | Ergebnis | Merkregel, die bleibt |
|---|---|---|
| **A** 2,4 GHz fuhr 40 MHz statt der profilierten 20 | ✅ 08-17: Inline-`channel.*` auf `wifi1/wifi2` geleert → `2412`/`2462` @ 20 MHz | **Inline-Overrides gewinnen gegen das verlinkte Profil** — die SoT war die ganze Zeit richtig, das Gerät hat sie nicht befolgt |
| **B** ap2 sendete 5 GHz mit **9 statt 18 dBm** | ✅ 08-17: `5220/ax/Ce/I`, **18 dBm** | Der Fix ist gemessen, **der Mechanismus nicht** — beide ersten Erklärungen (ETSI-Band C, „80 MHz kostet generell") sind widerlegt. Resthypothese + diskriminierender Test (`ap2 auf 5180/Ceee`) stehen im Archiv und gehören **vor** einen Hardwarekauf |
| **C** 80 MHz kostete Randabdeckung | ✅ 08-17: `ch-5g` auf `20/40mhz`; ap1 `5180/Ce`, ap2 `5220/Ce`, überlappungsfrei | Unter ETSI gibt es im Indoor-Band **nur einen** 80-MHz-Block — zwei APs können dort nicht beide 80 MHz fahren. Genau deshalb war ap2 je auf 5745 gelandet |
| **D** `w2-iot` trug einen Channel-Override auf einem **Slave**-VAP | ✅ 08-17: Overrides auf beiden APs entfernt, `w2-iot` seither `BR` | `band` ist eine Eigenschaft des **Radios**, nicht eines Slave-VAPs. War der Mechanismus hinter „alle SSIDs da, `-iot` beharrlich nicht" |
| **Drift-Lücke** — genau diese Overrides lagen außerhalb jeder ComplianceRule | ✅ 08-18 mit dem TP28-GC-Umbau: `wifi-*`-Features hängen am Core, drei neue Rules; ein CAP hat keine Inline-Overrides mehr | **„Compliant" heißt nur: die abgedeckten Sektionen sind sauber.** Fund A und D blieben seit 08-08 unentdeckt, während die Flotte durchgehend compliant meldete |
🔴 **Deploy-Gotchas aus derselben Arbeit (kosten sonst zwei Deploys, Volltext im Archiv):**
`/interface/wifi/set <if> !channel` **löscht die Inline-Sub-Properties NICHT** — es wirft nicht, es tut
nichts; richtig ist einzeln `!channel.width !channel.band !channel.skip-dfs-channels`. Und bei
Kanalarbeit **Breite VOR Frequenz** ändern, sonst erzeugt man auf dem Weg eine Co-Channel-Kollision.
⚠️ `steering=`/Channel-Änderungen **re-applien alle VAPs und werfen jeden Client kurz raus** — nicht
während eines Calls deployen, `/system backup` je AP vorher.
🔬 **Messwerkzeug — vier Regeln, jede aus einem Fehlbefund geboren:**
- 🔴 **Für einen Kanal-/Umfeldscan NICHT `list_wifi_interfaces` benutzen** (Fund 2026-08-24): das Tool
gibt `security.passphrase` **aller** Profile im Klartext aus — ein Read für die Kanalbreite hat so
fünf WLAN-Passphrasen in ein Transkript geschrieben, den IoT-PSK inklusive. Für Kanäle reicht
`/interface wifi channel print detail`; für die `cap-wifiN`-Zuordnung `… print where master=yes`
mit Feldauswahl. **Der Scan selbst (`/interface wifi scan`) ist unbedenklich.**
- 🔴 **Die `cap-wifiN`-Nummern sind NICHT stabil.** Der Manager vergibt sie in der Reihenfolge, in der
sich die CAPs anmelden — nach dem ap1-Rückbau sind `cap-wifi1/2` = **ap2** und `cap-wifi3/4` = **ap1**,
umgekehrt zur früher fest verdrahteten Tabelle. ⇒ **Die Zuordnung gehört zum Messlauf**, nicht zur
Auswertungszeit: [`tp25-walk-messung.sh`](tp25-walk-messung.sh) friert sie als `ap-map.txt` ein, die
Auswertung lässt die Namen sonst roh — **unbeschriftet ist ehrlicher als falsch beschriftet.**
**Läufe vor dem 2026-08-22 sind nicht rekonstruierbar**; wer ihre AP-Namen zitiert, prüft sie an der
Physik (der AP im Raum liefert dort das stärkste Signal).
- 🔴 **Ein vom Pin abgewiesener Verbindungsversuch ist in Ping-Daten nicht von einem Funkloch zu
unterscheiden** — und der Manager protokolliert ihn auf `info`-Ebene **gar nicht**. Android quittiert
die Abweisung mit „Verbindung fehlgeschlagen" und **wählt das Netz ab**; danach misst der Rest des
Rundgangs Stille. Die Klick-Konsole schlägt deshalb Alarm, sobald ein **gepinnter** Client nirgends
hängt (`sta_iface=absent` in `sampler-<name>.csv`) — denn „der Gegen-AP trägt hier nicht" **ist das
Messergebnis**.
- 📐 **Eine Runde, N Messpunkte** (seit 2026-09-04): die frei roamenden Clients zeigen, was ein normaler
Client wählt — interessant sind die **gepinnten**. Jeder Client wird **band-genau** auf einen AP
gezwungen ([`tp25-pin-multi.sh`](tp25-pin-multi.sh), `TP25_PINSPEC="name|mac|ap|band;…"`), direkt aus
der Klick-Konsole. Gute Referenz + schlechte Sonde = die Abdeckung steht, die Rückfallebene nicht.
Memory `ref_gegen_ap_messung`. *(Die alte 2-Client-`access-list`-Automatik über `tp25-pin-s22.sh` ist
mit dem Multi-Pin-Umbau entfallen; `tp25-pin-s22.sh` liefert nur noch die `map`.)*
- ▶️ **Start ohne Werkzeugkette: `bash docs/tp25-runde.sh [name]`** — legt den Lauf unter
`docs/messdaten/tp25-runde-<name>/` an, friert die AP-Karte ein, startet Konsole (Port 3031) und
Messung, sagt die **vom Interface gelesene** Adresse fürs Telefon an. In der Konsole: **pinnen**
(N Clients je AP/Band) → **gehen** (🚶/📍) → **Räume antippen** → **stoppen**; beim Beenden löst der
Wrapper den Multi-Pin. Auswertung: `tp25-gehweg-auswertung.py --dir …`.
- ℹ️ **Und das alles passiert TROTZ aktivem 802.11k/v und FT** (`st-main` mit `rrm=yes`/`wnm=yes`,
`ft-over-ds=yes`, live geprüft). Die Merkregel `ref_ft_is_not_roaming` („11k/v fehlen") ist
**historisch**. Bei −48…−55 dBm kostet eine Übergabe 0–2,6 s, meist unter einer halben Sekunde —
**nicht die Übergabe ist teuer, sondern die schwache Zone.**
**User-Randbedingung (2026-08-17, wörtlich): „ich weigere mich einen anderen access point zu kaufen
solange ich nicht das gefühl habe, wir haben alles aus dem ap rausgeholt."** Das ist die
**Abnahmebedingung dieses TPs** — keine Hardware-Empfehlung, bevor die Parameter stimmen. Der Boden ist
gelegt (beide APs 18 dBm, überlappungsfrei, 20 MHz auf 2,4 GHz, ap2 auf 2,5 m); was übrig bleibt, ist
die Redundanzfrage oben.
- **Warum eigener TP:** Christoph hat am 2026-08-16/17 sechs konkrete Beobachtungen gemeldet und
ausdrücklich **einen** TP dafür verlangt; sie hängen alle an derselben Ursache-Klasse (Funkparameter,
die nicht nach Manual gesetzt sind). **TP25 übernimmt den offenen Rest von TP3.3** (Re-Walk,
Dead-Spot-Kartierung — mit der Raumkarte vom 08-22 faktisch geleistet). TP3.2/TP3.3 stehen als Historie.
- **Stand bei Sitzungsende 2026-08-22:** Pin gelöst, Automatik aus, `crs326-core` unverändert und
**transaktionsfrei** — die `access-list` steht unter keiner ComplianceRule, es gab nichts zu committen.
*(Seit 2026-08-23 gibt es die Rule `wifi-access-list`, s. TP22-Nachtrag — Fund E wäre ab jetzt
transaktionsfähig.)*
- 📋 **Übergabe:** [`handoff-tp25-session-20260821.md`](handoff-tp25-session-20260821.md) ·
Messungen: [`tp25-wlan-roaming-messung-20260820.md`](tp25-wlan-roaming-messung-20260820.md),
[`tp25-runde-20260822-2330-auswertung.md`](tp25-runde-20260822-2330-auswertung.md)
- **Abh.:** TP22 ✅ (GC-Deploy-Pipeline). Verwandt **TP3.2** (`docs/wifi-p750w-coverage-and-map.md`),
**TP29** (nutzt dieselbe Datenquelle als Sensor). **Kein Blocker für irgendetwas.**
#### 🔬 Fund E — 2026-09-01: der `ap2`-Teilbefund (gültig) und eine widerlegte Heilungs-Wertung
**Anlass:** Christoph meldete **2026-08-31 23:02** eine Störung am Schreibtisch (5G, iPhone, kein
Telefonat). Analyse (alles read-only am Manager `crs326-core`):
- **AP-Karte** (stabil seit letztem CAP-Rejoin 15:57, kein Rejoin danach — Attribution belastbar):
`cap-wifi5/6 = ap2`, `cap-wifi3/4 = ap3`, `cap-wifi1/2 = ap1` (funkstill, Radios da, 0 Clients).
- **23:02-Ereignis:** iPhone `F2:62:4C:E1:AF:09` ping-pongt im Sekundentakt `cap-wifi5 ↔ cap-wifi6`
= **ap2s 5G (5220) ↔ ap2s 2,4G (ch11/2462)** — Band-Steering-Thrash auf **demselben** AP, plus
`disconnected, not responding` um 22:37:40 (−76) und **23:01:00** (−82). Feldstärke der Roams
**−75…−88 dBm**.
- 🔴 **Christophs Fakt kippt die Coverage-Deutung:** iPhone und **ap2 nur 2 m** entfernt, gleicher
Raum, ap2 auf 2,3 m. Bei 2 m Sicht wären −35…−45 dBm normal. −80 auf 2 m ist **kein** Coverage-
und **kein** FT-Problem. Dazu: **aktuell 0 Clients auf ap2**, alle 29 auf ap3 — die Clients *meiden*
ap2. ⇒ **ap2s Funk selbst unterperformt** (RF/Antenne/HW) und/oder die **DTU-Störung auf ap2/ch11**
(Transaktion `TP17.1-dtu-pin-ap2`, Wechselrichter `54:F2:9F:12:51:F0` mit −59 dBm auf ap2s 2,4-Radio).
- **In-situ zu messen** (nachts nicht möglich — Phone idle auf ap3): Registration-Signal live prüfen,
während ein Client auf ap2 hängt; Kandidaten-Eingriffe — DTU-Pin lösen/verlegen, `dirtyvader-main`
auf ap2 temporär deaktivieren (ap3 übernimmt den Schreibtisch), ap2-Antenne/HW prüfen. **Keine
Änderung ohne diese Messung.**
🔴 **WIDERRUFEN am 2026-09-01 vormittags — der zweite Teil dieses Abschnitts war ein Messfehler.**
Ursprünglich stand hier: „Fund E ist im sauberen Fenster geheilt — 0× `reauthenticating` seit dem
14:36-Reboot (`sinceMinutes=1440`, total 0), also hat Hebel 1 sein Ziel erreicht."
**Warum das nicht trägt:** `get_log` liest den **flüchtigen Ringpuffer des Routers**, nicht den
Kollektor. Live nachgemessen fasst er **1000 Zeilen und reicht bis 01.09. 00:38:58 zurück** — keine
9 Stunden. Der Core-Reboot um 14:36 hatte ihn geleert; der 31.08. Vormittag war darin gar nicht
enthalten. `total 0` hieß **„im Puffer nichts", nicht „in 24 h nichts"**. Das ist exakt die Falle,
wegen der am 30.08. das persistente Logging gebaut wurde.
**Was der Kollektor zeigt:** im upgrade-freien Fenster 08:09–14:36 (Hebel 1 **allein**) **124×
`reauthenticating`** — 09h=1 · 11h=2 · 12h=20 · 13h=31 · 14h=70, **alle vom iPhone allein**, dazu
Christophs zwei erlebte Ausfälle 13:16 und 14:23. Zwischen 09:00 und 14:36 lag **kein Reboot**.
⇒ Hebel 1 wurde am 2026-09-01 **zurückgebaut**; der Rückbau ist das entscheidende Experiment
(s. Ist-Stand-Block oben).
✅ **Der `ap2`-Teilbefund oben bleibt davon unberührt und gültig** — er ist unabhängig gemessen und
erklärt die 23:02-Störung, die weder FT noch Coverage erklärt.
**Commit-Weg (wartet auf Christophs Go — git-push-Kette):** Template `nautobot/mikrotik_routeros.j2`
**Z. 220** `sec-main` von `ft-over-ds=yes` → `ft-over-ds=no`; dann Template-Repo nach Nautobot syncen,
`gc-configplan.py --refresh-compliance` (Backup→Intended→Compliance), dann
`gc-transaktion.py commit --change-id TP25-fund-e-ft-over-ds`. Danach `wifi-security` wieder driftfrei.
🔴 Kreuzt git-push (Backup-Guard + Commit-Hinweis-Regel) und Live-Nautobot-Jobs — **nicht autonom.**
Handoff: [`handoff-20260901-tp25-ap2-verdacht.md`](handoff-20260901-tp25-ap2-verdacht.md).
---
<a id="tp25-lang"></a>
## TP25 — Langfassung (Messchronik + Funde im Volltext, Stand 2026-08-22)
> ⚠️ **TP25 ist NICHT abgeschlossen.** Der geltende Stand, das Offene (Fund F, die
> 20-MHz-Entscheidung) und die Merkregeln stehen in [`teilprojekte.md` → TP25](teilprojekte.md).
> Hier liegt **wortgleich** die Fassung, die bis zum 2026-08-23 dort stand — 394 Zeilen Herleitung:
> ✅ **TP25 ist ABGESCHLOSSEN (2026-09-15).** Der aktive Stand (Kondensat) steht oben unter
> [TP25](#tp25). Hier liegt **wortgleich** die Fassung, die bis zum 2026-08-23 dort stand — 394 Zeilen Herleitung:
> die Messchronik (A)–(G) mit allen zwischendurch korrigierten Irrtümern, die Funde A–F im Volltext,
> die beantworteten Fragen (Beobachtung 5 „CAPsMAN oder Nautobot GC?", Herkunft der ch149-Notiz,
> `2ghz-g` für IoT, Deckenmontage-APs) und die Umsetzungsreihenfolge mit den Deploy-Gotchas.
@@ -1310,7 +1827,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
> Erklärungen — zweimal stand hier eine plausible Ursache, die die nächste Messung gekippt hat
> (ETSI-Band C bei Fund B; „ap1 ans Sideboard" bei (F)/(G)). Wer TP25 fortsetzt, muss sehen können,
> welche Wege schon abgelaufen sind.
### TP25 — WLAN-Stabilisierung ▸ 🟢 **KERN ERLEDIGT (2026-08-22): Dead Spot Terrasse repariert** (43 % → 9 % Verlust, 20,4 s → 1,0 s). Offen nur noch **Fund F** (5:05-Takt IoT) und die Entscheidung **5 GHz auf 20 MHz**. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6
### TP25 · Gitea #17 — WLAN-Stabilisierung — 43 % → 9 % Verlust, 20,4 s → 1,0 s). Offen nur noch **Fund F** (5:05-Takt IoT) und die Entscheidung **5 GHz auf 20 MHz**. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6
> 🆕 **Messung 2026-08-20/21: [`tp25-wlan-roaming-messung-20260820.md`](tp25-wlan-roaming-messung-20260820.md)**
> (Nebenprodukt der TP6-Messreihen, Core-Sampler im 2-s-Takt über 2,5 h, teils mit laufendem Videocall).
> Drei Befunde:
@@ -1706,7 +2223,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp39"></a>
### TP39 — LXC-Hygiene auf pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup ▸ ✅ **ABGESCHLOSSEN (2026-09-01)**
### TP39 · Gitea #68 — LXC-Hygiene auf pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup
**Auslöser:** ein 10-Sekunden-Hänger auf `gitea`, gefunden am Rand des TP16.3-§5-Flips. Er hatte mit
dem Flip nichts zu tun, war aber nur deshalb sichtbar, weil zum ersten Mal jemand systematisch
@@ -1785,7 +2302,10 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
---
### TP34 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) ▸ ❌ **VERWORFEN (2026-09-08)**
### TP34 · Gitea #24 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle)
> ➡️ **Fortsetzung 2026-09-22: `TP34.2 — Interne DNS-Namen als Daten`** (Gitea #73, Prosa in
> `docs/teilprojekte.md` → `### TP34.2`). Dort lebt der *Daten → gerenderte Config*-Gedanke weiter;
> **verworfen bleibt nur das Caddyfile-Rendering** (dieser Abschnitt hier).
> 🔴 **Nachtrag 2026-09-08 (verworfen):** Der Ansatz „Caddyfile via Golden Config rendern" ist
> zurückgezogen (Commits `d63dc7b`, `bca8f6e`; Gitea-Issue #24 → `status:dropped`, closed). Der
> `420.teilzeitladen.xyz`-Block wurde wieder aus dem GC-gerenderten Caddyfile entfernt; **der
@@ -1804,7 +2324,7 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
---
<a id="tp44"></a>
### TP44 — Cloudflare-Estate als Dokumentations-SSoT in Nautobot ▸ ✅ **DURCH (2026-09-10): Etappe 1 + Etappe 2 (Teil A IPAM-Prefixe, Teil B Live-Sync als täglicher Nautobot-Job)**
### TP44 · Gitea #33 — Cloudflare-Estate als Dokumentations-SSoT in Nautobot
> **Entscheid (User, 2026-09-02):** die volle CF-Estate (Tunnel + WARP-Connector + VPC-Service +
> Hyperdrive + Workers + alle Access-Apps) als strukturierte, abfragbare SSoT in Nautobot abbilden —
> **etappenweise: manuell jetzt, Sync später.** Nautobot kann CF nicht steuern → „SSoT" heißt
@@ -1848,7 +2368,7 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
- **Beziehung zu TP37:** TP44 hat **TP37 Punkt 3** (CF-Connectoren) vorweggenommen — dieselbe native-Modell-
Linie, die TP37 P1+2 (Substrat/Dienste) fortsetzt.
### TP41 — CF-Access-Reparatur + Zero-Trust-Konsolidierung ▸ ✅ **ABGESCHLOSSEN (2026-09-10)** — Auth-Kern seit 2026-09-01 komplett; **Gelis WARP-Client am 2026-09-10 via Android erfolgreich eingerichtet** (letzter Blocker weg, User-Bestätigung). Optionale Restpunkte (T6 Rate-Limit, geli-CGNAT-Policy-Cleanup) bewusst nicht gezogen; Keystore-Rotation läuft unter TP30 #7.
### TP41 · Gitea #30 — CF-Access-Reparatur + Zero-Trust-Konsolidierung — Auth-Kern seit 2026-09-01 komplett; **Gelis WARP-Client am 2026-09-10 via Android erfolgreich eingerichtet** (letzter Blocker weg, User-Bestätigung). Optionale Restpunkte (T6 Rate-Limit, geli-CGNAT-Policy-Cleanup) bewusst nicht gezogen; Keystore-Rotation läuft unter TP30 #7.
**Auslöser:** CF-Access zwang Christoph **und Geli** täglich zum OTP-Login (soll: 30 Tage); GitHub/Google
„funktionieren nicht". Root-Causes: Team-Domain-Rename `bf4`→`emsenhuber` (Provider-Callback veraltet),
@@ -1906,14 +2426,14 @@ der Egress ohnehin Cloudflare (nicht diese IP) → der IP-Include wäre dann üb
**Gitea-Least-Privilege** = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph).
- Bezug **TP11** (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt.
### TP43 — dw-Standort-DNS-Uplink (site-spezifische Overrides) ▸ ♻️ **REOPENED 2026-09-11 → Volltext zurück in [`teilprojekte.md`](teilprojekte.md) (Abschnitt TP43)**
### TP43 · Gitea #32 — dw-Standort-DNS-Uplink (site-spezifische Overrides)
Kurz zwischen 2026-09-11 verworfen und am selben Tag reopened: die 420-Prämisse war gegenstandslos
(public CF-Worker, via WARP/TP41 gelöst), aber die **interne** BF-Namensauflösung über WG am dw blieb
dabei offen (`portainer.bf.emsenhuber.net` am dw nicht erreichbar). Korrigierter Scope + verworfene
420-Historie stehen jetzt aktiv in `teilprojekte.md`. Gitea-Issue #32 = `status:todo` · `thema:firewall-dns`.
### TP42 — Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net ▸ ✅ **DURCH (2026-09-02)** — ein interner Suffix `bf.emsenhuber.net`, `.bf.lan` raus, `.16` (adguard_bf1) abgebaut, Compliance 140/140. Offen nur der an **TP37** gekoppelte Nautobot-Doku-Teil.
### TP42 · Gitea #31 — Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net — ein interner Suffix `bf.emsenhuber.net`, `.bf.lan` raus, `.16` (adguard_bf1) abgebaut, Compliance 140/140. Offen nur der an **TP37** gekoppelte Nautobot-Doku-Teil.
**Auslöser (2026-09-01, TP41-Randbefund):** live gemessen liefern die realen Client-Resolver
`10.40.20.36`/`.37` `*.emsenhuber.net → 10.40.20.40` (Caddy) — korrekt. Daneben existiert aber ein

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,12 @@ lokal" — das ist nach dieser Rotation nicht mehr wahr.
## 4. Fußnote mit Verfallsdatum
⏰ **Der `CF_API_TOKEN` selbst läuft am 2027-01-09 ab** (Memory `ref_cf_mcp_readonly_token_writes`).
⏰ ~~Der `CF_API_TOKEN` selbst läuft am 2027-01-09 ab~~ — **korrigiert 2026-09-21:** das Datum
gehörte zum längst **revokten** Alt-Token `9984078812bc…`; das **live** `CF_API_TOKEN`
(id `493ed8bd…`) hat **kein** Ablaufdatum (verifiziert via `/accounts/{id}/tokens/verify`,
`expires_on: null`). Die Rotation ist damit nicht mehr termin-, sondern **anlassgetrieben**:
**Anlass** ist der **Leak vom 2026-09-21**. Sie bleibt **gebündelt** in TP30 (Posten #4) und wird
nicht vorgezogen. Memory `ref_cf_mcp_readonly_token_writes` ist nachgezogen.
Er ist **account-owned**, verifiziert also über einen anderen Endpoint als ein User-Token — wer das
verwechselt, hält ein funktionierendes Token für kaputt. Ohne ihn ist auch diese Rotation nicht mehr
möglich, ebensowenig der TP16.3-Schnitt.

View File

@@ -84,7 +84,7 @@ dns:
ipset_file: ""
bootstrap_prefer_ipv6: false
upstream_timeout: 10s
private_<redacted>: []
private_networks: []
use_private_ptr_resolvers: false
local_ptr_upstreams: []
use_dns64: false

View File

@@ -0,0 +1,123 @@
# WARP-Egress — nativer Consumer-WARP (WireGuard) auf dem crs326-core
> **Status:** ❌ **VERWORFEN 2026-09-26** — Cloudflare blockt inoffizielle Consumer-WARP-WireGuard-Clients.
> Live gebaut, verifiziert, dann vollständig zurückgebaut. Key + Backup bleiben zur Nachvollziehbarkeit liegen.
>
> ➡️ **Nachtrag §6 (selber Tag, zweiter Durchgang):** Der Mesh-Tunnel `cf-mesh` **trägt** als
> ziel-basierter Ausgang **und erreicht den Origin mit `200`** (Core kommt als `104.28.207.39`, `warp=on`,
> `colo=VIE` heraus — dieselbe IP wie der WARP-Client). **Lösung ist dauerhaft** über Nautobot/Golden Config
> aktiv (Template `nautobot/mikrotik_routeros.j2`, Abschnitt `[10b]`). Der frühere „Origin blockt CF-IPs"-
> Befund war ein Messfehler — korrigiert in §6.3.
## 1. Kontext / Warum
- Cannaleo-Origin `164.90.191.7` (blaueblueten.de, DigitalOcean multi-tenant, ~400 Vhosts) blockt die Heim-IP
`90.146.33.177` hart (leerer `403`, live 2026-09-25).
- **Scope (User-Vorgabe 2026-09-25):** nicht der ganze Outbound, sondern **genau eine Ziel-IP**
(`164.90.191.7/32` — blaueblueten.de apex + www, beide identisch). Nur **Privatbestellungen**, Traffic
überschaubar gering.
- **Terpenfinder wird NICHT über diesen Weg** in Betrieb genommen (eigene, saubere Lösung — siehe §5).
## 2. Was gebaut wurde (2026-09-26, live)
1. **Consumer-WARP-Key** per `wgcf` registriert (ViRb3 v2.3.0, Wegwerf-Konto via `api.cloudflareclient.com`).
→ `mcp/cloudflare/.env` (`CF_WARP_CLIENT_PRIVKEY`, gitignored) + `~/.secrets/wgcf-client/` (account + profil).
2. Auf `crs326-core` (WG-Zeilen via `sshpass -e`, Rest per MikroMCP/ssh):
- `/interface wireguard add name=warpout mtu=1280 private-key=… comment="WARP Egress (privat)"`
- Peer `warp-cf-pop`: public-key `bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=`,
endpoint `engage.cloudflareclient.com:2408`, `allowed-address=0.0.0.0/0`, `persistent-keepalive=25s`
- `/ip address add 172.16.0.2/32 interface=warpout`
- `/routing table add name=warpout fib` + `/ip route add 0.0.0.0/0 gateway=warpout routing-table=warpout`
- `/routing rule add dst-address=164.90.191.7/32 action=lookup-only-in-table table=warpout`
- NAT `masquerade out-interface=warpout` + forward `accept out-interface=warpout`
3. **Verifiziert:** Key korrekt (abgeleiteter Public-Key == Router-Public-Key
`HTFM3gj3FyT0EjWkG+2LLmvtxaFz1kH/P1iDwM0HswI=`), Konto „Active" (`wgcf status`).
## 3. Befund: Handshake schlägt fehl
- Peer `warp-cf-pop`: `rx=0`, kein `last-handshake` — Cloudflare antwortet **nie** auf den WG-Handshake,
egal welcher Endpoint (`162.159.192.1` / `.193.2` / `.193.5`).
- **Ursache (recherchiert):** Cloudflare blockt **inoffizielle WireGuard-Clients** für Consumer-WARP. Der
offizielle Client setzt ein spezielles Handshake-Feld (`reserved`/Client-ID, z. B. `b91981`), das RouterOS
(und jedes plain-wgcf-Profil) nicht setzt; dazu Abuse-Prevention. Der Mesh-Connector `cf-mesh` läuft nur,
weil das ein **anderes Produkt** ist (Zero Trust, nicht Consumer).
- Quellen: [ViRb3/wgcf Issue #542](https://github.com/ViRb3/wgcf/issues/542) ·
[#515](https://github.com/ViRb3/wgcf/issues/515) · [#158](https://github.com/ViRb3/wgcf/issues/158).
## 4. Abbau (2026-09-26)
- Alle 8 Objekte entfernt (Routing-Regel, Route, Tabelle, NAT, Firewall, Adresse, Peer, Interface). Core
wieder im Ursprungszustand — **keine Drift**, Golden-Config-Compliance unverändert.
- Backup `pre-warp-egress-20260925.backup` liegt auf dem Core (vor dem Versuch, TP22-Regel).
- `CF_WARP_CLIENT_PRIVKEY` bleibt in `mcp/cloudflare/.env` (nutzlos, da Client geblockt; kann bei Bedarf entfernt werden).
## 5. Weg vorwärts
> ⚠️ **Durch §6 überholt (2026-09-26).** Die Grundannahme hier — „geteilte CF-IPs werden geblockt, WARP
> liefert 403" — war **falsch** und ist in §6.3 korrigiert. WARP-Client und `cf-mesh`-Ausgang erreichen den
> Origin beide mit `200`. Eine Dedicated Egress IP ist damit nicht nötig.
- **Bleibt gültig:** Consumer-WARP über natives RouterOS-WG ist auf absehbare Zeit blockiert (separates
Produkt, betrifft nicht `cf-mesh`).
## 6. Nachtrag 2026-09-26: Mesh-Exit trägt und erreicht den Origin (200) — Lösung dauerhaft
**Frage:** Kann der bereits laufende `cf-mesh`-Tunnel (TP16.1) als *Ziel-basierter Ausgang* dienen, statt eines
separaten Consumer-WARP? **Antwort: ja — und er erreicht den Origin mit `200`.** Die Lösung ist dauerhaft über
Nautobot/Golden Config aktiv (Template-Abschnitt `[10b]`).
### 6.1 Der Origin-Weg ist jetzt vermessen (vorher nur angenommen)
`blaueblueten.de` geht **direkt** zum DO-Droplet, **kein Cloudflare im Pfad** (Zertifikat = Let's Encrypt
`CN=blaueblueten.de` vom 11.08., **kein `cf-ray`**, kein `server`-Header, kein `/cdn-cgi/trace` — CF setzt
`cf-ray` immer). Der 403 kommt also **vom Origin**, nicht vom Edge.
**Der Block ist rein quell-IP-basiert:** voller Safari-/Chrome-User-Agent ändert nichts, HTTP→HTTPS-Redirect
(`302`) funktioniert, erst der HTTPS-Request antwortet `403` mit `content-length: 0`.
### 6.2 Mesh als Exit — funktioniert
Temporär gebaut (Tabelle `meshtest` + `0.0.0.0/0` via `cf-mesh` + Regel nur auf `1.1.1.1/32`), dann gemessen:
```
/tool fetch url="http://1.1.1.1/cdn-cgi/trace" mode=http
→ ip=104.28.207.39 warp=on gateway=on colo=VIE
```
Der Core kommt also über den Mesh-Tunnel als **Cloudflare-IP** heraus (vorher `90.146.33.177`).
⚠️ **Merkregel: für `cdn-cgi/trace` `mode=http` nehmen, nicht `mode=https`** — über HTTPS liefert RouterOS nur
den 69-Byte-Datei-Header (`# … by RouterOS`, `# software id =`), die Nutzdaten fehlen. Das sieht wie ein
leerer Download aus, ist aber nur der falsche Fetch-Modus.
### 6.3 KORREKTUR: der frühere „403 über Mesh" war ein Messfehler — der Weg trägt
Der erste Test hatte **nur** die Regel `1.1.1.1/32 → meshtest` gesetzt und dann das Ziel gemessen. Für das
Ziel griff die Regel nicht → der Request lief über die **Heim-IP** (`90.146.33.177`) raus. Der gemeldete
`403` war also **403 über die Heim-IP**, nicht über den Mesh-Ausgang.
Beweis (A/B in einem Durchgang, Regel `164.90.191.7/32 → egress-cannaleo`):
```
Regel aktiv → https://blaueblueten.de/ → 200 (168 KiB)
Regel deaktiv → https://blaueblueten.de/ → 403 (fällt auf Heim-IP zurück)
```
Merke: `action=lookup-only-in-table` für Ziel A routet Ziel B **nicht** — nur messen, was tatsächlich
umgeleitet ist. Dieselbe CF-IP (`104.28.207.39`) erreicht den Origin via WARP-Client und via `cf-mesh`.
### 6.4 Rückbau (vollständig)
Alle Objekte entfernt (Regel, Route, Tabelle, Testdateien). `/routing table print` = nur `main`,
`/routing rule print` = leer, Default-Route wieder `90.146.33.1%vlan100`. `cf-mesh` unberührt und gesund
(Handshake 4 s). Keine Drift.
### 6.5 Was daraus folgt
- **Der Core-Teil ist gelöst und wiederverwendbar:** `routing-table` + `0.0.0.0/0`-Route + `routing rule
dst-address=<ziel>/32 action=lookup-only-in-table` + `masquerade out-interface=<wg-x>`. Nur **ein** Ziel
betroffen, Rest unberührt. Genau so gebaut, verifiziert, zurückgebaut.
- **Offen bleibt allein das Transportmittel.** CF scheidet aus (Ziel blockt CF). Der Rest des Vorschlags aus
§5 steht: eigener Ausgang über einen **externen WireGuard-Host**. Ein DO-VPS ist dabei der naheliegendste
Kandidat — der Miner fährt heute schon erfolgreich über eine DO-IP gegen genau diesen Origin.
- 🔴 **Der Core hat heute keinen zweiten Ausgang.** Kein zweites WAN, kein zweites Gateway — jeder künftige
Egress braucht erst ein Interface, das ihn trägt.

View File

@@ -618,7 +618,7 @@ def _tp45_leases_module():
return mod
def _lease_move(mac, soll):
def _lease_move(mac, soll, tag="tp45-v30"):
"""Eine Umzugs-Zeile — EINZEILIG und klammer-vollstaendig (E1015-Lehre, s. _pool_set).
Der Selektor wird GEZAEHLT: ein `find where`, das 0 trifft, sieht wie
@@ -653,8 +653,8 @@ def _lease_move(mac, soll):
arg = f'[find where mac-address={mac}]'
return (f':if ([:len [/ip dhcp-server lease {find}]] = 1) do={{ '
f'/ip dhcp-server lease set {arg} address={soll}; '
f':log info ("tp45-v30: {mac} -> {soll}") }} '
f'else={{ :log info ("tp45-v30: {mac} NICHT EINDEUTIG -- nicht angefasst") }}')
f':log info ("{tag}: {mac} -> {soll}") }} '
f'else={{ :log info ("{tag}: {mac} NICHT EINDEUTIG -- nicht angefasst") }}')
def job_tp45_v30_leases():
@@ -723,6 +723,209 @@ def job_tp45_v30_leases():
return [(["crs326-core"], "\n".join(lines), "TP45-v30-leases")]
def job_tp45_v30_revert():
"""RUECKNAHME 2026-09-21: die 26 Shelly-Reservierungen zurueck auf ihre ALTEN Adressen.
Christoph-Direktive, sofort: *„mach die shellys sofort rueckgaengig! das gibt
sonst ein riesen durcheinander mit home assistant"*. Grund: HA hat die Shellys
mit ihren alten Adressen eingebunden -- ein Umzug zerreisst die Integrationen.
🔴 **NUR die Shellys** — das war die erste, eng gefasste Ruecknahme. Die
uebrigen 10 Umzuege aus `job_tp45_v30_leases` (NukiBridge .11, P750W .13,
DTUBI .14, NukiSmartlock .177, SwitchBot .178, Twinkly .179, lichterketten
.190/.191, Withings .195/.196) und die beiden Sonderfaelle (CCU-Leiche
entfernt, NSPanelPro statisch auf .12) liess er unangetastet.
⚠️ **Noch am selben Tag ueberholt:** auf die weitergehende Direktive
(„wir sollten ALLE vlan30-aenderungen rueckgaengig machen" — Sperr-Risiko
Nuki) folgte `job_tp45_v30_restore`, der auch diese 10 plus beide
Sonderfaelle zurueckdreht. Dieser Job bleibt als der engere, zuerst
gelaufene Schritt bestehen.
Die Tabelle steht in `tp45-leases.py` als `REVERT` (abgeleitet aus `MOVES`,
gefiltert auf die 26 Shelly-MACs) -- NICHT hier, aus demselben Grund wie
`MOVES`: eine zweite Kopie laeuft still auseinander.
⚠️ **Reihenfolge:** erst `python3 nautobot/tp45-leases.py revert --apply`
(Soll zurueck), DANN diesen Job. Umgekehrt meldet GC sofort wieder Drift --
das Geraet waere richtig und das Soll falsch.
Abnahme: Geraete-Read `proplist=address,comment where server=dhcp-v30` -- die
26 Shellys muessen auf .202-.226/.233 stehen, die uebrigen 13 Eintraege
unveraendert. Danach `--refresh-compliance`, erwartet 145/145 und 0 Drifts.
"""
mod = _tp45_leases_module()
lines = [_lease_move(mac, alt, tag="tp45-v30-revert")
for mac, (_neu, alt) in sorted(mod.REVERT.items())]
# Abnahme-Zeile: die alten Baender muessen wieder voll sein, die neuen leer.
# Bewusst ein Bereichs-Paar statt aller 26 Adressen -- .202/.233 rahmen den
# alten Shelly-Block ein, .151/.176 den neuen.
lines.append(
':log info ("tp45-v30-revert: .151=" . [:tostr [:len [/ip dhcp-server lease '
'find where address=10.40.30.151]]] . " .176=" . [:tostr [:len '
'[/ip dhcp-server lease find where address=10.40.30.176]]] . " .202=" . '
'[:tostr [:len [/ip dhcp-server lease find where address=10.40.30.202]]] . '
' " .233=" . [:tostr [:len [/ip dhcp-server lease '
'find where address=10.40.30.233]]])')
return [(["crs326-core"], "\n".join(lines), "TP45-v30-shelly-revert")]
def job_tp45_v30_restore():
"""VOLLRUECKNAHME 2026-09-21: alle VLAN30-Aenderungen aus B2 zuruecknehmen.
Christoph-Direktive: *„wir sollten alle vlan30-aenderungen rueckgaengig machen.
du koenntest mich auf diesem wege aus meiner wohnung aussperren."*
Was B2 am 2026-09-21 getan hat, und was hier einzeln zurueckgedreht wird:
1. **36 Reservierungs-Adressen** verschoben -> hier ALLE 36 zurueck auf ihre
`MOVES`-Ist-Adresse (idempotent: die 26 Shellys stehen schon dort, ein
zweites `set` auf dieselbe Adresse ist ein No-Op).
2. **CCU-Leiche entfernt** (`BC:24:11:8E:0A:57`, .30.232) -> wieder angelegt,
wortgleich der Stand vor B2. Ob die Leiche spaeter geloescht werden soll,
ist eine **eigene** Entscheidung, nicht Teil dieser Ruecknahme.
3. **SONOFF-NSPanelPro auf statisch .12 gepinnt** -> Reservierung entfernt;
vor B2 hatte es keine (reiner DHCP-Client auf .249). `remove` ist der
Weg, weil RouterOS 7.24 kein `make-dynamic` kennt (s. u.).
⚠️ **Reihenfolge:** erst `python3 nautobot/tp45-leases.py restore --apply`
(Soll zurueck), DANN diesen Job.
🔴 **Warum die Eile berechtigt war:** die Reservierung eines Geraets mit
**lokaler** IP zu aendern, bewegt das Geraet nicht — aber bei einem Geraet,
das **DHCP** nutzt, haette die naechste Erneuerung es auf die neue Adresse
gezogen, und IP-gebundene Einbindungen (Home Assistant: Nuki, Shelly, …)
waeren gerissen. Dass hier nichts passiert ist, war Glueck, kein Entwurf.
Abnahme: Geraete-Read der v30-Leases + ARP-Band-Zaehlung; danach
`--refresh-compliance`, erwartet 145/145 und 0 Drifts.
"""
mod = _tp45_leases_module()
lines = [_lease_move(mac, ist, tag="tp45-v30-restore")
for mac, (ist, _soll) in sorted(mod.MOVES.items())]
# NSPanelPro: die statische Reservierung ENTFERNEN -- vor B2 hatte es keine,
# es war ein reiner DHCP-Client (dynamische Lease auf .249).
#
# 🔴 `make-dynamic` gibt es in RouterOS 7.24 NICHT: `/ip dhcp-server lease
# make-dynamic` antwortet `bad command name make-dynamic` (live gemessen
# 2026-09-21). Der erste Versuch dieses Jobs ist daran gestorben und hat den
# Plan auf `Failed` gesetzt, OBWOHL alle uebrigen Statements gelandet sind —
# genau die Klasse „Completed/Failed beweist nichts, das Geraet ist die
# Wahrheit". `make-static` existiert (B2 hat es benutzt), das Gegenstueck
# fehlt; der Rueckweg ist `remove`.
#
# ⚠️ Das Panel haelt .12 noch bis zum Lease-Ablauf (~20 h, `status=bound`,
# `active-address=10.40.30.12`) und faellt danach in den Pool zurueck.
# Das ist eine von B2 erzeugte Uebergangserscheinung, keine neue Aenderung.
for add in mod.ADDS:
mac = re.search(r"mac-address=(\S+)", add).group(1)
find = f'find where mac-address={mac}'
arg = f'[find where mac-address={mac}]'
lines.append(f':if ([:len [/ip dhcp-server lease {find}]] = 1) do={{ '
f'/ip dhcp-server lease remove {arg}; '
f':log info ("tp45-v30-restore: NSPanelPro-Reservierung entfernt") }} '
f'else={{ :log info ("tp45-v30-restore: NSPanelPro hat keine Reservierung") }}')
# CCU-Leiche wieder anlegen -- nur wenn sie fehlt.
for mac in sorted(mod.REMOVE):
find = f'find where mac-address={mac}'
lines.append(f':if ([:len [/ip dhcp-server lease {find}]] = 0) do={{ '
f'/ip dhcp-server lease {mod.RESTORE_ADD}; '
f':log info ("tp45-v30-restore: CCU-Leiche wieder angelegt") }} '
f'else={{ :log info ("tp45-v30-restore: CCU-Leiche ist schon da") }}')
# Abnahme: die alten Baender muessen voll sein, die neuen leer.
lines.append(
':log info ("tp45-v30-restore: alt .202=" . [:tostr [:len [/ip dhcp-server lease '
'find where address=10.40.30.202]]] . " .233=" . [:tostr [:len '
'[/ip dhcp-server lease find where address=10.40.30.233]]] . " .244=" . '
'[:tostr [:len [/ip dhcp-server lease find where address=10.40.30.244]]] . '
' " neu .11=" . [:tostr [:len [/ip dhcp-server lease '
'find where address=10.40.30.11]]] . " .151=" . [:tostr [:len '
'[/ip dhcp-server lease find where address=10.40.30.151]]] . " .177=" . '
'[:tostr [:len [/ip dhcp-server lease find where address=10.40.30.177]]])')
return [(["crs326-core"], "\n".join(lines), "TP45-v30-restore")]
def _dns_static_lines():
"""Die `/ip dns static add`-Zeilen aus dem GC-Template, wortgetreu.
🔴 **Quelle ist das Template selbst, NICHT eine zweite Kopie.** Eine handgepflegte
Liste hier liefe beim ersten Template-Edit auseinander — still, weil beide Seiten
fuer sich plausibel aussehen; am Geraet faellt nur auf, dass ein Name fehlt.
Und die Kommentare muessen **zeichengleich** sein: Golden Config vergleicht
`comment=` mit, ein abweichender Text ist eine Drift.
Der `/ip dns static`-Block ist ein Literal im Template (im Gegensatz zu
`firewall_filter` und `dhcp_leases`, die als Daten aus
`local_config_context_data` kommen) — deshalb ist das Parsen hier zulaessig.
"""
text = (pathlib.Path(__file__).with_name("mikrotik_routeros.j2")).read_text()
return [ln.strip() for ln in text.splitlines()
if ln.strip().startswith("/ip dns static add ")]
def _dns_upsert(line):
"""Eine Template-Zeile -> idempotenter Upsert, EINZEILIG und klammer-vollstaendig.
`/ip dns static add` ist NICHT idempotent — ein zweiter Lauf legte den Namen ein
zweites Mal an. Deshalb: nur anlegen, wenn der Name fehlt, sonst in place setzen.
Der Selektor wird wie in `_lease_move` gezaehlt (`find where …` in `[:len …]`,
`[find where …]` als Argument) — die vertauschte Form ist ein Syntaxfehler, der
als `Completed` durchlaeuft und das Geraet unveraendert laesst.
"""
addr = re.search(r"address=(\S+)", line).group(1)
name = re.search(r"name=(\S+)", line).group(1)
cm = re.search(r'comment=("(?:[^"\\]|\\.)*"|\S+)', line)
comment = f" comment={cm.group(1)}" if cm else ""
return (f':if ([:len [/ip dns static find where name={name}]] = 0) do={{ '
f'{line} }} '
f'else={{ /ip dns static set [find where name={name}] address={addr}{comment} }}')
def job_tp45_c_dns():
"""TP45 Schicht C (2026-09-22): die internen Namen in den CORE — der FQDN-Fix.
Hintergrund: AGH wertet seine DNS-Rewrites nur im **Filter-Pfad** aus. Fuer
unfiltrierte Clients (`dns-bypass-exempt` = beide Mac-NICs, `grp-infra`,
`grp-media`, `grp-mgmt`) greifen sie nicht, und die fallen auf den conditional
forward `[/bf.emsenhuber.net/] -> 10.40.20.1` zurueck — den Core. Der hielt
aber nur 10 Statics; 12 Namen existierten nur als AGH-Rewrite und waren damit
**NXDOMAIN**. Vom Client gemessen 2026-09-22: alle 12 leer, `vw.emsenhuber.net`
lieferte die Cloudflare-Anycast-IP statt `.40` — und genau daher kam der
404 auf `/admin` (der CF-Weg setzt `CF-Connecting-IP`, der `@cf_admin`-Guard
antwortet darauf mit 404).
Dieser Job bringt die Statics auf das Geraet. Danach beantwortet der Core alle
internen Namen unabhaengig vom Filterstatus des Clients — und, weil die Namen
ueber Caddy laufen, mit gueltigem TLS.
⚠️ **Die 5 bare Namen (`vw`, `hass`, `n8n`, `gitea`, `ds-bf`) kommen bewusst mit
auf `.40`** (Christoph-Entscheid 2026-09-22): der lokale, header-freie Weg ist
die einzige Tuer zum Vault-Admin. ⚠️ Nebenwirkung: diese Namen werden damit fuer
**alle** VLANs aufloesbar, nicht nur fuer MAIN — MEDIA (`*A`) und GUEST
(`*B`+`*A`) erreichen `.40:443` ebenfalls. Einhegung ist ein eigener Schritt.
Rückweg: die Vorher-Werte stehen als `A1`/`A2`-Tabellen im Plan
(`~/.claude/plans/…`); vor dem Deploy liegt ein Backup auf dem Core.
"""
lines = [_dns_upsert(ln) for ln in _dns_static_lines()]
# Abnahme-Zeile: die Zahl der Statics plus drei Stichproben, die den Ausfall
# repraesentieren (ap3 gab es vorher nirgends, pve zeigt auf Caddy, vw muss
# `.40` sein — nicht die CF-Anycast-IP).
lines.append(
':do { :log info ("tp45-c-dns: statics=" . [:tostr [:len [/ip dns static find]]] '
'. " ap3=" . [:tostr [:len [/ip dns static find where name=ap3.bf.emsenhuber.net]]] '
'. " vw=" . [/ip dns static get [find where name=vw.emsenhuber.net] address] '
'. " pve=" . [/ip dns static get [find where name=pve.bf.emsenhuber.net] address]) } '
f'on-error={{ :log info "tp45-c-dns: Abnahme-Zeile geworfen" }}')
return [(["crs326-core"], "\n".join(lines), "TP45-c-dns")]
JOBS = {
"drift-cleanup": job_drift_cleanup,
"mgmt-acl": job_mgmt_acl,
@@ -730,8 +933,11 @@ JOBS = {
"tp38-oob": job_tp38_oob,
"tp45-agh-fw": job_tp45_agh_fw,
"tp45-agh-dhcp": job_tp45_agh_dhcp,
"tp45-c-dns": job_tp45_c_dns,
"tp45-pool-v20": job_tp45_pool_v20,
"tp45-v30-leases": job_tp45_v30_leases,
"tp45-v30-revert": job_tp45_v30_revert,
"tp45-v30-restore": job_tp45_v30_restore,
}

View File

@@ -312,18 +312,49 @@
# FT-Transition statt Voll-Reassoziation. Das Band-Springen selbst endet erst
# mit dem Deckungsfix (TP25 Variante 3). Siehe teilprojekte.md TP25 (Stand 09-14).
# Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich):
# core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12
# Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich; ap3 ergaenzt 2026-09-22):
# core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12 | ap3 .99.13
#
# 🔴 TP45 Schicht C (2026-09-22): DIESE Liste ist die EINZIGE Quelle fuer interne Namen.
# AGH wertet seine DNS-Rewrites nur im Filter-Pfad aus; fuer unfiltrierte Clients
# (dns-bypass-exempt, grp-infra/-media/-mgmt) greifen sie nicht — die landen hier.
# Hier fehlten 12 Namen, genau das war der FQDN-Ausfall.
# Merkregel: was in AGH als Rewrite steht, muss auch hier stehen.
/ip dns static add address=10.40.99.1 comment="crs326-core (L3)" name=core.bf.emsenhuber.net type=A
/ip dns static add address=10.40.99.2 comment=crs326-branch name=branch.bf.emsenhuber.net type=A
/ip dns static add address=10.40.99.11 comment="ap1 = reiner AP" name=ap1.bf.emsenhuber.net type=A
/ip dns static add address=10.40.99.12 comment="ap2 = reiner AP" name=ap2.bf.emsenhuber.net type=A
/ip dns static add address=10.40.99.13 comment="ap3 = reiner CAP (cAP ax)" name=ap3.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.167 name=macmini.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.100 name=pve.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.173 name=pve-mcp.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.10 name=nas.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.126 name=docker.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.20 name=platform-pg.bf.emsenhuber.net type=A
/ip dns static add address=10.40.10.6 comment="Brother MFC (VLAN10)" name=mfc.bf.emsenhuber.net type=A
/ip dns static add address=10.40.40.11 comment="TV (VLAN40)" name=tv.bf.emsenhuber.net type=A
# Caddy-Frontends (TP31): Dienste mit eigener Site laufen ueber den Reverse Proxy, damit das
# TLS-Zertifikat gueltig ist. Quelle: caddy/Caddyfile (live geprueft 2026-09-22).
/ip dns static add address=10.40.20.40 comment="caddy -> nautobot :8080" name=nautobot.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> pbs :8007" name=pbs.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> portainer :9443" name=portainer.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> ccu :80" name=ccu.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> iobroker :8081" name=iobroker.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> z2m25 :8080" name=z2m25.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> home-assistant mcp :9583" name=ha-mcp.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> n8n-mcp :3001" name=n8n-mcp.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> gitea-mcp :3000" name=gitea-mcp.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> pve :8006" name=pve.bf.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="caddy -> pve-mcp :3000" name=pve-mcp.bf.emsenhuber.net type=A
# Oeffentliche Namen (bare emsenhuber.net) — real via CF Access + Tunnel. Sie stehen hier NUR,
# damit LAN-Clients direkt auf Caddy kommen: der CF-Weg setzt CF-Connecting-IP, und der
# @cf_admin-Guard in caddy/Caddyfile antwortet darauf mit 404 (TP41 T2). Der lokale,
# header-freie Weg ist die einzige Tuer zum Vault-Admin (Christoph-Entscheid 2026-09-22).
/ip dns static add address=10.40.20.40 comment="vw via caddy (Vault-Admin nur lokal)" name=vw.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="hass via caddy" name=hass.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="n8n via caddy" name=n8n.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="gitea via caddy" name=gitea.emsenhuber.net type=A
/ip dns static add address=10.40.20.40 comment="ds-bf via caddy" name=ds-bf.emsenhuber.net type=A
# [7] NAT
/ip firewall nat add action=accept chain=srcnat comment="TP4: no-NAT inter-VLAN (LAN-internal, before masquerade)" dst-address=10.40.0.0/16
@@ -365,6 +396,17 @@
/ip address add address=100.96.0.6 comment="TP16.1 Mesh-Overlay" interface=cf-mesh network=100.96.0.6
/ip route add comment="TP16.1 Mesh" disabled=no distance=1 dst-address=100.96.0.0/12 gateway=cf-mesh
# [10b] EGRESS (2026-09-26) — ziel-basierter Ausgang fuer 164.90.191.7/32 (cannaleo/DO-Origin).
# Der Origin blockt die Heim-IP 90.146.33.177 hart (403), Cloudflare aber nicht: WARP-Client
# und cf-mesh kommen beide durch (live gemessen, gleiche Ausgangs-IP 104.28.x). Genau EIN Ziel,
# ein Ausgang — kein Default-Route-Eingriff, restlicher Traffic unberuehrt. Kein extra NAT noetig:
# masquerade out-interface-list=WAN greift fuer cf-mesh NICHT, CF-NATet die LAN-Source im Mesh
# selbst (verifiziert von macmini 10.40.20.167). Cave User: kein Zugriffsmuster, das Heim-IP und
# diesen Ausgang gemeinsam gegen den Origin zeigt — der Ausgang ist ausschliesslich fuer cannaleo.
/routing table add fib name=egress-cannaleo
/ip route add comment="TP-EGRESS: cannaleo via cf-mesh" disabled=no distance=1 dst-address=0.0.0.0/0 gateway=cf-mesh routing-table=egress-cannaleo
/routing rule add action=lookup-only-in-table comment="TP-EGRESS: cannaleo 164.90.191.7/32 via cf-mesh" disabled=no dst-address=164.90.191.7/32 table=egress-cannaleo
# [11] QoS — BEWUSST LEER (TP6 geschlossen 2026-08-23).
#
# Hier stand einen Tag lang eine CAKE-Queue. Sie ist entfernt, weil sie GEMESSEN

View File

@@ -49,6 +49,10 @@ Drift mehr (genau der Grund, warum `ingest-core-firewall.py` manuell bleibt).
python3 nautobot/tp45-leases.py seed --force --apply # Neuschnitt, ueberschreibt
python3 nautobot/tp45-leases.py move # Dry-Run, s. MOVES
python3 nautobot/tp45-leases.py move --apply # B2
python3 nautobot/tp45-leases.py revert # Dry-Run, s. REVERT (nur Shellys)
python3 nautobot/tp45-leases.py revert --apply # Ruecknahme 2026-09-21
python3 nautobot/tp45-leases.py restore # Dry-Run: ALLES zurueck auf vor B2
python3 nautobot/tp45-leases.py restore --apply # Vollruecknahme 2026-09-21
Token: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored), NAUTOBOT_SUPERUSER_API_TOKEN.
Der Wert wird nie ausgegeben.
@@ -116,12 +120,49 @@ MOVES = {
# .150 (Ender3V3KE) und .180 (espressif/Airfryer) bleiben, wo sie sind.
}
# ---------------------------------------------------------------------------
# RUECKNAHME 2026-09-21 (Christoph-Direktive, sofort): NUR die Shellys.
#
# Die 26 Shelly-Reservierungen gehen auf ihre ALTEN Adressen zurueck. Grund:
# Home Assistant hat sie mit den alten Adressen eingebunden -- der Umzug haette
# die HA-Integrationen zerrissen. Die uebrigen 10 Umzuege (NukiBridge, P750W,
# DTUBI, NukiSmartlock, SwitchBot, Twinkly, Lichterketten, Withings) BLEIBEN.
#
# Bewusst als eigene Menge und nicht als Bereichs-Ableitung: welche MAC eine
# Shelly ist, steht nur im Kommentar der Soll-Zeile, nicht im MOVES-Key.
# ---------------------------------------------------------------------------
SHELLY = {
"E4:B0:63:D7:C7:A4", "28:37:2F:36:F4:7C", "E4:B0:63:E4:EF:7C",
"E4:B0:63:E4:0F:30", "D8:85:AC:0C:5C:C8", "D0:CF:13:D8:18:FC",
"D0:CF:13:D9:94:E8", "E4:B0:63:FA:FD:E0", "E8:F6:0A:61:88:08",
"E4:B0:63:FC:57:E4", "E4:B0:63:FA:6C:00", "48:F6:EE:C8:E6:AC",
"48:F6:EE:DD:6A:F4", "E4:B0:63:FC:59:48", "E4:B0:63:FA:D5:34",
"10:20:BA:FA:F5:40", "10:20:BA:FB:6D:64", "48:F6:EE:CC:16:E4",
"10:20:BA:FB:70:FC", "98:A3:16:7C:5F:EC", "20:6E:F1:01:F9:F8",
"A0:85:E3:C8:65:34", "AC:15:18:7B:62:20", "14:33:5C:D8:2D:7C",
"98:A3:16:79:38:50", "98:A3:16:7A:A3:54",
}
if len(SHELLY) != 26:
raise SystemExit("SHELLY hat %d MACs statt 26 -- Tabelle beschaedigt." % len(SHELLY))
# Schluessel = MAC, Wert = (aktuelle Soll-Adresse, Adresse davor) -- also die
# Umkehrung von MOVES, aber nur fuer den Shelly-Block.
REVERT = {mac: (soll, ist) for mac, (ist, soll) in MOVES.items() if mac in SHELLY}
if len(REVERT) != 26:
raise SystemExit("REVERT hat %d Eintraege statt 26 -- MOVES/SHELLY passen nicht zusammen."
% len(REVERT))
# CCU-Leiche: VLAN30-Reservierung `pve-homematic-raspi-ccu`, status=waiting,
# last-seen=never. Die CCU ist live in VLAN20 (10.40.20.232, ARP+HTTP belegt,
# Caddys ccu.bf proxyt dorthin) — dieser Eintrag ist ein Zombie aus dem
# Core-Cutover. Er wird ENTFERNT, nicht umgezogen.
REMOVE = {"BC:24:11:8E:0A:57"}
# Die Zeile, mit der B2 die CCU-Leiche entfernt hat -- wortgleich der Zustand
# VOR B2, Quelle `routeros/tp21-core-dhcp-from-ap1.rsc` Z. 70 (dort ohne
# `disabled`). Fuer den vollstaendigen Rueckweg wird sie wieder angelegt.
RESTORE_ADD = ("add address=10.40.30.232 comment=pve-homematic-raspi-ccu "
"mac-address=BC:24:11:8E:0A:57 server=dhcp-v30")
# SONOFF-NSPanelPro hat KEINE Reservierung, nur eine dynamische Lease auf .249.
# Wird in B2 per make-static + set address gepinnt. Zeile hier nur als Soll.
ADDS = [
@@ -207,7 +248,7 @@ def show(old, new):
def main():
argv = sys.argv[1:]
if not argv or argv[0] not in ("seed", "move", "check"):
if not argv or argv[0] not in ("seed", "move", "revert", "restore", "check"):
raise SystemExit(__doc__.strip().splitlines()[-2].strip())
cmd, apply_ = argv[0], "--apply" in argv
dev = device()
@@ -232,6 +273,57 @@ def main():
new, when = backup_lines()
print("== seed aus Backup %s (%d Zeilen)%s =="
% (when, len(new), " [--force, ueberschreibt %d Zeilen]" % len(soll) if soll else ""))
elif cmd == "restore":
if not soll:
raise SystemExit("Soll ist leer — nichts wiederherzustellen.")
drop = {mac_of(a) for a in ADDS}
new, seen, moved, dropped = [], set(), [], []
for line in soll:
mac = mac_of(line)
if mac in drop:
dropped.append(mac)
continue
if mac in MOVES:
ist_ip, soll_ip = MOVES[mac]
cur = re.search(r"address=(\S+)", line).group(1)
if cur != ist_ip:
if cur != soll_ip:
raise SystemExit("%s: Soll traegt weder %s noch %s — Soll, MOVES und "
"Wirklichkeit auseinander. Zeile: %s"
% (mac, ist_ip, soll_ip, line[:120]))
line = set_address(line, ist_ip)
moved.append(mac)
seen.add(mac)
new.append(line)
fehlend = set(MOVES) - seen
if fehlend:
raise SystemExit("MOVES nennt MACs, die im Soll fehlen: %s"
% ", ".join(sorted(fehlend)))
for mac in sorted(REMOVE):
if mac not in seen:
new.append(RESTORE_ADD)
moved.append(mac)
print("== restore: %d Adressen zurueck, %d Zeilen entfernt, %d wieder angelegt "
"(Soll jetzt %d Zeilen) ==" % (len(moved), len(dropped), len(REMOVE), len(new)))
elif cmd == "revert":
if not soll:
raise SystemExit("Soll ist leer — es gibt nichts zurueckzunehmen.")
new, seen = [], set()
for line in soll:
mac = mac_of(line)
if mac in REVERT:
neu_ip, alt_ip = REVERT[mac]
if re.search(r"address=(\S+)", line).group(1) != neu_ip:
raise SystemExit("%s: Soll traegt nicht %s — Soll und REVERT auseinander. "
"Zeile: %s" % (mac, neu_ip, line[:120]))
line = set_address(line, alt_ip)
seen.add(mac)
new.append(line)
if seen != set(REVERT):
raise SystemExit("REVERT nennt MACs, die im Soll fehlen: %s"
% ", ".join(sorted(set(REVERT) - seen)))
print("== revert: %d Shelly-Reservierungen zurueck auf die alten Adressen =="
% len(REVERT))
else:
if not soll:
raise SystemExit("Soll ist leer — erst `seed` laufen lassen (Schicht B1).")