# 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 "<>" der ConfigReplace-Backup-Regeln. # 🔴 Zeilen mit <> 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 # "<>", 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="<>" /interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-infra passphrase="<>" /interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-iot passphrase="<>" /interface wifi security add authentication-types=wpa2-psk,wpa3-psk ft=no name=sec-media passphrase="<>" /interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-untrusted passphrase="<>" /interface wifi security add authentication-types=wpa2-psk,wpa3-psk disabled=no ft=no name=sec-guest passphrase="<>" /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 # 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="<>" /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="<>" /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 {% 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 %}