From 5ad71b37c010d331287d3ca1d1e1681c9bff955a Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Sat, 3 Oct 2026 17:17:33 +0200 Subject: [PATCH] =?UTF-8?q?TP10:=20Mesh-DDNS-Updater=20gebaut,=20Handoff?= =?UTF-8?q?=20f=C3=BCr=20Finalisierung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...andoff-20261003-tp10-ddns-finalisierung.md | 103 ++++++++++++++++++ routeros/tp10-ddns-cf-updater.rsc | 98 +++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 docs/handoff-20261003-tp10-ddns-finalisierung.md create mode 100644 routeros/tp10-ddns-cf-updater.rsc diff --git a/docs/handoff-20261003-tp10-ddns-finalisierung.md b/docs/handoff-20261003-tp10-ddns-finalisierung.md new file mode 100644 index 0000000..70757e0 --- /dev/null +++ b/docs/handoff-20261003-tp10-ddns-finalisierung.md @@ -0,0 +1,103 @@ +# Handoff 2026-10-03 — TP10: Mesh-DDNS für bf4.emsenhuber.net — Finalisierung + +## Kontext / Auslöser +TP10 (Multi-Site VPN) war im Archiv als „KERN DURCH" mit offenem DDNS-Updater für `bf4.emsenhuber.net`. +Der A-Record wurde manuell gepflegt; bei Liwest-IP-Wechsel würden beide WG-Tunnel (fm/dw) sterben. + +Neuer Befund 2026-10-03: CF_DNS_API_TOKEN lag bereits in `~/.secrets/credentials.env` — DNS-Schreiben +funktioniert, Token kann *keine* Tokens anlegen (eng gescoped: `Zone → DNS:Edit` nur `emsenhuber.net`). + +## Stand (alles live auf crs326-core) + +| Objekt | Status | +|---|---| +| **Skript** `tp10-ddns-bf4` | angelegt, `invalid=false` — liest WAN-IP aus DHCP-Lease `client1` (`vlan100`), Public-IPv4-Guard, `check-certificate=yes`, Idempotenz über globale Variable `CFWanIP4Cur` | +| **Globale Variable** `cfDdnsToken` | auf Core gesetzt (`len=53`), persistiert in `/system script environment` | +| **CF_DNS_API_TOKEN** | liegt in `~/.secrets/credentials.env` → `CF_DNS_API_TOKEN` — Scope: `Zone → DNS:Edit` nur `emsenhuber.net` (Record `bf4`, ID `dba9d9ec3b261fdb314c0f83e4e62f0d`) | +| **Handlauf 1 (Idempotenz-Beweis)** | **PENDING** — Skript-Variable `cfDdnsToken` ist im Skript-Kontext nicht sichtbar (`:global` im Skript vs. `/system script environment`) | + +## Offener Kernpunkt: Global-Variable im Skript nicht sichtbar + +- `/system script environment print` → `cfDdnsToken` vorhanden (`count=1`, `len=53`) +- CLI-Test: `:global cfDdnsToken; :put [:len $cfDdnsToken]` → `53` ✅ +- **ABER** im Skript (`:global cfDdnsToken; :if ([:typeof $cfDdnsToken] = "nothing")`) → `nothing` +- **Ursache:** RouterOS-Global-Scope-Differenz zwischen CLI-Interaktion und Skript-Ausführung +- **Workaround noch nicht angewendet** (Probe-Skript `tp10-ddns-probe` angelegt, `:log`-Ausgabe nicht bestätigt) + +## Abhängigkeiten (NEU: envs auf Geräten) + +| Gerät | Variable | Ort | Rotation bei Änderung | +|---|---|---|---| +| **crs326-core** | `cfDdnsToken` (53 Zeichen, `CF_DNS_API_TOKEN`) | `/system script environment` — **nicht** im Skripttext | Token-Wechsel: Wert in `/system script environment` aktualisieren, Skript unverändert | + +**Wichtig:** Das Token liegt **nicht** im Git, nicht im Skript, nicht in Nautobot — nur in: +1. `~/.secrets/credentials.env` → `CF_DNS_API_TOKEN` (SSoT) +2. `crs326-core` → `/system script environment cfDdnsToken` (Live) +3. `mcp/cloudflare/.env` → `CF_DNS_API_TOKEN` (MCP-Bereitstellung) + +Bei Token-Rotation: alle drei Orte synchron ändern. + +## Was noch fehlt (Restmetern) + +1. **Global-Scope-Fix** — Skript muss die Variable lesen können. + - Option A: Skript per `run_command` statt als benanntes Skript ausführen (CLI-Scope) + - Option B: Variable per `:global cfDdnsToken` *im Skript* **vor** der Prüfung setzen (Dummy, dann überschreiben) + - Option C: Skript via `run_command` als Inline-Befehl ausführen (CLI-Scope) + +2. **Handlauf 1 (Idempotenz):** Skript einmal laufen lassen → `CFWanIP4Cur` wird gesetzt → Log `⚠ aktualisiert` +3. **Handlauf 2 (Idempotenz-Beweis):** Skript nochmal → Log `⚠ unverändert` +4. **Scheduler anlegen** — DHCP-Client-Trigger `client1` + stündlicher Scheduler als Netz +5. **Positiv-Test** — A-Record kurz auf falsche IP setzen, Scheduler-Lauf abwarten, `dig bf4.emsenhuber.net` prüfen +6. **TP10 #49 schließen** — Issue: beide Close-Bedingungen abhaken (DDNS-Updater steht, GC-konform), Label `status:done` +7. **Handoff löschen** — diese Datei gehört ins Archiv, kein Handoff mehr nötig + +## Nächste Schritte (für die nächste Session) + +```bash +# 1. Probe: CLI-Scope Test (läuft das Skript inline?) +/tool fetch ... (wie im Skript, aber inline auf CLI) + +# 2. Falls Global-Scope im Skript unrettbar: +# - Skript als Scheduler-Aufruf inline bauen (DHCP-Client script=tp10-ddns-bf4) +# - ODER: Skript die Variable selbst setzen lassen (Dummy → überschreiben) + +# 3. Handlauf 1+2, Scheduler, Positiv-Test, TP10 schließen +``` + +## Wichtige Dateien + +| Datei | Zweck | +|---|---| +| `routeros/tp10-ddns-cf-updater.rsc` | Skript-Quelle (MIT, Copyright bajodel 2025) | +| `routeros/tp10-ddns-cf-updater.rsc` (lokal) | importiert als `tp10-ddns-bf4` | +| `docs/teilprojekte.md` → `### TP10` | Prosa mit Befund, Close-Bedingung | +| `docs/teilprojekte-archiv.md` → `### TP10` | Zeiger: `♻️ REOPENED 2026-10-03` | +| `~/.secrets/credentials.env` | `CF_DNS_API_TOKEN` (SSoT) | +| `mcp/cloudflare/.env` | `CF_DNS_API_TOKEN` (MCP) | + +## Sicherheit / Governance + +- Token `CF_DNS_API_TOKEN` ist **eng gescoped**: `Zone → DNS:Edit` nur `emsenhuber.net`, Record `bf4` +- **Kein** `Account → Account API Tokens:Edit` — Token kann keine neuen Tokens anlegen +- Token liegt **nicht** im Git, nicht im Skript, nicht in Nautobot +- Rotation: drei Orte (`credentials.env`, Core `script environment`, `mcp/cloudflare/.env`) + +## Bekannte offene Punkte + +- **Recovery-Lücke:** GC-Template rendert kein `/system script` / `/system scheduler` — Recovery von null hätte den Updater nicht. Im TP10-Issue als offener Punkt vermerkt, Entscheidung vertagt. +- **WAN-IP-Quelle:** DHCP-Lease (korrekt), **nicht** Echo-Dienste — Liwest ist Quelle, kein Cloudflare im Pfad. +- **Mesh-IP-Falle widerlegt:** Core holt via `api.ipify.org` korrekt `90.146.33.177` (WAN), nicht Mesh-IP `100.96.0.6`. + +## Nächste Session: genau das + +1. Global-Scope-Problem lösen (10 Min) +2. Handlauf 1+2 (5 Min) +3. Scheduler (DHCP-Trigger + stündlich) + Positiv-Test (15 Min) +4. TP10 #49 `status:done` + Prosa finalisieren (5 Min) +5. Handoff löschen, pushen + +**Geschätzte Restzeit: ~45 Min** + +--- + +*Erstellt 2026-10-03 — Session-Handoff für Finalisierung TP10 DDNS* diff --git a/routeros/tp10-ddns-cf-updater.rsc b/routeros/tp10-ddns-cf-updater.rsc new file mode 100644 index 0000000..583c90b --- /dev/null +++ b/routeros/tp10-ddns-cf-updater.rsc @@ -0,0 +1,98 @@ +# ============================================================================= +# 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 der globalen Variable holen (NIE im Skriptkopf) +:global cfDdnsToken +:if ([:typeof $cfDdnsToken] = "nothing") do={ + :log error "[tp10-ddns] cfDdnsToken nicht gesetzt - 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)" } + } + } +}