From 6d1d56e463682ca99fc4449374ac2e91b46d513c Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Tue, 22 Sep 2026 12:16:56 +0200 Subject: [PATCH] TP45 Schicht C: FQDN-Wiederherstellung (C1-C3) C1 - Der Core haelt jetzt 27 /ip dns static (vorher 10). Die 12 toten Namen loesen wieder auf: 9 Caddy-Frontends, tv.bf/mfc.bf, das bisher nirgends existierende ap3.bf, dazu pve.bf und pve-mcp.bf von der Host-IP auf .40 und die 5 bare Namen. Neuer GC-Job tp45-c-dns; er liest die Zeilen aus dem Template selbst und macht daraus einen idempotenten Upsert. C2 - AGH entdoppelt: upstream_dns nur noch 10.40.20.1, alle 26 Rewrites raus. Der Core ist damit die einzige Quelle. /admin ist lokal wieder erreichbar (header-frei 200, mit CF-Connecting-IP weiter 404 - der Guard bleibt zu). C3 - ds-bf-Ingress-Origin auf https://10.40.20.40:443 + SNI (CF-Write). Oeffentlich 200 in 0,24 s statt 000 nach 20 s. Abnahme: Geraete-Read (27 Statics), Compliance 145/145 / 0 Drifts, dig vom Mac fuer alle Namen, /admin lokal 200, ds-bf oeffentlich 200, Filter (doubleclick -> 0.0.0.0) intakt, vw/hass/n8n/gitea unveraendert. Offener Befund: der Core reicht AAAA weiter und liefert damit fuer die bare Namen die CF-IPv6. Heute wirkungslos (kein globales IPv6 im Netz), aber latent - bewusst dokumentiert statt geaendert. Co-Authored-By: Claude Sonnet 4.6 --- adguard/AdGuardHome.yaml | 89 +++-------------------------------- nautobot/gc-configplan.py | 76 ++++++++++++++++++++++++++++++ nautobot/mikrotik_routeros.j2 | 39 +++++++++++++-- 3 files changed, 118 insertions(+), 86 deletions(-) diff --git a/adguard/AdGuardHome.yaml b/adguard/AdGuardHome.yaml index f37c5c7..32d3c8e 100644 --- a/adguard/AdGuardHome.yaml +++ b/adguard/AdGuardHome.yaml @@ -31,10 +31,11 @@ dns: ratelimit_subnet_len_ipv6: 56 ratelimit_whitelist: [] refuse_any: true + # TP45 C2 (2026-09-22): der Core ist der EINZIGE Upstream β€” AGH macht nur noch + # Filtering. Die interne Zone und die bare Namen kommen damit aus derselben Quelle + # wie fuer jeden anderen Client. DoH nach draussen macht allein der Core. upstream_dns: - - https://px35ynlol3.cloudflare-gateway.com/dns-query - - '[/bf.emsenhuber.net/]10.40.20.1' - - '[/40.10.in-addr.arpa/]10.40.20.1' + - 10.40.20.1 upstream_dns_file: "" bootstrap_dns: - 1.1.1.1 @@ -158,85 +159,9 @@ filtering: blocking_mode: default parental_block_host: family-block.dns.adguard.com safebrowsing_block_host: standard-block.dns.adguard.com - rewrites: - - domain: pbs.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: portainer.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: pve.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: gitea-mcp.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: n8n-mcp.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: ha-mcp.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: pve-mcp.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: z2m25.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: iobroker.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: ccu.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: nautobot.bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: vw.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: hass.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: n8n.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: gitea.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: ds-bf.emsenhuber.net - answer: 10.40.20.40 - enabled: true - - domain: nas.bf.emsenhuber.net - answer: 10.40.20.10 - enabled: true - - domain: macmini.bf.emsenhuber.net - answer: 10.40.20.167 - enabled: true - - domain: mfc.bf.emsenhuber.net - answer: 10.40.10.6 - enabled: true - - domain: tv.bf.emsenhuber.net - answer: 10.40.40.11 - enabled: true - - domain: docker.bf.emsenhuber.net - answer: 10.40.20.126 - enabled: true - - domain: platform-pg.bf.emsenhuber.net - answer: 10.40.20.20 - enabled: true - - domain: core.bf.emsenhuber.net - answer: 10.40.99.1 - enabled: true - - domain: ap1.bf.emsenhuber.net - answer: 10.40.99.11 - enabled: true - - domain: ap2.bf.emsenhuber.net - answer: 10.40.99.12 - enabled: true - - domain: branch.bf.emsenhuber.net - answer: 10.40.99.2 - enabled: true + # TP45 C2 (2026-09-22): alle 26 Rewrites entfernt β€” sie waren die zweite, stille + # Definitionsquelle. Der Core haelt jeden dieser Namen als /ip dns static. + rewrites: [] safe_fs_patterns: - /opt/AdGuardHome/userfilters/* max_http_size: 256MB diff --git a/nautobot/gc-configplan.py b/nautobot/gc-configplan.py index e977075..8193c04 100644 --- a/nautobot/gc-configplan.py +++ b/nautobot/gc-configplan.py @@ -851,6 +851,81 @@ def job_tp45_v30_restore(): return [(["crs326-core"], "\n".join(lines), "TP45-v30-restore")] +def _dns_static_lines(): + """Die `/ip dns static add`-Zeilen aus dem GC-Template, wortgetreu. + + πŸ”΄ **Quelle ist das Template selbst, NICHT eine zweite Kopie.** Eine handgepflegte + Liste hier liefe beim ersten Template-Edit auseinander β€” still, weil beide Seiten + fuer sich plausibel aussehen; am Geraet faellt nur auf, dass ein Name fehlt. + Und die Kommentare muessen **zeichengleich** sein: Golden Config vergleicht + `comment=` mit, ein abweichender Text ist eine Drift. + + Der `/ip dns static`-Block ist ein Literal im Template (im Gegensatz zu + `firewall_filter` und `dhcp_leases`, die als Daten aus + `local_config_context_data` kommen) β€” deshalb ist das Parsen hier zulaessig. + """ + text = (pathlib.Path(__file__).with_name("mikrotik_routeros.j2")).read_text() + return [ln.strip() for ln in text.splitlines() + if ln.strip().startswith("/ip dns static add ")] + + +def _dns_upsert(line): + """Eine Template-Zeile -> idempotenter Upsert, EINZEILIG und klammer-vollstaendig. + + `/ip dns static add` ist NICHT idempotent β€” ein zweiter Lauf legte den Namen ein + zweites Mal an. Deshalb: nur anlegen, wenn der Name fehlt, sonst in place setzen. + Der Selektor wird wie in `_lease_move` gezaehlt (`find where …` in `[:len …]`, + `[find where …]` als Argument) β€” die vertauschte Form ist ein Syntaxfehler, der + als `Completed` durchlaeuft und das Geraet unveraendert laesst. + """ + addr = re.search(r"address=(\S+)", line).group(1) + name = re.search(r"name=(\S+)", line).group(1) + cm = re.search(r'comment=("(?:[^"\\]|\\.)*"|\S+)', line) + comment = f" comment={cm.group(1)}" if cm else "" + return (f':if ([:len [/ip dns static find where name={name}]] = 0) do={{ ' + f'{line} }} ' + f'else={{ /ip dns static set [find where name={name}] address={addr}{comment} }}') + + +def job_tp45_c_dns(): + """TP45 Schicht C (2026-09-22): die internen Namen in den CORE β€” der FQDN-Fix. + + Hintergrund: AGH wertet seine DNS-Rewrites nur im **Filter-Pfad** aus. Fuer + unfiltrierte Clients (`dns-bypass-exempt` = beide Mac-NICs, `grp-infra`, + `grp-media`, `grp-mgmt`) greifen sie nicht, und die fallen auf den conditional + forward `[/bf.emsenhuber.net/] -> 10.40.20.1` zurueck β€” den Core. Der hielt + aber nur 10 Statics; 12 Namen existierten nur als AGH-Rewrite und waren damit + **NXDOMAIN**. Vom Client gemessen 2026-09-22: alle 12 leer, `vw.emsenhuber.net` + lieferte die Cloudflare-Anycast-IP statt `.40` β€” und genau daher kam der + 404 auf `/admin` (der CF-Weg setzt `CF-Connecting-IP`, der `@cf_admin`-Guard + antwortet darauf mit 404). + + Dieser Job bringt die Statics auf das Geraet. Danach beantwortet der Core alle + internen Namen unabhaengig vom Filterstatus des Clients β€” und, weil die Namen + ueber Caddy laufen, mit gueltigem TLS. + + ⚠️ **Die 5 bare Namen (`vw`, `hass`, `n8n`, `gitea`, `ds-bf`) kommen bewusst mit + auf `.40`** (Christoph-Entscheid 2026-09-22): der lokale, header-freie Weg ist + die einzige Tuer zum Vault-Admin. ⚠️ Nebenwirkung: diese Namen werden damit fuer + **alle** VLANs aufloesbar, nicht nur fuer MAIN β€” MEDIA (`*A`) und GUEST + (`*B`+`*A`) erreichen `.40:443` ebenfalls. Einhegung ist ein eigener Schritt. + + RΓΌckweg: die Vorher-Werte stehen als `A1`/`A2`-Tabellen im Plan + (`~/.claude/plans/…`); vor dem Deploy liegt ein Backup auf dem Core. + """ + lines = [_dns_upsert(ln) for ln in _dns_static_lines()] + # Abnahme-Zeile: die Zahl der Statics plus drei Stichproben, die den Ausfall + # repraesentieren (ap3 gab es vorher nirgends, pve zeigt auf Caddy, vw muss + # `.40` sein β€” nicht die CF-Anycast-IP). + lines.append( + ':do { :log info ("tp45-c-dns: statics=" . [:tostr [:len [/ip dns static find]]] ' + '. " ap3=" . [:tostr [:len [/ip dns static find where name=ap3.bf.emsenhuber.net]]] ' + '. " vw=" . [/ip dns static get [find where name=vw.emsenhuber.net] address] ' + '. " pve=" . [/ip dns static get [find where name=pve.bf.emsenhuber.net] address]) } ' + f'on-error={{ :log info "tp45-c-dns: Abnahme-Zeile geworfen" }}') + return [(["crs326-core"], "\n".join(lines), "TP45-c-dns")] + + JOBS = { "drift-cleanup": job_drift_cleanup, "mgmt-acl": job_mgmt_acl, @@ -858,6 +933,7 @@ JOBS = { "tp38-oob": job_tp38_oob, "tp45-agh-fw": job_tp45_agh_fw, "tp45-agh-dhcp": job_tp45_agh_dhcp, + "tp45-c-dns": job_tp45_c_dns, "tp45-pool-v20": job_tp45_pool_v20, "tp45-v30-leases": job_tp45_v30_leases, "tp45-v30-revert": job_tp45_v30_revert, diff --git a/nautobot/mikrotik_routeros.j2 b/nautobot/mikrotik_routeros.j2 index 74b2c99..9a4f078 100644 --- a/nautobot/mikrotik_routeros.j2 +++ b/nautobot/mikrotik_routeros.j2 @@ -312,18 +312,49 @@ # FT-Transition statt Voll-Reassoziation. Das Band-Springen selbst endet erst # mit dem Deckungsfix (TP25 Variante 3). Siehe teilprojekte.md TP25 (Stand 09-14). -# Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich): -# core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12 +# Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich; ap3 ergaenzt 2026-09-22): +# core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12 | ap3 .99.13 +# +# πŸ”΄ TP45 Schicht C (2026-09-22): DIESE Liste ist die EINZIGE Quelle fuer interne Namen. +# AGH wertet seine DNS-Rewrites nur im Filter-Pfad aus; fuer unfiltrierte Clients +# (dns-bypass-exempt, grp-infra/-media/-mgmt) greifen sie nicht β€” die landen hier. +# Hier fehlten 12 Namen, genau das war der FQDN-Ausfall. +# Merkregel: was in AGH als Rewrite steht, muss auch hier stehen. /ip dns static add address=10.40.99.1 comment="crs326-core (L3)" name=core.bf.emsenhuber.net type=A /ip dns static add address=10.40.99.2 comment=crs326-branch name=branch.bf.emsenhuber.net type=A /ip dns static add address=10.40.99.11 comment="ap1 = reiner AP" name=ap1.bf.emsenhuber.net type=A /ip dns static add address=10.40.99.12 comment="ap2 = reiner AP" name=ap2.bf.emsenhuber.net type=A +/ip dns static add address=10.40.99.13 comment="ap3 = reiner CAP (cAP ax)" name=ap3.bf.emsenhuber.net type=A /ip dns static add address=10.40.20.167 name=macmini.bf.emsenhuber.net type=A -/ip dns static add address=10.40.20.100 name=pve.bf.emsenhuber.net type=A -/ip dns static add address=10.40.20.173 name=pve-mcp.bf.emsenhuber.net type=A /ip dns static add address=10.40.20.10 name=nas.bf.emsenhuber.net type=A /ip dns static add address=10.40.20.126 name=docker.bf.emsenhuber.net type=A /ip dns static add address=10.40.20.20 name=platform-pg.bf.emsenhuber.net type=A +/ip dns static add address=10.40.10.6 comment="Brother MFC (VLAN10)" name=mfc.bf.emsenhuber.net type=A +/ip dns static add address=10.40.40.11 comment="TV (VLAN40)" name=tv.bf.emsenhuber.net type=A + +# Caddy-Frontends (TP31): Dienste mit eigener Site laufen ueber den Reverse Proxy, damit das +# TLS-Zertifikat gueltig ist. Quelle: caddy/Caddyfile (live geprueft 2026-09-22). +/ip dns static add address=10.40.20.40 comment="caddy -> nautobot :8080" name=nautobot.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> pbs :8007" name=pbs.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> portainer :9443" name=portainer.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> ccu :80" name=ccu.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> iobroker :8081" name=iobroker.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> z2m25 :8080" name=z2m25.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> home-assistant mcp :9583" name=ha-mcp.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> n8n-mcp :3001" name=n8n-mcp.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> gitea-mcp :3000" name=gitea-mcp.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> pve :8006" name=pve.bf.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="caddy -> pve-mcp :3000" name=pve-mcp.bf.emsenhuber.net type=A + +# Oeffentliche Namen (bare emsenhuber.net) β€” real via CF Access + Tunnel. Sie stehen hier NUR, +# damit LAN-Clients direkt auf Caddy kommen: der CF-Weg setzt CF-Connecting-IP, und der +# @cf_admin-Guard in caddy/Caddyfile antwortet darauf mit 404 (TP41 T2). Der lokale, +# header-freie Weg ist die einzige Tuer zum Vault-Admin (Christoph-Entscheid 2026-09-22). +/ip dns static add address=10.40.20.40 comment="vw via caddy (Vault-Admin nur lokal)" name=vw.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="hass via caddy" name=hass.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="n8n via caddy" name=n8n.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="gitea via caddy" name=gitea.emsenhuber.net type=A +/ip dns static add address=10.40.20.40 comment="ds-bf via caddy" name=ds-bf.emsenhuber.net type=A # [7] NAT /ip firewall nat add action=accept chain=srcnat comment="TP4: no-NAT inter-VLAN (LAN-internal, before masquerade)" dst-address=10.40.0.0/16