Files
bf-infra/nautobot/mikrotik_routeros.j2
Christoph Emsenhuber 890c9f4869 TP6.1 live: NAS-Sync weicht zurueck -- 40,8 auf 11,0 ms unter Last
Christophs Einwand beim TP6-Abschluss war berechtigt, und er kippt die Erklaerung,
die ich einen Tag lang vertreten habe.

ABNAHME (gleiche NAS-Last, gleicher Messclient, Kontrolle inklusive):
  Priorisierung aus    40,8 ms avg / 53,4 max
  Priorisierung an     11,0 ms avg / 14,6 max     -73 %

Die Last war echt, nicht behauptet: wan-up-total fuhr 9,0 Mbit (max-limit greift
exakt), wan-up-bulk 8,87 Mbit mit 1581 Drops, vlan100 tx 9,0 Mbps. Die Kontrolle
mit abgeschalteter Markierung war noetig, um auszuschliessen, dass die Verbesserung
bloss daher kommt, dass Bulk und Ping jetzt von verschiedenen Hosts stammen.

KORREKTUR AN TP6: dort steht, die 30 ms entstuenden im DOCSIS-Uplink und seien
unabaenderlich. Das ist widerlegt. Der Fehler lag in der METHODE dieser Messung:
Bulk und Messverkehr kamen vom SELBEN Host, also gab es fuer CAKEs triple-isolate
nichts zu trennen -- alles lag in derselben Klasse, und Shapen allein aendert keine
Reihenfolge. Was CAKE nicht schaffte, schafft explizite Klassentrennung.
TP6 traegt jetzt einen Warnhinweis und ist als Messprotokoll zu lesen, nicht als
Ursachenanalyse. Weiter gueltig bleibt: CAKE als EINE Queue ueber allem brachte
nichts (dreimal gemessen), und Interface-HTB ist auf diesem Geraet tot.

GEBAUT (Template [11a], via GC):
  2x mangle postrouting (VOR src-nat -- nur dort ist die LAN-Quelle sichtbar):
     NAS 10.40.20.10 -> bulkup, alles andere -> restup
  wan-up-total (parent=global, max-limit=9M)
     wan-up-rest  priority=1 limit-at=6M
     wan-up-bulk  priority=8 limit-at=1M

Drei Entscheidungen, begruendet im Template:
  - Prioritaet statt Cap: ein Limit haette die NAS auch nachts gebremst und
    Bandbreite verschenkt (DSM drosselt nicht, User-bestaetigt). limit-at=1M
    sorgt dafuer, dass der Sync nie ganz verhungert.
  - src-address=10.40.20.10, NICHT VLAN20: dort haengen auch HA, Nautobot, pve.
  - parent=global, nicht vlan100: Interface-HTB ist hier wirkungslos.

GOVERNANCE: ComplianceRule ip-firewall-mangle angelegt (ingest-mangle-rule.py),
config_ordered=True -- die NAS-Regel muss vor dem Catch-all stehen, sonst waere die
Priorisierung STILL wirkungslos. 26 Rules x 4 Geraete = 104 Checks, 104/104 gruen.

Formatfalle nebenbei: priority=8 ist Default und wird von /export weggelassen --
explizit im Template erzeugt es Dauer-Drift, bei dem das Geraet recht hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:38:39 +02:00

491 lines
35 KiB
Django/Jinja

