34 Commits

Author SHA1 Message Date
6c2d9c0ea7 docs: DNS-Override-Mechanismus bleibt definiert, Airfryer-Cloud-Integration bleibt
Praezisierung zum Eintrag von 2026-10-12. Zwei Entscheidungen festgehalten:

- Der DHCP-Override-Mechanismus (dns 1.1.1.1) und die DNS-Redirect-Variante (a)
  bleiben als Werkzeug fuer kuenftige Low-Trust-Kandidaten definiert und werden
  NICHT abgebaut. Er verliert nur fuer den Airfryer seine Rechtfertigung.
- Die Cloud-Integration des Airfryers bleibt bewusst in Betrieb.

Die vorherige Formulierung "kann abgebaut werden" war zu scharf und haette eine
Folge-Session dazu verleiten koennen, den Override wegzuraeumen.

Co-Authored-By: Claude Code (deepseek-v4-pro)
2026-10-12 17:35:46 +02:00
5c8824850f docs: Airfryer ist FUSION/Cloud-only -- UNTRUSTED-Move zurueckgestellt (TP5.1)
Airfryer "airschreier" (10.40.30.180, VLAN30) hat die Philips-FUSION-Migration
bereits vollzogen. In HA verifiziert:

- Config-Entry `philips_homeid`, Titel "Airschreier 5000 Series XXL (Cloud)"
  -- das (Cloud) ist die FUSION-Markierung der Integration
- NCP 1.1.14 / Host 0.5.6 -> matcht die dokumentierte FUSION-Liste
  (HomeID-App zeigt die Reihenfolge umgedreht an: 0.5.6/1.1.14)
- Lokaler Port 80/443 antwortet nicht; mDNS announct weiter
- Kein Weg zurueck (cloud-seitige externalDeviceId-Verankerung)

Konsequenzen fuer TP5.1:
- UNTRUSTED-Move-Kandidat zurueckgestellt: bei FUSION ist die Cloud die
  einzige Steuerungsroute, internetloses VLAN macht das Geraet unsteuerbar
- DHCP-Override dns 1.1.1.1 verliert seine OTA-Rechtfertigung
- VLAN-Nummer in der Prosa korrigiert: UNTRUSTED ist VLAN 40, nicht 50

Co-Authored-By: Claude Code (deepseek-v4-pro)
2026-10-12 17:34:06 +02:00
359ad666c4 TP49 abgenommen: fremder GitHub-Account wird abgewiesen
Verhaltensabnahme durch echten Login (nicht nur API-Read): Christophs zweiter
GitHub-Account wird abgewiesen, eigener Zugang und Geli funktionieren, uebrige
Auth-Wege wie erwartet. email AND github trennt eigenes von fremdem Konto,
ohne jemanden auszusperren.

Der Fix war bis dahin nur gesetzt und zurueckgelesen — die API sagt nichts
darueber, ob die Regel greift. Mit diesem Test ist sie verhaltensbestaetigt.

TP49 geschlossen, Gitea #76 closed / status:done, kein offener Punkt.
Der Umweg (Fehlschluss -> Schliessung -> Wiedereroeffnung -> Fix -> Abnahme)
bleibt als Aktenhistorie in allen drei Dateien stehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 08:27:17 +02:00
4d4779e400 TP49 behoben: login_method:github von include nach require
Christophs zweiter GitHub-Account kam durch — der Ursprungsfund war echt. Die
vormittägliche Schliessung als "Phantom" war ein Fehlschluss (Google-Test
ungeprueft auf GitHub uebertragen) und ist hiermit zurueckgenommen.

Fix: login_method:github aus include raus, in require -> email AND github.
Gesetzt via PUT /accounts/{id}/access/policies/{policy_id} (der App-Pfad gibt
12130 fuer reusable Policies), zurueckgelesen ueber beide Endpunkte.

Sweep ueber alle Apps: login_method nur noch in Terpenfinder v3 — christoph
(github+onetimepin) und geli (nur onetimepin, harmlos). Scope geprueft: die
reusable Policy haengt nur an dieser einen App, kein Kollateral.

Drei neue Gotchas: 12130 (reusable nur unter /access/policies/{id}, der alte
Merkatz meinte das Detach, nicht das Aendern), MCP read-only (1010), 32-Hex-IDs.

Grenze dokumentiert: die Absicht war Kopplung an den GitHub-Username — den
Selector gibt es nicht. Identity-provider-group braucht SCIM (hier disabled,
scim/groups -> 500/12158). email AND github ist das vollstaendige Moegliche.

Aktenhistorie des Umwegs bleibt in allen drei Dateien stehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 08:04:15 +02:00
4b307315f0 TP49 widerlegt und geschlossen — kein Fix, Doku korrigiert
Christoph hat die Praemisse am 2026-10-06 selbst widerlegt: testus@emsenhuber.com
wird von CF Access abgelehnt. Die E-Mail-Bindung der Policy gilt, login_method
hebelt sie nicht aus. Es gab nie eine offene Tuer.

Der Fehler war ein Fehlschluss, kein Raten: Doku-Semantik zu login_method
("welche Methode wurde benutzt") wurde so gelesen, als ersetze der Include die
danebenstehende email-Zeile. include ist ein OR — die Zeilen sind Alternativen,
nicht Bedingungen. Aus "GitHub ist eine Alternative zur E-Mail" wurde "GitHub
allein genuegt", und das stand als Tatsache in der Akte (Fund 2026-07-10).

Belegt: kein IdP traegt eine Domain-Bindung (github/google/onetimepin alle nur
client_id + redirect_url, live per GET .../identity_providers). Die Einschraenkung
kann also nur aus der Policy kommen — und sie greift nachweislich.

Grenze der Aussage: der GitHub-Pfad selbst wurde nicht gemessen, nur geschlossen.

Korrigiert: teilprojekte.md TP49 (Herkunft des Fehlers mitgenannt), cf-tunnel-
mapping.md Paragraf 10 (Ursprungsfund widerlegt, nicht geloescht), Startauftrag
als "NICHT AUSFUEHREN" markiert. Kein Schreibzugriff auf die Terpenfinder-Policy;
blauebluethen-Projektgrenze gewahrt.