# Nautobot Golden Config -- intended RouterOS config for {{ obj.name }}
#
# FORMAT-REGEL (TP22, 2026-08-13): Zeilen sind im RouterOS-**/export-Format** geschrieben
# (Space-Form `/ip address add ...`, Attribute ALPHABETISCH), nicht in der Slash-Form der
# Provisionierungs-Skripte. Golden Config vergleicht Backup (= echter /export) und Intended
# textuell -- jede Abweichung in Reihenfolge, Quoting oder Pfadform ist ein Falsch-Drift.
# Referenz-Exporte: routeros/state-backups/*-gc-backup-scrubbed-*.cfg
#
# Secrets tragen die Maske "<<REDACTED>>" der ConfigReplace-Backup-Regeln.
# 🔴 Zeilen mit <<REDACTED>> duerfen NIE per Deploy gepusht werden.
/system identity set name={{ obj.name }}
# MGMT-DIENSTE + ADDRESS-ACL (TP16.1, 2026-08-17) — fuer ALLE vier Geraete identisch.
# Warum die Mesh-Range dazu MUSS: seit TP16.1 kommt der Remote-Zugriff aus 100.96.0.0/12
# (Cloudflare Mesh). Eine ACL, die nur 10.40.0.0/16 kennt, laesst Ping durch, sperrt aber
# SSH/REST/MikroMCP aus — das Symptom hat auf dem Core eine halbe Stunde gekostet.
# Warum ftp/telnet/api-ssl aus: Klartext-Protokolle bzw. ungenutzt. Der Core hatte das
# bereits, branch/ap1/ap2 fuhren sie offen und OHNE jede Address-Beschraenkung.
# winbox war auf ALLEN vier unbeschraenkt.
# ⚠️ Lockout-Regel: 10.40.0.0/16 (LAN) UND 100.96.0.0/12 (Mesh) muessen beide drin sein —
# das sind die zwei Wege, ueber die dieses Netz administriert wird. Nie einen davon kappen.
# ⚠️ Attribut heisst `available-from`, NICHT `address` (korrigiert 2026-08-18): RouterOS
# 7.23/7.24 exportiert `available-from=`, das Template schrieb `address=`. Ergebnis waren
# vier Dauer-Drifts (ap1/ap2/branch/core), bei denen das GERAET recht hatte und das Soll
# falsch war. Beim Push haette `address=` auf 7.24 zudem auflaufen koennen.
/ip service set ftp disabled=yes
/ip service set ssh available-from=10.40.0.0/16,100.96.0.0/12
/ip service set telnet disabled=yes
/ip service set www available-from=10.40.0.0/16,100.96.0.0/12
/ip service set winbox available-from=10.40.0.0/16,100.96.0.0/12
/ip service set api available-from=10.40.0.0/16,100.96.0.0/12
/ip service set api-ssl disabled=yes
{% if obj.name == "crs326-core" %}
{#- VLAN-Tabelle: Site-Konstante. name = RouterOS-comment, lease/pool je VLAN. -#}
{%- set vlans = [
{"id": 10, "name": "MAIN", "pool": "10.40.10.100-10.40.10.200", "lease": "1d"},
{"id": 20, "name": "INFRA", "pool": "10.40.20.100-10.40.20.200", "lease": "1d"},
{"id": 30, "name": "IOT", "pool": "10.40.30.100-10.40.30.250", "lease": "1d"},
{"id": 40, "name": "MEDIA", "pool": "10.40.40.100-10.40.40.200", "lease": "1d"},
{"id": 50, "name": "UNTRUSTED", "pool": "10.40.50.100-10.40.50.200", "lease": "1d"},
{"id": 90, "name": "GUEST", "pool": "10.40.90.100-10.40.90.200", "lease": "12h"},
{"id": 99, "name": "MGMT", "pool": "10.40.99.100-10.40.99.200", "lease": "1d"}
] %}
# =====================================================================================
# crs326-core — PRODUKTIVER L3-Router (seit Cutover 2026-08-08, TP21.1).
# Die frueheren `disabled=yes`-Staging-Flags sind ENTFERNT: sie stammten aus der
# Vorbereitung des Cutovers und beschreiben nicht mehr den Sollzustand.
# =====================================================================================
# [0] L2-FABRIC — Bridge-Ports + Bridge-VLAN-Tabelle.
# Literal-Block, KEINE Ableitung aus DCIM: der As-built traegt bewusste Schiefstaende
# (ether3 ist untagged-Member von VLAN40, hat aber pvid=1; ether2 haengt zusaetzlich in
# VLAN1). Wuerde man pvid aus `untagged_vlan` generieren, meldete die Compliance genau
# diese akzeptierten Altlasten als Dauerdrift. Der Block ist damit das, was Golden Config
# fuer einen handgebauten L2-Layer leisten soll: Drift-Erkennung gegen den Ist-Zustand.
# `/interface bridge` selbst ist bewusst NICHT abgedeckt (traegt die geraetespezifische
# admin-mac -> waere pro Geraet verschieden und ist kein Fleet-Standard).
/interface bridge port add bridge=bridge comment=defconf frame-types=admit-only-vlan-tagged interface=ether1
/interface bridge port add bridge=bridge comment=defconf interface=ether2 pvid=40
/interface bridge port add bridge=bridge comment=defconf interface=ether3
/interface bridge port add bridge=bridge comment=defconf interface=ether4 pvid=40
/interface bridge port add bridge=bridge comment=defconf interface=ether5 pvid=40
/interface bridge port add bridge=bridge comment=defconf interface=ether6 pvid=40
/interface bridge port add bridge=bridge comment=defconf interface=ether7
/interface bridge port add bridge=bridge comment=defconf interface=ether8
/interface bridge port add bridge=bridge comment=defconf interface=ether9
/interface bridge port add bridge=bridge comment=defconf interface=ether10
/interface bridge port add bridge=bridge comment=defconf interface=ether11 pvid=10
/interface bridge port add bridge=bridge comment=defconf interface=ether12
/interface bridge port add bridge=bridge comment=defconf interface=ether13
/interface bridge port add bridge=bridge comment=defconf interface=ether14 pvid=30
/interface bridge port add bridge=bridge comment=defconf interface=ether15
/interface bridge port add bridge=bridge comment=defconf interface=ether16 pvid=100
{% for n in range(17, 25) %}
/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }}
{% endfor %}
/interface bridge port add bridge=bridge comment=defconf interface=sfp-sfpplus1
/interface bridge port add bridge=bridge comment=defconf interface=sfp-sfpplus2
# ether4 = Wohnzimmer-Sideboard: 2026-08-13 von Trunk auf untagged VLAN40 gezogen (TP21.1/4).
/interface bridge vlan add bridge=bridge untagged=ether1,ether2 vlan-ids=1
# ether13 (pve) traegt VLAN10 + VLAN40 seit 2026-08-18: der SSDP-Relay-LXC fuer die
# macOS-Sonos-App braucht ein Bein in beiden (TP7.1). SSDP ist link-lokal mit TTL 1 —
# ein Relay muss Mitglied beider Broadcast-Domaenen sein, Routing hilft nicht.
/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether13,ether15 vlan-ids=10
/interface bridge vlan add bridge=bridge tagged=ether8,ether13,ether15 vlan-ids=20
/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 untagged=ether14 vlan-ids=30
/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether13,ether15 untagged=ether2,ether3,ether4,ether5 vlan-ids=40
/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 vlan-ids=50
/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 vlan-ids=90
/interface bridge vlan add bridge=bridge tagged=ether8,ether15,bridge vlan-ids=99
/interface bridge vlan add bridge=bridge tagged=ether1 untagged=ether16 vlan-ids=100
# [1] INTERFACE-LISTS
/interface list add comment=TP15 name=WAN
/interface list add comment=TP15 name=LAN
/interface list member add interface=vlan100 list=WAN
{% for v in vlans %}
/interface list member add interface=vlan{{ v.id }} list=LAN
{% endfor %}
# [2] VLAN-SVIs auf der Bridge (vlan99 traegt bewusst keinen comment)
{% for v in vlans %}
/interface vlan add {% if v.id != 99 %}comment={{ v.name }} {% endif %}interface=bridge name=vlan{{ v.id }} vlan-id={{ v.id }}
{% endfor %}
/interface vlan add comment=WAN interface=bridge name=vlan100 vlan-id=100
# [3] GW-IPs je VLAN — datengetrieben aus Nautobot (IP am SVI-Interface gebunden).
# comment = Interface-Description; network wird aus der Host-IP abgeleitet.
{% for iface in obj.interfaces.all() %}
{% if iface.name.startswith("vlan") %}
{% for ip in iface.ip_addresses.all() %}
/ip address add address={{ ip.address }} comment="{{ iface.description }}" interface={{ iface.name }} network={{ ip.host.split(".")[:3] | join(".") }}.0
{% endfor %}
{% endif %}
{% endfor %}
# [4] WAN — DHCP-Client auf vlan100 (Liwest/Arris)
/ip dhcp-client add comment="WAN Arris DHCP (Liwest) via vlan100" interface=vlan100 use-peer-dns=no use-peer-ntp=no
# [5] DHCP-Server je VLAN
{% for v in vlans %}
/ip pool add name=pool-v{{ v.id }} ranges={{ v.pool }}
{% endfor %}
{% for v in vlans %}
/ip dhcp-server add address-pool=pool-v{{ v.id }} comment={{ v.name }} interface=vlan{{ v.id }} lease-time={{ v.lease }} name=dhcp-v{{ v.id }}
{% endfor %}
{% for v in vlans %}
/ip dhcp-server network add address=10.40.{{ v.id }}.0/24 dns-server=10.40.{{ v.id }}.1 domain=bf.lan gateway=10.40.{{ v.id }}.1 ntp-server=10.40.{{ v.id }}.1
{% endfor %}
/ip dhcp-server option add code=6 name="dns 1.1.1.1" value=0x01010101
# [6] DNS — DoH zum Cloudflare-Gateway, MIT Zertifikatspruefung (TP23 Stufe 1,
# scharf seit 2026-08-18; schliesst TP21.1 TODO 1).
# `servers=1.1.1.1` BLEIBT als Bootstrap stehen: RouterOS loest den
# DoH-Hostnamen selbst darueber auf, sonst Henne-Ei. Alles andere geht
# ueber DoH.
# Die frueher hier notierte Begruendung "core hat kein CA-Bundle" war zu
# pessimistisch: es braucht kein Bundle, sondern genau EINEN Root. Die Kette
# ist Leaf -> GTS WE1 -> GTS Root R4, importiert ist nur R4 (~1 KB, gueltig
# bis 2036) — entscheidend bei 2,8 MB freiem Flash.
# Der Import selbst steht bewusst NICHT im Template: er braucht die Datei
# auf dem Geraet und ist ein einmaliger Bootstrap, kein Sollzustand.
# Artefakt: routeros/tp23-stufe1-doh-core.rsc
/ip dns set allow-remote-requests=yes mdns-repeat-ifaces=vlan10,vlan20,vlan40,vlan30,vlan90 servers=1.1.1.1 use-doh-server=https://px35ynlol3.cloudflare-gateway.com/dns-query verify-doh-cert=yes
# [6a] ZEIT. Der Core ist die Zeitquelle des Standorts — und braucht dafuer selbst eine.
# Vor dem 2026-08-18 war /system ntp client AUS und der Server ebenfalls: die anderen drei
# Geraete zeigten auf 10.40.99.1 ins Leere (`status: waiting`), ap2 war 17 h versetzt.
# ⚠️ Ein NTP-Server mit `use-local-clock=no` und unsynchronisiertem Client liefert Stratum 16,
# das jeder Client verwirft — Server einschalten allein reicht also NICHT.
# Die Clients bekommen den Server zusaetzlich per DHCP (ntp-server=, s. [5]).
# Schreibweise flach-einzeilig, weil das GC-Backup so ablegt (nicht im Sektions-Stil von /export).
/system ntp client set enabled=yes
/system ntp client servers add address=time.cloudflare.com
/system ntp client servers add address=at.pool.ntp.org
/system ntp server set enabled=yes
# [6b] WLAN — CAPsMAN. Seit TP28 (2026-08-17) ist der Core der Manager und ap1/ap2
# sind reine CAPs. Die fuenf wifi-*-ComplianceFeatures gehoeren deshalb HIERHER
# und nicht mehr in den AP-Zweig; auf den APs ist die entsprechende Config
# abgeraeumt worden (sie war nach dem Cutover nur noch wirkungsloser Rest).
#
# 🔴 SECURITY IST NICHT DEPLOYBAR. Die Passphrasen stehen hier als
# "<<REDACTED>>", weil TP22-Regel 1 maskierte Sektionen generell vom Push
# ausschliesst. Ein gruener Compliance-Haken auf `wifi-security` beweist
# deshalb NUR, dass Soll und Ist dieselbe Maske tragen — er beweist NICHT,
# dass die Passphrasen stimmen und schon gar nicht, dass die Sektion
# ausgerollt werden koennte. Wer sie wirklich aendern will, macht das am
# Geraet und liest die Werte per REST zurueck
# (`GET http://10.40.99.1/rest/interface/wifi/security`, s. TP28) —
# NICHT aus `wlan-psks.env` oder den drei alten Bring-up-Dateien, die
# voneinander abgeschrieben sind und schon einmal 20 Shellys aus dem Netz
# geworfen haben.
#
# Literal-Block wie die L2-Fabric: Radio-MACs und Kanalzuordnung sind
# geraetegebunden und in Nautobot nicht modelliert. Aenderungen daran sind
# bewusst sichtbare Template-Aenderungen.
/interface wifi channel add band=5ghz-ax frequency=5180 name=ch-5g-ap1 width=20/40mhz
/interface wifi channel add band=5ghz-ax frequency=5220 name=ch-5g-ap2 width=20/40mhz
/interface wifi channel add band=2ghz-ax frequency=2412 name=ch-2g-ap1 width=20mhz
/interface wifi channel add band=2ghz-ax frequency=2462 name=ch-2g-ap2 width=20mhz
/interface wifi datapath add bridge=bridge name=dp-main vlan-id=10
/interface wifi datapath add bridge=bridge name=dp-infra vlan-id=20
/interface wifi datapath add bridge=bridge name=dp-iot vlan-id=30
/interface wifi datapath add bridge=bridge name=dp-media vlan-id=40
/interface wifi datapath add bridge=bridge name=dp-untrusted vlan-id=50
/interface wifi datapath add bridge=bridge name=dp-guest vlan-id=90
/interface wifi security add authentication-types=wpa3-psk ft=yes ft-over-ds=yes name=sec-main passphrase="<<REDACTED>>"
/interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-infra passphrase="<<REDACTED>>"
/interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-iot passphrase="<<REDACTED>>"
/interface wifi security add authentication-types=wpa2-psk,wpa3-psk ft=no name=sec-media passphrase="<<REDACTED>>"
/interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-untrusted passphrase="<<REDACTED>>"
/interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-guest passphrase="<<REDACTED>>"
/interface wifi configuration add country=Austria datapath=dp-infra mode=ap name=cfg-infra security=sec-infra ssid=dirtyvader-infra
/interface wifi configuration add country=Austria datapath=dp-iot mode=ap name=cfg-iot security=sec-iot ssid=dirtyvader-iot
/interface wifi configuration add country=Austria datapath=dp-media mode=ap name=cfg-media security=sec-media ssid=dirtyvader-media
/interface wifi configuration add country=Austria datapath=dp-untrusted mode=ap name=cfg-untrusted security=sec-untrusted ssid=dirtyvader-untrusted
# st-guest hat eine EIGENE neighbor-group: mit bf-main wuerde 802.11k den Gaesten
# BSSIDs einer SSID melden, auf die sie gar nicht assoziieren koennen (TP28).
/interface wifi steering add name=st-main neighbor-group=bf-main rrm=yes wnm=yes
/interface wifi steering add name=st-guest neighbor-group=bf-guest rrm=yes wnm=yes
/interface wifi configuration add channel=ch-5g-ap1 country=Austria datapath=dp-main mode=ap name=cfg-main-5g-ap1 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add channel=ch-5g-ap2 country=Austria datapath=dp-main mode=ap name=cfg-main-5g-ap2 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add channel=ch-2g-ap1 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap1 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add channel=ch-2g-ap2 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap2 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add country=Austria datapath=dp-guest mode=ap name=cfg-guest security=sec-guest ssid=dirtyvader-guest steering=st-guest
/interface wifi capsman set ca-certificate=auto certificate=auto enabled=yes interfaces=vlan99 upgrade-policy=none
/interface wifi provisioning add action=create-dynamic-enabled comment="ap1 5GHz" master-configuration=cfg-main-5g-ap1 radio-mac=D0:EA:11:4D:40:E5 slave-configurations=cfg-media,cfg-untrusted,cfg-guest
/interface wifi provisioning add action=create-dynamic-enabled comment="ap1 2.4GHz" master-configuration=cfg-main-2g-ap1 radio-mac=D0:EA:11:4D:40:E6 slave-configurations=cfg-iot,cfg-media,cfg-untrusted,cfg-guest
/interface wifi provisioning add action=create-dynamic-enabled comment="ap2 5GHz" master-configuration=cfg-main-5g-ap2 radio-mac=D0:EA:11:4D:40:3E slave-configurations=cfg-media,cfg-untrusted,cfg-guest
/interface wifi provisioning add action=create-dynamic-enabled comment="ap2 2.4GHz" master-configuration=cfg-main-2g-ap2 radio-mac=D0:EA:11:4D:40:3F slave-configurations=cfg-iot,cfg-media,cfg-untrusted,cfg-guest
# [6c] WLAN ACCESS-LIST — der Soll-Zustand ist LEER, und das ist eine Aussage.
#
# Bis 2026-08-22 stand dieser Abschnitt unter KEINER ComplianceRule. Aenderungen
# dort erzeugten keinen Drift und liessen sich nicht als Transaktion fuehren --
# `gc-transaktion.py` verlangt eine ComplianceRule als --scope. Das war nicht
# theoretisch: `docs/tp25-pin-s22.sh` schreibt bei jedem Messrundgang
# access-list-Regeln (Kommentar TP25-PIN-S22) auf den Manager, um ein Telefon auf
# einen AP festzunageln.
#
# Seit der ComplianceRule `wifi-access-list` faellt jeder Eintrag hier auf.
# Waehrend eines Messlaufs ist der gemeldete Drift ERWARTET -- dann gehoert er als
# offene Transaktion gefuehrt:
# python3 nautobot/gc-transaktion.py begin --device crs326-core \
# --scope wifi-access-list --change-id TEST-... --expires JJJJ-MM-TT
#
# 🔴 Hier landet TP25 **Fund E** (`signal-range`-Boden gegen klebende Clients),
# sobald er entschieden ist. Stand 2026-08-22 ist er es NICHT: der Sticky-Client
# vom 08-20 reproduzierte sich beim Rundgang am 08-22 nicht, und in den schwachen
# Raeumen gibt es keinen besseren AP, auf den ein Boden steuern koennte. Ein
# voreilig gesetzter Boden verschlechtert die Terrasse, statt zu helfen.
# Vor dem Eintragen also: Sticky-Fall erneut messen. Siehe teilprojekte.md TP25.
# Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich):
# core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12
/ip dns static add address=10.40.99.1 comment="core = GW/Router" name=router.lan type=A
/ip dns static add address=10.40.99.1 comment="crs326-core (L3)" name=core.bf.lan type=A
/ip dns static add address=10.40.99.2 comment=crs326-branch name=branch.bf.lan type=A
/ip dns static add address=10.40.99.11 comment="ap1 = reiner AP" name=ap1.bf.lan type=A
/ip dns static add address=10.40.99.12 comment="ap2 = reiner AP" name=ap2.bf.lan type=A
/ip dns static add address=10.40.20.167 name=macmini.bf.lan type=A
/ip dns static add address=10.40.20.100 name=pve.bf.lan type=A
/ip dns static add address=10.40.20.173 name=pve-mcp.bf.lan type=A
/ip dns static add address=10.40.20.10 name=nas.bf.lan type=A
/ip dns static add address=10.40.20.126 name=docker.bf.lan type=A
/ip dns static add address=10.40.20.20 name=platform-pg.bf.lan 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
/ip firewall nat add action=masquerade chain=srcnat comment="defconf: masquerade" ipsec-policy=out,none out-interface-list=WAN
# [7b] FIREWALL FILTER (TP26, 2026-08-17). Kommt als DATEN aus
# local_config_context_data.firewall_filter, nicht als Templatezeilen —
# 36 Regeln fest hier einzutragen hiesse, dass jede TP5-Etappe einen
# Template-Edit braucht. Befuellt/re-baselined von
# nautobot/ingest-core-firewall.py (bewusst manuell, nie automatisch:
# ein Soll, das dem Ist nachlaeuft, erkennt keine Drift mehr).
# Reihenfolge ist semantisch — die ComplianceRule steht auf config_ordered.
{% for line in obj.get_config_context().get("firewall_filter", []) %}
/ip firewall filter {{ line }}
{% endfor %}
# [8] ROUTEN zu den WG-Spokes (Default-Route kommt dynamisch vom WAN-DHCP-Client)
/ip route add comment="TP10: FM spoke -> wg-hub" dst-address=10.101.0.0/24 gateway=wg-hub
/ip route add comment="TP10: DW spoke -> wg-hub" dst-address=10.95.10.0/24 gateway=wg-hub
# [9] WG-HUB (TP10 Multi-Site). Der echte Privkey liegt in routeros/ap1-wg-hub.privkey.
/interface wireguard add comment="TP10 Multi-Site VPN Hub (FM/DW spokes)" listen-port=51821 mtu=1412 name=wg-hub private-key="<<REDACTED>>"
/interface wireguard peers add allowed-address=10.101.0.0/24 comment=fm endpoint-address=abxhutfgsid9snmy.myfritz.net endpoint-port=52476 interface=wg-hub name=fm public-key="dzRY0lnnUygijChWe5wXZmXPeNcbdALpqBptA3fyiVE="
/interface wireguard peers add allowed-address=10.95.10.0/24 comment=dw interface=wg-hub name=dw public-key="FqYZLztQ/qME5VDie5QLGH++5299WJiE1dPpQnPbzUw="
# [10] CF-MESH (TP16.1) — privater Zugang unterwegs via natives WireGuard zum CF-PoP.
# Ersetzt den cloudflared-QUIC-Proxy fuer 10.40.0.0/16 (die oeffentlichen FQDNs bleiben
# auf dem 3-Tunnel-HA aus TP16). Privkey kommt von CLOUDFLARE, nicht vom Geraet — er
# steht in mcp/cloudflare/.env (CF_MESH_PRIVKEY, gitignored) und wurde einmalig per SSH
# eingespielt; hier nur die Backup-Maske, wie bei wg-hub.
# MTU 1420 aus dem animmouse-Guide. Vergleichswert auf demselben Geraet: wg-hub = 1412.
# Bei Path-MTU-Symptomen (TCP baut auf, grosse Transfers haengen) zuerst dorthin angleichen.
# Peer wurde von RouterOS zunaechst "peer5" auto-benannt und auf "cf-pop" umbenannt.
# ⚠️ MikroMCPs manage_wireguard_peer kennt weder persistent-keepalive noch einen Namen,
# und es schreibt endpoint="IP:Port" komplett in endpoint-address (endpoint-port bleibt 0
# -> KEIN Handshake). Nach jedem Peer-Add per `print detail` gegenlesen.
/interface wireguard add comment="TP16.1 Cloudflare Mesh" listen-port=13279 mtu=1420 name=cf-mesh private-key="<<REDACTED>>"
/interface wireguard peers add allowed-address=0.0.0.0/0 comment="TP16.1 CF-PoP" endpoint-address=162.159.193.2 endpoint-port=2408 interface=cf-mesh name=cf-pop persistent-keepalive=25s public-key="bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo="
/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
# [11] QoS — BEWUSST LEER (TP6 geschlossen 2026-08-23).
#
# Hier stand einen Tag lang eine CAKE-Queue. Sie ist entfernt, weil sie GEMESSEN
# nichts gebracht hat — und das ist die Aussage dieses Abschnitts:
#
# ohne CAKE 42,4 ms | 88 % 43,3 | 73 % 44,0 | 52 % der Linerate 44,6 (idle 12,9)
#
# Selbst bei halber Linerate keine Verbesserung. Dort MUSS die Warteschlange im Router
# liegen — also entsteht die Zusatzlatenz stromabwaerts, im Arris/DOCSIS-Uplink
# (Request/Grant-Scheduling auf MAC-Layer, gar keine Queue). Kein Shaper hier erreicht das.
#
# 🔴 Zwei Dinge, die beim naechsten QoS-Gedanken Zeit sparen:
# 1. `/queue tree parent=<interface>` ist auf diesem Geraet WIRKUNGSLOS — MikroTik:
# "Interface HTB will not work correctly when the out-interface is hardware offloaded
# and the bridge Fast Path is not active." Der Weg ist `parent=global` + ein
# `mangle chain=postrouting out-interface=vlan100 action=mark-packet`. So gebaut
# zaehlt und drosselt es nachweislich (9,3 -> 7,78 Mbit bei cake-bandwidth=7M).
# 2. Eine Simple Queue auf `10.40.0.0/16` taugt hinter NAT NICHT als Probe: src-nat
# laeuft VOR der Simple Queue, das Target matcht nie.
# Merkregeln: Memory `ref_queue_htb_platzierung_docsis`.
#
# ⚖️ Was hier sehr wohl ginge und offen ist: nicht Bufferbloat bekaempfen, sondern
# Saettigung VERMEIDEN (Bulk-Uploads zuruecknehmen, damit ein Call gar nicht erst in
# die Saettigung geraet). Das ist ein anderes Ziel und steht als TP6.1.
# Die ComplianceRule `queue` bewacht diesen Abschnitt weiter — jetzt mit dem Inhalt aus [11a].
# [11a] UPLOAD-PRIORISIERUNG (TP6.1, 2026-08-23) — das, was TP6 NICHT konnte.
#
# Der Unterschied in einem Satz: TP6 wollte das Warten ertraeglicher machen und
# scheiterte am DOCSIS-Puffer. Das hier verhindert, dass ueberhaupt gewartet wird.
# Die 42 ms treten NUR unter Saettigung auf — im Leerlauf misst dieselbe Strecke
# 12,9 ms. Ein NAS-Sync, der bei Last zurueckweicht, haelt die Leitung da raus.
#
# 🔴 PRIORITAET, KEIN CAP. Ein festes Limit fuer die NAS wuerde sie auch nachts
# bremsen, wenn die Leitung leer ist — genau die ungenutzte Bandbreite, die
# Christoph zu Recht nicht will (DSM drosselt von sich aus nicht). HTB nimmt dem
# Bulk die Bandbreite nur, solange jemand anders sie braucht.
#
# 🔴 `src-address=10.40.20.10` — die NAS-HOST-IP, bewusst NICHT ganz VLAN20.
# In INFRA haengen auch HA, Nautobot und pve; die gehoeren nicht pauschal nach
# hinten. Wer das je auf 10.40.20.0/24 erweitert, verlangsamt seine Werkzeuge.
#
# 🔴 `parent=global`, NICHT `parent=vlan100`. Interface-HTB ist auf diesem Geraet
# wirkungslos (hw-offloadetes out-interface, s. [11]). Der Global-Weg ist in TP6
# als wirksam BEWIESEN: 9,3 -> 7,78 Mbit bei gesetztem Limit.
#
# ⚠️ Die Markierung MUSS im postrouting stehen und die NAS-Regel VOR dem Catch-all:
# mangle postrouting laeuft VOR src-nat, nur dort ist die LAN-Quelle noch sichtbar
# (nach dem Masquerade ist alles die WAN-IP). Deshalb steht `ip-firewall-mangle`
# auf `config_ordered` — eine vertauschte Reihenfolge waere STILL wirkungslos.
#
# max-limit=9M auf dem Parent: knapp unter der gemessenen Linerate von 9,6 Mbit.
# HTB kann nur verteilen, was es selbst begrenzt — ohne dieses Limit gibt es keine
# Knappheit zu priorisieren. Preis: ~0,6 Mbit Spitze.
# limit-at sind Garantien, keine Grenzen: der Sync verhungert nie ganz (1M), und
# der Rest hat 6M sicher, auch wenn der Sync laeuft.
/ip firewall mangle add action=mark-packet chain=postrouting comment="TP6.1: NAS-Bulk" new-packet-mark=bulkup out-interface=vlan100 passthrough=no src-address=10.40.20.10
/ip firewall mangle add action=mark-packet chain=postrouting comment="TP6.1: Rest (interaktiv)" new-packet-mark=restup out-interface=vlan100 passthrough=no
/queue tree add max-limit=9M name=wan-up-total parent=global
/queue tree add limit-at=6M max-limit=9M name=wan-up-rest packet-mark=restup parent=wan-up-total priority=1
# ⚠️ Bei wan-up-bulk steht KEIN priority=8, obwohl genau das gemeint ist: 8 ist der
# Default, und /export laesst Defaults weg. Ein explizites priority=8 im Template
# erzeugt deshalb einen Dauer-Drift, bei dem das Geraet recht hat.
/queue tree add limit-at=1M max-limit=9M name=wan-up-bulk packet-mark=bulkup parent=wan-up-total
{% elif obj.role.name == "Switch" %}
# =====================================================================================
# L2-Switch (crs326-branch) — Sollzustand des Mgmt-Layers.
# Bewusst NICHT emittiert: die TEMP-Bring-up-Objekte (vlan10 "TEMP Mgmt VLAN10" +
# 10.40.10.2). Die tauchen dadurch als Compliance-`extra` auf — genau richtig, denn sie
# sind Altlast aus dem Bring-up und gehoeren entfernt (VLAN1/VLAN10-Interim, TP19).
# =====================================================================================
/interface vlan add comment=MGMT interface=bridge name=vlan99 vlan-id=99
/ip address add address={{ obj.primary_ip4.address }} comment=TP21-mgmt-target interface=vlan99 network={{ obj.primary_ip4.host.split(".")[:3] | join(".") }}.0
/ip dns set servers=10.40.99.1
/system ntp client set enabled=yes
/system ntp client servers add address=10.40.99.1
/ip route add comment=TP21-default dst-address=0.0.0.0/0 gateway=10.40.99.1
# L2-FABRIC — wie beim Core ein Literal-Block (Begruendung dort).
# ether9-23 tragen pvid=20 OHNE VLAN-Membership (Bootstrap-Rev4-Rest, funktionslos) —
# bewusst so abgebildet, damit eine spaetere Bereinigung als Drift sichtbar wird.
/interface bridge port add bridge=bridge comment=defconf interface=ether1
/interface bridge port add bridge=bridge comment=defconf interface=ether2
/interface bridge port add bridge=bridge comment=defconf interface=ether3 pvid=10
/interface bridge port add bridge=bridge comment=defconf interface=ether4 pvid=10
/interface bridge port add bridge=bridge comment=defconf interface=ether5 pvid=40
/interface bridge port add bridge=bridge comment=defconf interface=ether6 pvid=40
/interface bridge port add bridge=bridge comment=defconf interface=ether7 pvid=20
/interface bridge port add bridge=bridge comment=defconf interface=ether8
{% for n in range(9, 24) %}
/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }} pvid=20
{% endfor %}
/interface bridge port add bridge=bridge comment=defconf interface=ether24
/interface bridge port add bridge=bridge comment=defconf disabled=yes interface=sfp-sfpplus1
/interface bridge port add bridge=bridge comment=defconf disabled=yes interface=sfp-sfpplus2
/interface bridge vlan add bridge=bridge comment=MAIN tagged=ether1,ether24,bridge untagged=ether3,ether4 vlan-ids=10
/interface bridge vlan add bridge=bridge comment=INFRA tagged=ether1,ether24,bridge,ether2,ether8 untagged=ether7 vlan-ids=20
/interface bridge vlan add bridge=bridge comment=IOT tagged=ether1,ether24,bridge vlan-ids=30
/interface bridge vlan add bridge=bridge comment=MEDIA tagged=ether1,ether24,bridge untagged=ether5,ether6 vlan-ids=40
/interface bridge vlan add bridge=bridge comment=UNTRUSTED tagged=ether1,ether24,bridge vlan-ids=50
/interface bridge vlan add bridge=bridge comment=GUEST tagged=ether1,ether24,bridge vlan-ids=90
/interface bridge vlan add bridge=bridge comment=MGMT tagged=ether1,ether24,bridge vlan-ids=99
{% elif obj.role.name == "Wireless AP" %}
{% set ap = obj.get_config_context()["ap"] %}
{% set mgmt_host = obj.primary_ip4.host %}
{% set uplink = ap.uplink %}
# =====================================================================================
# TP22 — Standalone-AP (kein CAPsMAN). Gerendert aus ConfigContext "ap-common"
# + Device.local_config_context_data (Kanaele) + DCIM (Name, primary_ip4).
#
# SCOPE: nur die COMPLIANCE-VERWALTETEN Sektionen, bewusst NICHT die ganze Config.
# Golden Config vergleicht per ComplianceRule.match_config sektionsweise -> defconf-
# Boilerplate (ipv4/ipv6-Firewall, /disk, /tool mac-server) und die VAP-Zeilen
# (/interface wifi add ... mac-address=...) bleiben draussen: deren MACs sind
# geraetevergeben und in Nautobot nicht modelliert.
#
# 🔴 NIE als Ganzes auf ein laufendes Geraet pushen: die Provisionierung setzt
# zwischenzeitlich vlan-filtering=no, was den vlan99-Mgmt-Pfad abreisst -> der Rest
# der Zeilen kaeme ueber dieselbe SSH-Session nie an. Voll-Reprovisionierung
# ausschliesslich per Datei-Upload + /import (siehe routeros/.secrets/ap*-prov-base.rsc).
#
# Attributreihenfolge = alphabetisch, exakt wie RouterOS /export sie schreibt.
# Referenz: routeros/state-backups/ap1-gc-backup-scrubbed-20260813.cfg
# =====================================================================================
# [1] WLAN — ein CAP haelt keine eigenen SSIDs, aber sehr wohl den lokalen Anschluss.
# Bis 2026-08-18 rendert dieser Zweig Kanaele, Datapaths, Security, Configurations
# und Steering. Das war der Standalone-Stand von TP3; seit dem CAPsMAN-Cutover
# (TP28, 2026-08-17) kommen all diese Objekte vom Manager `crs326-core` und werden
# auf dem CAP als DYNAMISCHE Interfaces angelegt. Die statischen Reste auf ap1/ap2
# waren danach wirkungslos — aber sie standen weiter in der Config und haetten,
# als Soll gerendert, totes Zeug zur Absicht erklaert (dasselbe Argument wie bei
# den defconf-Firewallregeln in TP26). Sie sind am 2026-08-18 abgeraeumt worden.
#
# Die fuenf wifi-*-ComplianceFeatures greifen jetzt am Core, s. dort [6b].
# Hier bleiben zwei Zeilen: wo der CAP seinen Manager suchen soll, und WOHIN er
# die vom Manager erzeugten Slave-VAPs haengt.
# ⚠️ `discovery-interfaces` ist Layer-2-Discovery — der Manager wird ueber seine
# MAC gefunden, nicht ueber eine IP. Deshalb muessen MNDP (udp/5678) und CAPWAP
# (udp/5246-5247) in der input-Kette des Core offen bleiben (TP5 Etappe 4).
#
# 🔴 `slaves-datapath` IST NICHT OPTIONAL — Ausfall 2026-08-18, rund 14 h IOT+MEDIA:
# Der Manager schickt `datapath.bridge=bridge` zwar mit, der CAP uebernimmt davon
# aber NUR die vlan-id — die Bridge-Mitgliedschaft ist und bleibt lokal. Die
# Master-Radios wifi1/wifi2 sind statische Bridge-Ports (s. [4b]) und liefen
# deshalb weiter; die dynamischen Slave-VAPs (iot/media/untrusted/guest) haengen
# dagegen in GAR KEINER Bridge, solange `slaves-datapath` auf kein lokales
# datapath-Objekt zeigt. Beim Cutover (2026-08-17) wurde das Feld nie gesetzt —
# der Ausfall beginnt dort, nicht bei der TP5-Haertung und nicht beim Abraeumen
# der Standalone-Reste am 2026-08-18 (das hat ihn nur unreparierbar gemacht).
# Symptom: Client assoziiert sauber (am Manager als Registration sichtbar!),
# bekommt aber nie DHCP; am Core kommt auf dem VLAN kein einziges Frame an —
# `torch` leer UND Drop-Log leer, es wird also nichts verworfen, es kommt nichts.
# Das Objekt traegt bewusst KEINE vlan-id: die liefert der Manager pro VAP.
/interface wifi cap set discovery-interfaces=vlan{{ ap.mgmt.vlan }} enabled=yes slaves-datapath=dp-caps-slaves
# ⚠️ Export-Reihenfolge != Deploy-Reihenfolge: RouterOS exportiert `cap` VOR `datapath`,
# beim Push muss das datapath-Objekt aber ZUERST existieren.
/interface wifi datapath add bridge={{ ap.bridge }} comment="TP28-fix: bridge fuer CAPsMAN-Slave-VAPs" name=dp-caps-slaves
# [4b] BRIDGE-PORTS — ether2 fehlt bewusst: seit der Uplink-Umstellung auf ether1 (PoE)
# ist er KEIN Bridge-Port mehr. ether1 traegt kein comment=defconf (beim Rebuild
# neu angelegt), die uebrigen schon — deshalb hier nicht ueber einen Kamm geschoren.
/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface=ether3
/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface=ether4
/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface=ether5
{% for r in ap.radios %}
/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface={{ r.iface }}
{% endfor %}
/interface bridge port add bridge={{ ap.bridge }} interface={{ uplink }}
# [5] BRIDGE-VLAN — wifi1/wifi2 sind tagged-Member je Client-VLAN. Die Begruendung von
# 2026-08-17 ("Slave-VAPs forwarden ueber den Master") war falsch: mit gesetztem
# `slaves-datapath` haengt jeder Slave-VAP als eigener DYNAMISCHER Bridge-Port mit
# pvid = seiner Manager-vlan-id in der Bridge. Die tagged-Zeilen bleiben trotzdem
# stehen — sie kosten nichts und tragen den Uplink-Trunk. Die frueheren
# "Ausnahmen" fallen hier aus den Daten heraus: IOT hat bands=["2g"] -> nur wifi2.
/interface bridge vlan add bridge={{ ap.bridge }} untagged={{ ap.bridge }},{{ uplink }} vlan-ids={{ ap.native_vlan }}
{% for z in ap.zones %}
{% set members = [uplink] + (ap.radios | selectattr("key", "in", z.bands) | map(attribute="iface") | list) %}
/interface bridge vlan add bridge={{ ap.bridge }} tagged={{ members | join(",") }} vlan-ids={{ z.vlan }}
{% endfor %}
# MGMT liegt auf der Bridge, NICHT auf den Radios - sonst waere Management over-the-air.
/interface bridge vlan add bridge={{ ap.bridge }} tagged={{ uplink }},{{ ap.bridge }} vlan-ids={{ ap.mgmt.vlan }}
# [6] MGMT-LAYER
# interface-list + dns hier, damit die platform-weiten ComplianceFeatures
# (gelten pro Platform, nicht pro Rolle) auf APs nicht ins Leere laufen.
/interface list add comment=defconf name=WAN
/interface list add comment=defconf name=LAN
/interface list member add comment=defconf interface={{ ap.bridge }} list=LAN
/interface list member add interface=vlan{{ ap.mgmt.vlan }} list=LAN
/ip dns set allow-remote-requests=yes servers={{ ap.mgmt.gateway }}
# Zeitquelle ist der Core. Vor dem 2026-08-18 lief dort kein NTP-Server — ap2 war deshalb 17 h
# versetzt und seine Logs fuer jede geraeteuebergreifende Korrelation wertlos.
/system ntp client set enabled=yes
/system ntp client servers add address={{ ap.mgmt.gateway }}
/interface vlan add interface={{ ap.bridge }} name=vlan{{ ap.mgmt.vlan }} vlan-id={{ ap.mgmt.vlan }}
/ip address add address={{ obj.primary_ip4.address }} interface=vlan{{ ap.mgmt.vlan }} network={{ mgmt_host.split(".")[:3] | join(".") }}.0
/ip route add comment="mgmt default via core" dst-address=0.0.0.0/0 gateway={{ ap.mgmt.gateway }}
{% endif %}