Erhalten bleiben nur die Gotchas: 32-Hex-App-IDs, cloudflare-MCP ist read-only
(Write-Probe no-op-PUT -> 1010), Reusable Policy nur per App-PUT detachbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 07:37:46 +02:00
81cd7bfb7e TP49-Startauftrag: MCP-Pruefschritt an den Anfang, read-only-Grenze dokumentiert
Christoph-Direktive: zu Sessionbeginn pruefen ob der Cloudflare-MCP laeuft, sonst anhalten.

Der MCP ist als Remote-Server definiert (https://mcp.cloudflare.com/mcp, OAuth) und
laut cf-tunnel-mapping.md:321 read-only — er kann den Fix nicht ausfuehren. Der
Bestandsaufnahme-Teil laeuft ueber den MCP, die Aenderung braucht curl + CF_API_TOKEN.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:54:47 +02:00
c5086db223 TP49-Startauftrag: Vorbereitung fuer die Durchfuehrungs-Session
Live geprueft: interner Wildcard ist durch TP41 sauber (Groups statt login_method).
Offen ist genau ein Eintrag — login_method:github in Policy 'christoph' auf
420.teilzeitladen.xyz (oeffentlich, Projekt blaueblueten).

Enthaelt konkrete IDs, die vier bekannten Gotchas (32-Hex-App-IDs, Schreib-Token,
Reusable-Policy-Detach, Projektgrenze) und den Ablauf inkl. Ruecksprache-Pflicht.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:49:22 +02:00
a3f2ade4e0 TP49: Befund live korrigiert — nur 420.teilzeitladen.xyz betroffen, nicht die interne Infra
Der Fund vom 2026-07-10 beschrieb login_method-Includes am internen Wildcard
*.emsenhuber.net. Live-Nachpruefung am 2026-10-06: dieser Teil ist durch TP41 behoben —
die Policies binden jetzt an Groups (E-Mail/IP/service_token).

Offen ist genau ein Eintrag: login_method:github in Policy 'christoph' auf
420.teilzeitladen.xyz (oeffentlich, Projekt blaueblueten). Kein Zugriff auf
hass/pve/portainer — die kritische Tuer war schon zu.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:48:42 +02:00
52a8ac462c TP49 angelegt: CF-Access-Wildcard oeffnet alle internen Dienste fuer jeden GitHub/Google-User
Fund vom 2026-07-10, seither unbehoben — stand nur in der Memory project_cf_tunnel_1033,
in keiner TP-Liste. Beim Sichern der Memories wiederentdeckt.

login_method:<idp> als include matcht JEDEN via diesen IdP, nicht den Owner. Betroffen
sind alle Dienste hinter *.emsenhuber.net (hass/grafana/portainer/pve). Gitea #76.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:47:03 +02:00
9638a03e4b CLAUDE.md: Memory-Versionierung dokumentiert
Memories sind seit 2026-10-06 versioniert (Master admin/claude-memories, Arbeitsbaum
~/dev/claude-memories/, Anbindung per Symlink). Dasselbe Muster wie bei den Skills,
aus demselben Befund: reiner Home-Ordner-State, kein Backup.

Festgehalten, weil beides am 2026-10-06 belegt gerissen ist:
- name: muss dem Dateinamen entsprechen — 12 Wikilinks zeigten ins Leere, weil 8
  Dateien einen abweichenden Slug trugen. Bricht still, kein Fehler.
- MEMORY.md ist Index, kein Log — nur dieses File wird pro Session geladen. 67
  Log-Zeilen trieben den r6-Index auf 16,4 KB (mehr als alle anderen elf zusammen).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:45:13 +02:00
fb415ae52d TP47 (Gitea #74) abgeschlossen: Egress-Weg verifiziert, Prosa ins Archiv
Nachmessung vom Core aus (crs326-core via MikroMCP, 15er-Stichproben je Zustand):
Regel AN -> 15/15 200 (168,1 KiB), Regel AUS -> 14/15 403. Der Weg ist
deterministisch; der eine Ausreisser sitzt reproduzierbar am ersten Sample
nach dem Umschalten (Connection-/Cache-Nachlauf), nicht am Pfad.

Merkregel im Archiv festgehalten: nie ein Einzelsample werten, erste Anfrage
nach Routing-Umschalten verwerfen.

Template [10b] in nautobot/mikrotik_routeros.j2 deckungsgleich mit live
(Table egress-cannaleo, Route 0.0.0.0/0 -> cf-mesh, Regel 164.90.191.7/32),
Drift 0. Issue #74 geschlossen, Label status:done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 21:37:34 +02:00
f63149a769 TP48 (Gitea #75) angelegt: Recovery-Luecke + Ansible-Abloese-Evaluation
Anlass aus TP10-Abschluss: DDNS-Updater lebt als /system script auf dem Core, in keinem
Template — Recovery von null haette ihn nicht. Systematische Klasse, nicht Einzelfund.

- Gitea #75: TP48 mit status:todo + thema:nautobot-doku angelegt
- docs/teilprojekte.md: Prosa-Abschnitt TP48 (Inventur + Evaluationsfragen)
- Live gemessener Kernbefund: Nautobot ist heute nur TEILWEISE SSoT — datengetrieben sind
  nur Device-Felder, interfaces/IPs und zwei ConfigContexts; 7 VLAN-Defs, 37 bridge-port-
  und 19 bridge-vlan-Zeilen, Core-Firewall, NAT/Mangle/Queue/Routen stehen LITERAL im Jinja.
  Der Abloese-Vergleich lautet damit 'Ansible vs. Jinja-im-Repo', nicht 'vs. Nautobot-SSoT'.
- Nebenbefund dokumentiert (nicht angefasst): WG-Hub-Privkey im Klartext im Recovery-Skript
  tp21-wan-wg-restore auf dem Core
- Korrektur: 'Abloesung durch Komodo' (CLAUDE.md + Archiv) war eine Kategorie-Verwechslung —
  Komodo verwaltet Container, nicht RouterOS-Config; es kann diese Luecke nicht schliessen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 19:05:02 +02:00
3601a2b6cb TP10: Prosa ins Archiv verschoben, Uebersichtstabelle auf abgeschlossen
- teilprojekte.md: TP10-Volltext-Abschnitt entfernt (62 Zeilen), Tabelle 'Zugang' auf
  abgeschlossen 2026-10-05 gesetzt
- teilprojekte-archiv.md: zurueckgeholten Volltext als Fortsetzung hinter der eingefrorenen
  ap1-Historie eingefuegt, eigener Anchor tp10-ddns (Doppel-Anchor aufgeloest),
  Inhaltsverzeichnis-Zeile aktualisiert

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 18:56:19 +02:00
75bd43d1f7 TP10: DDNS-Updater live + Mesh-NAT im GC-Template konform — TP abschliessbar
Close-Bedingung 1 (DDNS-Updater bf4.emsenhuber.net):
- Global-Scope-Fix (6e2a7cf): /system script environment get statt :global cfDdnsToken
  (RouterOS 7.24 trennt CLI- und Skript-Scope)
- Live auf crs326-core: Script tp10-ddns-bf4, Trigger DHCP-Client client1 (Lease-Event),
  Token in /system script environment (53 Zeichen)
- Positiv-Test bestanden: A-Record auf 1.2.3.4 gesetzt -> Script-Lauf -> 90.146.33.177,
  Comment '2026.10.03-18:08 by crs326-core', Idempotenz-Variable CFWanIP4Cur gesetzt

Close-Bedingung 2 (Mesh-NAT-Regel im taeglichen GC-Lauf konform):
- nautobot_golden_config: ip-firewall-nat = true fuer crs326-core
- Regel in der Intended Config (Z.425), nicht nur im Backup
- Zwei taegliche AllDevicesGoldenConfig-Laeufe nach dem Fix gruen (2026-10-04, 2026-10-05)

Doku: Handoff-Datei entfernt, TP10-Prosa in teilprojekte.md auf abgeschlossen gesetzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 18:54:57 +02:00
315f1e79e7 TP28: WLAN/CAPsMAN abgeschlossen — ap1-ap3 platziert, Kanalplan committed
- Standort final: ap1 Sideboard · ap2 Technik UG · ap3 Technik OG, alle drei CAPs am crs326-core
- Kanalplan committed (ap1 5260/5280, ap2 5300/5320, ap3 5180), Template mikrotik_routeros.j2
- wifi-cap-Rest auf dem Core (cap enabled=yes, Manager ohne eigene Radios) auf enabled=no bereinigt
- Compliance 29/29 über 5 Geräte, 0 Drifts (145/145 im GC-Job)
- Transaktionen: TEST-ether3-trunk-ap1-sideboard-2 + TP25-kanalplan-ap1-live committed in SoT
- Prosa: Volltext nach teilprojekte-archiv.md, Pointer bleibt in teilprojekte.md
- Gitea #18: body aktualisiert + Abschluss-Kommentar

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-05 13:16:42 +02:00
6e2a7cfb47 TP10: Global-Scope-Fix für cfDdnsToken im DDNS-Updater (RouterOS 7.24 Skript-Scope vs CLI-Scope)
Problem: :global cfDdnsToken liest im Skript-Kontext 'nothing', obwohl
/system script environment cfDdnsToken gesetzt ist (53 Zeichen, live verifiziert).

Fix: /system script environment get statt :global — scope-unabhängig auf ROS 7.24.
Skript importiert als tp10-ddns-bf4, Trigger: DHCP-Client client1 (Lease-Event),
kein stündlicher Scheduler nötig (Entscheidung 2026-10-03).

Recovery-Lücke dokumentiert: GC-Template rendert kein /system script /
/system scheduler — Recovery von null hätte Updater nicht.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-03 17:38:55 +02:00
5ad71b37c0 TP10: Mesh-DDNS-Updater gebaut, Handoff für Finalisierung
Skript `tp10-ddns-bf4` auf crs326-core angelegt (MIT, Basis bajodel/mikrotik-cloudflare-dns):
- WAN-IP aus DHCP-Lease (client1/vlan100), NICHT von Echo-Diensten
- check-certificate=yes, Public-IPv4-Guard, Idempotenz, Token als globale Variable

CF_DNS_API_TOKEN (eng gescoped: Zone→DNS:Edit nur emsenhuber.net/bf4) liegt
in ~/.secrets/credentials.env und ist als cfDdnsToken auf Core gesetzt.

Offen: Global-Scope-Problem (Variable im Skript nicht sichtbar) — Handlauf 1+2,
Scheduler, Positiv-Test, TP10 #49 schließen stehen aus.
Handoff-Dokumentation in docs/handoff-20261003-tp10-ddns-finalisierung.md.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-03 17:17:33 +02:00
5bcd0b6989 TP10 reopened: Mesh-Quell-NAT dokumentiert, widerlegte Handoff-Diagnose entfernt
Der Fix aus 1fddf5d ist live und verifiziert. Dieser Commit traegt die Doku nach und
raeumt eine falsche Diagnose weg, die eine Folge-Session in die Irre gefuehrt haette.

- TP10 (#49) reopened (status:wip), Prosa-Volltext zurueck aus dem Archiv nach
  teilprojekte.md; im Archiv bleibt der Zeiger.
- Neuer Befund dokumentiert: WARP-Clients kommen via cf-mesh mit 100.96.0.0/12 an,
  ausserhalb der Spoke-AllowedIPs (10.40.0.0/16) -> FritzBox verwirft. Beweis-Tabelle
  und Abnahme-Werte im Abschnitt.
- Close-Bedingung ins Issue geschrieben: DDNS-Updater bf4.emsenhuber.net + taeglicher
  GC-Lauf konform. Der Mesh-NAT-Teil ist erledigt.
- docs/handoff-20260928-core-wghub-ping-fix.md GELOESCHT: er behauptete, die fehlende
  Interface-IP von wg-hub sei ein Defekt, und schlug preferred-source vor. Beides
  widerlegt — die fehlende IF-IP ist beabsichtigt (im Archiv dokumentiert), und der
  Core-eigene Ping ist kein Betriebsproblem. Die Erkenntnis steht jetzt in TP10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 16:02:11 +02:00
1fddf5d798 TP10: Mesh-WARP-Quelle fuer FM/DW-Spokes auf BF-IP NATen
Spoke-FritzBoxen akzeptieren am WG-Peer nur Quell-IPs aus 10.40.0.0/16; WARP-Clients kommen ueber cf-mesh mit 100.96.0.0/12 und wurden verworfen. src-nat auf wg-hub-Egress legt die Quelle in den AllowedIPs-Raum.

Template (Soll) + idempotenter Job tp10-mesh-nat (manual Plan, nie full-intended). Compliance 145/145, end-to-end verifiziert (Mac/WARP -> 10.95.10.1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-03 15:56:44 +02:00
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
14 changed files with 2077 additions and 1225 deletions

439
CLAUDE.md
View File

@@ -1,62 +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 (Gitea #71) — Schicht C (DNS/FQDN) ist durch (2026-09-22, Commit `6d1d56e`), vorgezogen.**
Die internen `*.bf.emsenhuber.net` lösen wieder auf (der Core hält jetzt **27** Statics statt 10),
`ds-bf` ist öffentlich heil (läuft über Caddy), AGH macht **nur noch Filtering** (Upstream = Core).
Der *sichtbare* Schaden ist damit weg — der Ursachen-Kontext steht unter `### TP45` (AGH wertete
Rewrites im Filter-Pfad aus → unfiltrierte Clients fielen auf den Core zurück).
**Offen sind die IP-Schichten, in dieser Reihenfolge:** **A** VLAN20 ins TP32-Schema (Nautobot
`.30–.34` → `.154–.158` zuerst, dann AGH `.36/.37` → `.30/.31` — ⚠️ AGH ist **doppelt adressiert**,
beide Adressen live) → **B** VLAN30-Pool-Grenze (braucht den DHCP-Reservierungs-Renderer) →
**E** nas/pve/macmini umziehen (eigene Session) → **C4** (TP34: FQDN als Daten).
**`### TP46`** = CF-/Tunnel-SSoT + Recovery (eigene Baustelle).
1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Für den **Detail/Kontext** eines TP dann den `### TPnn`-Prosa-Abschnitt in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`** lesen. *(Die frühere „Quick-Index-Block, 1 Read reicht"-Zeile ist überholt — Index/Status stehen nicht mehr in der Datei.)*
1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Detail/Kontext eines TP → `### TPnn` in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`**.
2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat.
3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**.
4. ✅ **TP16.3 §5 durch (2026-09-01):** [`docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md`](docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md)
— alle vier oeffentlichen Namen laufen ueber `bf-public`/Caddy; Alttunnel bleiben als Rueckfallebene
(§6 offen). Enthaelt den 10-Sekunden-DNS-Fund und drei Messfallen.
5. ✅ **WLAN/Fund E — GESCHLOSSEN 2026-09-02: 7.24.1 war die Ursache, nicht `ft-over-ds`:**
[`docs/handoff-20260902-tp25-fund-e-messung.md`](docs/handoff-20260902-tp25-fund-e-messung.md)
— die Abschlussmessung ergab am 09-02 nur **4 `reauthenticating` in 8,6 h** (alle iPhone `F2:62`,
isolierte Einzel, 0 Ausfaelle), Verlauf **218 → 40 → 4**; Vorbedingung geprueft (Core-Uptime 1d18h,
`caps joined=0`). Hebel 1 (`ft-over-ds=no`) war widerlegt (6,4 h allein → 124× + 2 Ausfaelle) und ist
am 09-01 zurueckgebaut; Hebel 2–4 entfallen, **kein Geraete-Write** (Soll stand: `ft=yes ft-over-ds=yes`,
**Compliance 140/140**). **Am 2026-09-04 hat Christoph `ft=no` auf `sec-main` als Dauerzustand
entschieden** (Hebel 2 doch gezogen: Template + bf-golden-config + Nautobot-Sync, `wifi-security` gruen).
⚠️ **Rueckgenommen am 2026-09-14 (Terrasse-In-situ-Session): `ft=yes ft-over-ds=yes` zurueck** — mit
`ft=no` war jeder Roam eine Voll-Reassoziation, darin starben 2 Anrufe; Rueckkehrstand = Fund-E-Known-Good
von 09-02 (ruhig auf 7.24.1). Weg: Template + `push-gc-template.py` + `gc-configplan.py
--refresh-compliance` + `gc-manual-plan.py --deploy` (Plan `b1135f3e`, Completed) → **140/140, 0 Drifts**;
CAP-Abnahme: **keine** Inline-Overrides auf ap1/ap2/ap3. Commands:
`docs/transaktionen/TP25-ft-yes-secmain.commands.rsc`, s. Handoff 2026-09-14.
🔴 **Nachtrag 09-04: `ft=no` war NICHT uniform.** ap3 trug einen Werks-Inline `security.ft=yes` auf
`wifi1/wifi2` (Bring-up unset `!ft-over-ds`, aber nicht `!ft`; Abnahme zählt nur benannte
security-Objekte → Inline-Wert unsichtbar). Gemischtes FT-ESS → Strong-Signal-Abbrüche (−43/−45 dBm)
**nur auf ap3** bei 3/3 Strichen. Fix: `!security.ft` auf ap3 (uniform `ft=no`), Quelle
`tp28-ap3-phase2-cap.rsc` korrigiert. **Merkregel: nach jeder `sec-main`-FT-Änderung jeden CAP einzeln
`get wifiN security.ft` prüfen — CAP-lokale Inline-Overrides sind compliance-unsichtbar.**
Der TP25-Kanalplan ist am 09-04 committed (DFS-Frequenzlisten + ap1-Provisioning, **140/140, 0 Drifts**).
Offen in TP25 bleibt nur **ap2 unterperformt** (2 m/−80 dBm). Vorstufen:
[Befund](docs/handoff-20260831-tp27-upgrade-und-hebel1-befund.md) ·
[Eingriff](docs/handoff-20260831-fund-e-hebel1-gezogen.md) ·
[Rueckbau](docs/handoff-20260901-fund-e-hebel1-zurueckgebaut.md).
6. ap3-Bring-up 2026-08-30: [`docs/handoff-20260830-ap3.md`](docs/handoff-20260830-ap3.md) — sechs Funde zu Werks-MikroTiks, CAPsMAN und Golden-Config-Lücken.
7. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift.
3. Bei Live-Writes/Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/gotchas.md`** (ausführliche Diagnose-Fallen), **`docs/teilprojekte-archiv.md`**.
**Aktueller Fokus:** TP45 (IP-Schichten A/B/E, C4→**TP34.2**), TP34.2 (interne DNS-Namen als Daten, Gitea #73) und TP46 (CF-/Tunnel-SSoT) offen — Status im Gitea-Issue, Detail in den `### TPnn`-Abschnitten von `teilprojekte.md`.
## SSoT & Branch-Konvention (TP36-Cutover 2026-09-07)
- **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`),
@@ -67,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)
@@ -89,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`).
---
@@ -112,264 +84,88 @@ 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. Systematisch erfasst in **[TP48](docs/teilprojekte.md#tp48)** (Gitea #75),
der auch prüft, ob Ansible Nautobot als SSoT ablösen kann. ⚠️ **Nicht Komodo** — es verwaltet Container,
nicht RouterOS-Config (frühere Notiz hier war eine Kategorie-Verwechslung).
---
## 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.
---
@@ -381,43 +177,22 @@ 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.
- **Projekt-Memories sind seit 2026-10-06 versioniert** — dasselbe Muster wie die Skills, aus demselben Befund (reiner Home-Ordner-State). Master = Gitea **`admin/claude-memories`** (privat), Arbeitsbaum **`~/dev/claude-memories/`**. Ein Ordner je Projekt, Name = Projektpfad ohne das Präfix `-Users-christophemsenhuber-`; **führender Unterstrich = projektübergreifend** (`_dev` = `~/dev`, `_home` = `~`). Anbindung per **Symlink**, nicht Kopie: `~/.claude/projects/<kodiertes-projekt>/memory/` → `~/dev/claude-memories/<projekt>/` (eine zweite Kopie wäre genau die Drift, die das Repo verhindern soll). **Ablauf: editieren → `MEMORY.md`-Zeile ergänzen → committen → pushen.**
- 🔴 **`name:` muss dem Dateinamen entsprechen** — die `[[wikilinks]]` adressieren den Slug, nicht den Pfad. Laufen beide auseinander, brechen Verweise **still** (kein Fehler, kein Hinweis). Belegt am 2026-10-06: 12 Links zeigten ins Leere, weil 8 Dateien einen abweichenden `name:` trugen.
- 🔴 **`MEMORY.md` ist ein Index, kein Log** — nur das Index-File wird in jede Session des Projekts geladen, die Einzeldateien nicht. Statusberichte/Chronologie gehören in eine eigene, nicht geladene Datei (Muster: `bf-network-r6/CHRONIK.md`). 67 Log-Zeilen hatten den r6-Index auf 16,4 KB getrieben — mehr als alle anderen elf Indizes zusammen.
- Keine Secrets in Memories — sie werden in jede Session geladen und liegen versioniert. Platzhalter 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:**
@@ -426,4 +201,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

@@ -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,63 @@ zeigt die Einträge, die seit dem letzten `/news`-Lauf dazugekommen sind (Rest a
---
## 2026-10-12 — Airfryer „airschreier" ist FUSION/Cloud-only — lokale Anbindung endgültig tot
**Betrifft:** TP5.1 (Airfryer-Fall, DNS-Redirect + Live-Torch), die UNTRUSTED-Einordnung des Geräts
(`10.40.30.180`, VLAN30) und jede Session, die den Airfryer als „Low-Trust-Gerät mit lokalem OTA-Problem"
behandelt.
**Aktion:** Die Begründung für den DHCP-Override `dns 1.1.1.1` auf **dieser** Lease („OTA geht nicht über
den DoH-Gateway-Pfad") ist **erledigt** — das Gerät hat die FUSION-Migration bereits vollzogen und hängt
sowieso an der Cloud. **Der Override-Mechanismus selbst bleibt bestehen** und ist für künftige Low-Trust-
Kandidaten weiter definiert (Entscheidung 2026-10-12) — er wird **nicht** abgebaut, sondern behält seinen
Wert als Werkzeug, falls wieder ein Gerät mit OTA-DoH-Latenzproblemen auftaucht. Das gilt ausdrücklich
auch für die in TP5.1 skizzierte DNS-Redirect-Variante (a).
**Aber:** Bei FUSION ist die **Cloud-Verbindung die einzige** Steuerungsroute — das Gerät darf **nicht**
ins internetlose UNTRUSTED-VLAN (40) wandern, sonst ist es komplett unsteuerbar (auch aus HA). Der
frühere UNTRUSTED-Move-Kandidat aus TP5.1 ist damit **zurückzustellen**, nicht umzusetzen.
**Steuerungsentscheidung (2026-10-12):** Die Cloud-Integration **bleibt** — der Airfryer wird bewusst
über `philips_homeid` / Cloud-Relay betrieben. Kein Rückbau, keine weitere Diskussion nötig.
**Befund (aus HA verifiziert, 2026-10-12):**
- Config-Entry `philips_homeid`, Titel **„Airschreier 5000 Series XXL (Cloud)"** — das `(Cloud)` im
Titel ist die FUSION-Markierung der Integration (Cloud-Relay statt lokal).
- Firmware-Sensoren: **NCP `1.1.14` / Host `0.5.6`** → entspricht der dokumentierten FUSION-Liste
(`1.1.14/0.5.6`; vgl. HD9285-Fälle `1.6.2/0.6.8`, `1.1.8/0.5.6`). Achtung: Die HomeID-App zeigt die
Reihenfolge **umgedreht** an (`0.5.6/1.1.14`) — nicht als SPECTRE fehldeuten.
- Modellnummer-Sensor: `HD9285` (SPECTRE wäre die lokale Variante, ist es aber nicht mehr).
- Lokaler Port 80/443 antwortet **nicht** (aus MAIN getestet); mDNS announce't aber weiter
`PHILIPS_HD9285_2_1FB281._http._tcp.local.` → Port 80. Das Gerät läuft also, hat aber keine
lokalen Credentials mehr.
- **Kein Weg zurück:** Laut Integrations-Doku ist die FUSION-Migration einwegig („There is no way back"),
weil die Zuordnung cloud-seitig über `externalDeviceId` verankert ist. Firmware-Downgrade hilft nicht,
Custom-Firmware existiert nicht (geschlossener ESP-Stack, signierter Bootloader).
- Steuerung **funktioniert** über die Cloud (Live-Werte plausibel: Kochstatus `setting`, 180 °C,
`fresh_fries`; Buttons für Start/Pause/Stopp/Warmhalten vorhanden).
- Nebenbefund: `dns 1.1.1.1` = anonymer Consumer-Resolver → **DNS-Sicht weiter blind**, Gerät umgeht
die Filterung. Redet nachweislich mit Versuni → AWS `eu-west-1` (IoT). Bleibt ein Beobachtungspunkt,
aber ohne OTA-Rechtfertigung.
---
## 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

View File

@@ -303,6 +303,30 @@ Tokens: nur im Container-Env + jederzeit via `GET /accounts/…/cfd_tunnel/<id>/
**🔴 NEUER FUND (wichtiger als vw!) — Wildcard `*.emsenhuber.net` (`16aaec78`) zu offen:** Policy `christoph` hat `include: email:christoph OR login_method:github OR login_method:google`. **`login_method:<idp>` als include matcht JEDEN, der sich via GitHub/Google einloggt** — nicht nur christoph. D.h. **jeder GitHub-/Google-User erreicht nach Login alle internen Dienste** (hass, grafana, portainer, pve, gitea, …). Fix = die zwei `login_method`-Includes raus (nur `email:christoph` + ggf. weitere erlaubte Mails + `non_identity bf4` + `claude-mcp`). **NICHT einseitig geändert** (ändert Login-Verhalten überall) → User-Entscheidung. War im Memory schon als „Policy christoph verschlanken" geflaggt.
> ✅ **BESTÄTIGT und BEHOBEN am 2026-10-06 — der Fund oben war RICHTIG.**
> Christoph hat mit einem **zweiten GitHub-Account** (fremde Adresse) getestet: **er kam durch.**
> `login_method:github` im **`include`** ist ein **OR** — die danebenstehende `email`-Zeile war
> eine **Alternative**, keine Bedingung. Jeder GitHub-Account kam hinein.
>
> **Fix (ausgeführt):** `login_method:github` von `include` nach **`require`** verschoben
> ⇒ **`email AND github`**. Zurückgelesen über beide Endpunkte, Fix bestätigt.
> ⚠️ Nebenwirkung: der E-Mail-Einmalcode allein reicht auf GitHub-Seite **nicht mehr**.
>
> 🔴 **Zwischenzeitliche Fehlspur (dokumentiert, weil lehrreich):** eine frühere Fassung dieser
> Notiz erklärte den Fund am selben Tag für **widerlegt** — auf Basis eines **Google**-Tests
> (`testus@emsenhuber.com` → abgelehnt) plus eines **Schlusses** auf GitHub. Der Schluss war
> falsch: der Google-Test zeigt nur Googles Verhalten. Die IdP-Configs sind konfigurationsgleich
> (beide nur `client_id` + `redirect_url`) — **das unterscheidende Feld steht nicht in der Config.**
> **Lehre: pro IdP einzeln messen, nicht den ersten Test auf den zweiten übertragen.**
>
> **Bleibt gültig:** TP41 hat den internen Wildcard auf Access-Groups (`zt-humans`/`zt-home`/
> `zt-automation`) umgestellt — dort steht heute kein `login_method` mehr. Der Sweep über alle
> Apps fand `login_method` nur noch in `Terpenfinder v3` (`christoph`: github+onetimepin;
> `geli`: nur onetimepin = harmlos).
>
> Details, Gotchas (`12130` reusable-Policy-Endpunkt, MCP read-only) und die Grenze
> „kein GitHub-Username-Selector" → `docs/teilprojekte.md` → `### TP49`.
**⏸️ `hass` 400 / `ha-mcp` — zwei getrennte Layer:**
- **Access-Layer (remote):** HA-Companion-App kann kein interaktives Access-Login → Fix = iPhone in **WARP** enrollen (`Warp Login App` `6c32fe23` `allow:christoph` existiert → christoph darf enrollen). Client-seitig, braucht User am Gerät. Vom Heim-Netz (bf4-IP) ohnehin ohne Login erreichbar.
- **Origin-Layer:** `hass.emsenhuber.net` gibt **vom Heim-Netz** (Access passt via bf4-IP) trotzdem **400** (CF `server:cloudflare`, aber Origin-verursacht) = **HA lehnt den Request ab** — HA-`http:`-Config (`use_x_forwarded_for` + `trusted_proxies` inkl. cloudflared-Range, ggf. Host-Allowlist). **Separates HA-Thema, NICHT Tunnel/Access** — offen.

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.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
# Startauftrag — TP49: CF-Access `login_method`-Include ✅ ERLEDIGT (2026-10-06)
> **Abgeschlossen.** Gitea #76, Prosa in `docs/teilprojekte.md` → `### TP49`.
>
> 🔴 **Diese Datei nicht mehr als Auftrag lesen — der ist ausgeführt.** Sie bleibt als Nachweis
> stehen: Lage, Vorgehen und die Fehlschläge auf dem Weg dorthin.
## Der Auftrag (erledigt)
`login_method:github` aus dem `include` der Policy `christoph` der CF-Access-App
`420.teilzeitladen.xyz` entfernen — nach Rücksprache, wen der Zugang treffen soll.
## Ausgeführt (2026-10-06)
`github` von `include` nach **`require`** verschoben ⇒ **`email AND github`**.
| | vorher | jetzt |
|---|---|---|
| `include` | email · **github** · onetimepin | email · onetimepin |
| `require` | — | **github** |
Gesetzt über **`PUT /accounts/{id}/access/policies/{policy_id}`**, zurückgelesen über **beide**
Endpunkte (App-Pfad + reusable-Pfad) — identisch. Der `success:true` der API wurde **nicht**
geglaubt, sondern die Policy neu gelesen.
**App-ID** `e6a7991f-466c-4d1d-bac6-4b30fafccce4` · **Policy (reusable)** `8e44f5d1-8257-443e-a797-b3de939952ee`
**github-IdP** `72c1291b…` · **onetimepin** `9024c7a6…`
**Sweep über alle Apps** (`login_method` in include/require/exclude): genau zwei Treffer, beide in
`Terpenfinder v3` — `christoph` (github+onetimepin) und `geli` (nur onetimepin, harmlos).
Interne Apps/Wildcard: sauber (TP41).
## 🔴 Gotchas — live bestätigt, alle drei neu am 2026-10-06
1. **Reusable Policies ändern geht NICHT über den App-Pfad.**
`PUT …/access/apps/{app}/policies/{pol}` → **`12130 can not update reusable policies through
this endpoint`**. Korrekt: **`PUT …/access/policies/{policy_id}`**.
⚠️ Der ältere Merksatz „Reusable Policy nur per App-PUT detachbar" meint das **Detach** vom
App-`policies`-Array — **nicht** das Ändern des Inhalts. Zwei verschiedene Operationen.
2. **Der `cloudflare`-MCP ist strikt read-only.** Write-Probe: no-op-PUT (byte-identischer Body)
→ **`1010: undefined`**. Writes nur via `CF_API_TOKEN`/curl
(`cd mcp/cloudflare && set -a; . ./.env; set +a`). Token **nie** ausgeben.
3. **Access-App-IDs sind 32 Hex** — Kurzform `16aaec78` → `12045 invalid_application_id`.
## ⚠️ Was `login_method` nicht kann
Die ursprüngliche Absicht war eine Kopplung **an den GitHub-Username**. Das gibt Access nicht her:
`login_method` prüft die **Methode**, nicht den Nutzer; einen Username-Selector gibt es nicht.
Der einzige GitHub-bezogene Identitäts-Selector ist **„Identity provider group"** — der braucht
**SCIM**, und am hiesigen GitHub-IdP ist `scim_config.enabled = false`
(`GET …/scim/groups` → `500 (12158)`). **Weg zu.**
⇒ `email AND github` ist die vollständige Umsetzung des hier Möglichen, keine Notlösung.
Eine echte Org-Kopplung bräuchte SCIM aktiviert — **eigener TP**.
## Aktenhistorie — der Umweg (nicht löschen)
Zwischenzeitlich wurde TP49 als **Phantom geschlossen** — auf Basis eines **Google**-Tests plus
eines **Schlusses** auf GitHub. Beides falsch:
1. **Lese-Schluss als Befund verkauft.** „`include` ist ein OR" + „Google bindet die Adresse"
⇒ „GitHub bindet auch". Nie gemessen, aber als Tatsache in Akte und Issue geschrieben.
2. **Der Gegenbeweis war keiner.** Der Google-Test zeigt nur Googles Verhalten. Die IdP-Configs
sind konfigurationsgleich — das unterscheidende Feld steht **nicht** in der Config.
Christoph hat mit einem **zweiten GitHub-Account** getestet: er kam durch. Damit war der
Ursprungsfund bestätigt und der Fix nötig.
**Lehre: pro IdP einzeln messen, nicht den ersten Test auf den zweiten übertragen.**
## Verifikation
- [x] Policy über beide Endpunkte zurückgelesen — `require` trägt github, `include` nicht mehr
- [x] Sweep: keine weiteren `login_method` in `include` außer den bekannten
- [x] Scope: reusable Policy nur an diese eine App gebunden (kein Kollateral)
- [x] **Verhaltensabnahme (2026-10-06, echter Login):** fremder GitHub-Account → **abgewiesen**;
eigener Zugang + Geli → funktionieren; alle übrigen Auth-Pfade wie erwartet
**Status: abgeschlossen.** Gitea #76 `closed` / `status:done`. Kein offener Punkt.

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

@@ -926,6 +926,34 @@ def job_tp45_c_dns():
return [(["crs326-core"], "\n".join(lines), "TP45-c-dns")]
def job_tp10_mesh_nat():
"""TP10 (2026-10-03): Mesh-WARP-Verkehr Richtung FM/DW-Spokes auf eine BF-IP NATen.
WARUM: die Spoke-FritzBoxen akzeptieren am WG-Peer nur Quell-IPs aus 10.40.0.0/16
(AllowedIPs). WARP-Clients kommen ueber cf-mesh mit 100.96.0.0/12 -> die FritzBox
verwirft den Verkehr (bewiesen: ping src=100.96.0.6 -> 100% Loss, src=10.40.20.1
-> 0%). Eine src-nat auf dem wg-hub-Egress legt die Quelle in den AllowedIPs-Raum.
`action=src-nat` (NICHT masquerade): wg-hub hat keine Interface-IP, masquerade
haette keine Quelle. src-nat auf eine feste BF-IP (10.40.10.1) haelt zudem das
Connection-Tracking -- die Antwort an 10.40.10.1 wird im prerouting auf die
Mesh-IP zurueckuebersetzt und via cf-mesh geroutet.
KEIN LOCKOUT-RISIKO: reine srcnat-Ergaenzung; der no-NAT-accept (dst 10.40.0.0/16)
und der WAN-masquerade bleiben unberuehrt, die Regel matcht nur out-interface=wg-hub.
"""
cmt = "TP10: NAT mesh source to WG spokes (AllowedIPs)"
find = f'find where comment="{cmt}"'
add = (f'/ip firewall nat add action=src-nat chain=srcnat comment="{cmt}" '
f'out-interface=wg-hub src-address=100.96.0.0/12 to-addresses=10.40.10.1')
lines = [
f':if ([:len [/ip firewall nat {find}]] = 0) do={{ {add} }} '
f'else={{ :log info "tp10-mesh-nat: Regel existiert schon" }}',
f':log info ("tp10-mesh-nat: regeln=" . [:tostr [:len [/ip firewall nat {find}]]])',
]
return [(["crs326-core"], "\n".join(lines), "TP10-mesh-nat")]
JOBS = {
"drift-cleanup": job_drift_cleanup,
"mgmt-acl": job_mgmt_acl,
@@ -938,6 +966,7 @@ JOBS = {
"tp45-v30-leases": job_tp45_v30_leases,
"tp45-v30-revert": job_tp45_v30_revert,
"tp45-v30-restore": job_tp45_v30_restore,
"tp10-mesh-nat": job_tp10_mesh_nat,
}

View File

@@ -359,6 +359,7 @@
# [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
/ip firewall nat add action=masquerade chain=srcnat comment="defconf: masquerade" ipsec-policy=out,none out-interface-list=WAN
/ip firewall nat add action=src-nat chain=srcnat comment="TP10: NAT mesh source to WG spokes (AllowedIPs)" out-interface=wg-hub src-address=100.96.0.0/12 to-addresses=10.40.10.1
# [7b] FIREWALL FILTER (TP26, 2026-08-17). Kommt als DATEN aus
# local_config_context_data.firewall_filter, nicht als Templatezeilen —
@@ -396,6 +397,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

@@ -0,0 +1,112 @@
# =============================================================================
# TP10 — Cloudflare-DDNS-Updater fuer bf4.emsenhuber.net
# =============================================================================
# Zweck: der WG-Hub-Endpoint der Spokes (fm/dw) ist bf4.emsenhuber.net. Der
# A-Record muss der Liwest-Public-IP des Core folgen. Heute manuell gepflegt.
#
# BASIS: bajodel/mikrotik-cloudflare-dns (MIT, Copyright (c) 2025 bajodel)
# https://github.com/bajodel/mikrotik-cloudflare-dns
# Aenderungen ggue. dem Original (Christoph-Entscheid 2026-10-03):
# 1. IP kommt aus der DHCP-Lease (client1/vlan100), NICHT von Echo-Diensten.
# Liwest ist die Quelle, kein Dritter; kein Cloudflare im Pfad.
# 2. check-certificate=yes (Original-Default war false -> Token ungeprueft).
# 3. Nur EIN Record (bf4), Konfiguration auf das Noetige reduziert.
#
# Token: liegt NICHT hier, sondern als globale Script-Variable `cfDdnsToken`.
# Einmalig setzen per SSH (siehe docs/teilprojekte.md ### TP10).
#
# Installation: /import file=tp10-ddns-cf-updater.rsc
# danach /system scheduler add ... (siehe unten)
# =============================================================================
# ---- Konfiguration ----------------------------------------------------------
:local DnsRcName "bf4.emsenhuber.net"
:local DnsZoneID "1b5bc02fdf61b6b989afde543f3f4134" # CF_ZONE_ID aus mcp/cloudflare/.env
:local DnsRcrdID "dba9d9ec3b261fdb314c0f83e4e62f0d"
:local RecordTTL "120" # wie der bestehende Record
:local RecordProxied "false" # WG-Endpoint: NICHT proxied
:local VerboseLog false
:local WanDhcpName "client1"
# -----------------------------------------------------------------------------
# Token aus /system script environment holen (NIE im Skriptkopf, NIE per :global).
# Grund (2026-10-03): :global cfDdnsToken liest im Skript-Kontext "nothing", obwohl
# /system script environment cfDdnsToken gesetzt ist. CLI-Scope und Skript-Scope sind
# auf RouterOS 7.24 unterschiedlich. `/system script environment get` ist scope-unabhaengig.
:do {
:local tk ""
:local found [/system script environment find name=cfDdnsToken]
:if ([:len $found] > 0) do={
:set tk [/system script environment get [find name=cfDdnsToken] value]
}
:if ([:typeof $tk] = "") do={
:log error "[tp10-ddns] cfDdnsToken nicht gesetzt - Abbruch"
:error "cfDdnsToken fehlt"
}
:global cfDdnsToken
:set cfDdnsToken $tk
} on-error={
:log error "[tp10-ddns] cfDdnsToken nicht lesbar - Abbruch"
:error "cfDdnsToken fehlt"
}
# ---- WAN-IPv4 aus der DHCP-Lease (Quelle: Liwest, kein Dritter) ------------
:local CFWanIP4New ""
:do {
:local leaseAddr [/ip dhcp-client get [find name=$WanDhcpName] address]
:if ([:typeof $leaseAddr] != "nothing") do={
# "90.146.33.177/24" -> "90.146.33.177"
:local slashPos [:find $leaseAddr "/"]
:if ($slashPos >= 0) do={
:set CFWanIP4New [:pick $leaseAddr 0 $slashPos]
} else={
:set CFWanIP4New $leaseAddr
}
}
} on-error={
:log error "[tp10-ddns] DHCP-Lease nicht lesbar ($WanDhcpName)"
}
:if ($CFWanIP4New = "") do={
:log warning "[tp10-ddns] Keine WAN-IP aus der Lease, naechster Lauf"
} else={
# ---- Guard: nur oeffentliche IPv4 zulassen --------------------------------
# Faengt den Fall ab, dass DHCP Muell liefert (privat/CGNAT) -> kein Write.
:local isPrivate false
:if ([:pick $CFWanIP4New 0 3] = "10.") do={ :set isPrivate true }
:if ([:pick $CFWanIP4New 0 8] = "192.168.") do={ :set isPrivate true }
:if ([:pick $CFWanIP4New 0 7] = "172.16.") do={ :set isPrivate true }
:if ([:pick $CFWanIP4New 0 8] = "100.64.0") do={ :set isPrivate true } # CGNAT 100.64/10
:if ($isPrivate) do={
:log error "[tp10-ddns] Lease liefert nicht-oeffentliche IP ($CFWanIP4New) - ABBRUCH, kein Write"
} else={
# ---- Nur bei Abweichung schreiben (Idempotenz) --------------------------
:global CFWanIP4Cur
:if ($CFWanIP4New != $CFWanIP4Cur) do={
:if ($VerboseLog) do={ :log info "[tp10-ddns] WAN-IP: $CFWanIP4Cur -> $CFWanIP4New" }
# Kommentar am Record: Zeit + Geraet, wie im Original
:local ds [/system clock get date]
:local ts [/system clock get time]
:local datetime ([:pick $ds 0 4] . "." . [:pick $ds 5 7] . "." . [:pick $ds 8 10] . "-" . [:pick $ts 0 2] . ":" . [:pick $ts 3 5])
:local CFComment ("$datetime by crs326-core")
:local url "https://api.cloudflare.com/client/v4/zones/$DnsZoneID/dns_records/$DnsRcrdID/"
:do {
:local res [/tool fetch http-method=put mode=https url=$url check-certificate=yes output=user as-value http-header-field="Authorization: Bearer $cfDdnsToken,Content-Type: application/json" http-data="{\"type\":\"A\",\"name\":\"$DnsRcName\",\"content\":\"$CFWanIP4New\",\"ttl\":$RecordTTL,\"proxied\":$RecordProxied,\"comment\":\"$CFComment\"}"]
:if (($res->"status") = "finished") do={
:log warning "[tp10-ddns] $DnsRcName -> $CFWanIP4New aktualisiert"
:set CFWanIP4Cur $CFWanIP4New
} else={
:log error "[tp10-ddns] Update fehlgeschlagen (status != finished)"
}
} on-error={
:log error "[tp10-ddns] Exception beim Cloudflare-Update - naechster Lauf"
}
} else={
:if ($VerboseLog) do={ :log info "[tp10-ddns] WAN-IP unveraendert ($CFWanIP4New)" }
}
}
}