# 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. # ether21 = ap3 (cAP ax, Technik UG) seit 2026-08-30, TP28-Nachtrag: gleicher AP-Trunk # wie ether15/ap1. Der Port traegt pvid=1 + frame-types=admit-all wie ether15 -- das # VLAN1-untagged-Bein entsteht dynamisch aus dem pvid und steht deshalb nicht hier. /interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether13,ether15,ether21 vlan-ids=10 /interface bridge vlan add bridge=bridge tagged=ether8,ether13,ether15,ether21 vlan-ids=20 /interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15,ether21 untagged=ether14 vlan-ids=30 /interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether13,ether15,ether21 untagged=ether2,ether3,ether4,ether5 vlan-ids=40 /interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15,ether21 vlan-ids=50 /interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15,ether21 vlan-ids=90 /interface bridge vlan add bridge=bridge tagged=ether8,ether15,ether21,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.20.36,10.40.20.37 domain=bf.emsenhuber.net 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 # [6a2] LOGGING โ€” Beweissicherung, kein Betriebsdetail. TP25 Fund E, 2026-08-30. # # Bis dahin liefen `wireless`-Logs NUR nach `action=memory`; nur `firewall` ging in den # Kollektor. Der Ringpuffer fasste 75 Zeilen und war von einer Stunde Arbeit fast # ueberschrieben -- ein seit Wochen gemeldeter iPhone-Abbruch war rueckwirkend deshalb # nicht diagnostizierbar. Die zwei neuen Zeilen sind die Datengrundlage fuer die # Entscheidung ueber die Steering-Hebel (2g-probe-delay / ft-over-ds / signal-range). # # โš ๏ธ Der Topic heisst `wireless`, NICHT `wifi` -- auch auf dem neuen wifi-qcom-Stack # (am Geraet nachgelesen). Ein geratenes `wifi` waere wirkungslos geblieben. # โš ๏ธ RouterOS UND-verknuepft Topics innerhalb EINER Regel (`info,!firewall` = info UND # NICHT firewall). `topics=wireless,caps` haette NICHTS gematcht -> zwei Regeln. # ๐Ÿ“Œ Nur der Core traegt hier Zeilen: RouterOS exportiert Defaults nicht, und unter # CAPsMAN fuehrt ohnehin der MANAGER die Roaming-Events -- die CAPs loggen lokal nichts. # Auf ap1/ap2/ap3/branch sind Soll und Ist beide leer, also compliant. /system logging action add name=tp5syslog remote=10.40.20.18 target=remote /system logging set 0 topics=info,!firewall /system logging add action=tp5syslog topics=firewall /system logging add action=tp5syslog comment="TP25 Fund E: WLAN-Roaming/Disconnects dauerhaft in den Kollektor" topics=wireless /system logging add action=tp5syslog comment="TP25 Fund E: CAP-An-/Abmeldungen dauerhaft in den Kollektor" topics=caps # [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 # ap3 (cAP ax, Technik UG) seit 2026-08-30. Kanaele aus dem TP25-Plan vom 2026-08-24: # Band A (5150-5250) ist das einzige non-DFS-Band mit vollen 23 dBm; ap3 bekommt es, weil # seine Flaeche von keinem anderen AP mitgetragen wird. 2437 = ch6 -> mit ap1 (ch1) und # ap2 (ch11) ist 2,4 GHz erstmals vollstaendig ueberlappungsfrei. /interface wifi channel add band=5ghz-ax frequency=5180 name=ch-5g-ap3 width=20/40mhz /interface wifi channel add band=2ghz-ax frequency=2437 name=ch-2g-ap3 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 channel=ch-5g-ap3 country=Austria datapath=dp-main mode=ap name=cfg-main-5g-ap3 security=sec-main ssid=dirtyvader-main steering=st-main /interface wifi configuration add channel=ch-2g-ap3 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap3 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 # ๐Ÿ”ด ap1 hat BEWUSST keine Provisioning-Zeile (2026-08-30). ap3 hat in Technik UG seinen # Platz uebernommen und funkt dort auf 5180; ap1 steht bis zu seinem Umzug ans Sideboard # direkt daneben und wuerde auf demselben Kanal kollidieren. Ohne passende Regel legt # CAPsMAN fuer ein Radio gar nichts an -> es bleibt still, das Geraet aber erreichbar. # Die Objekte `ch-5g-ap1`/`ch-2g-ap1` und `cfg-main-*-ap1` bleiben absichtlich stehen: # beim Umzug bekommt ap1 laut TP25 `5260/Ce` (Band B, DFS) -- dann werden nur diese # zwei Zeilen wieder eingesetzt und das Channel-Objekt auf eine Frequenz-LISTE gestellt # (mit nur einem Wert waere eine Radar-Erkennung ein SPOF). /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 /interface wifi provisioning add action=create-dynamic-enabled comment="ap3 5GHz" master-configuration=cfg-main-5g-ap3 radio-mac=D0:EA:11:FF:16:9F slave-configurations=cfg-media,cfg-untrusted,cfg-guest /interface wifi provisioning add action=create-dynamic-enabled comment="ap3 2.4GHz" master-configuration=cfg-main-2g-ap3 radio-mac=D0:EA:11:FF:16:A0 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. Die alte Auflage lautete "vor dem Eintragen den # Sticky-Fall erneut messen" -- das ist am 2026-08-30 passiert: dasselbe iPhone # machte 16 Bandwechsel in 20 Minuten bei -53..-81 dBm, erst an ap3, dann an ap2. # Fund E ist damit WIEDER OFFEN. # Eingetragen wird hier trotzdem NICHTS, und zwar bewusst: ein signal-range-Boden # ist der invasivste der drei Hebel und kann Clients AUSSPERREN -- genau das war # 08-22 das Gegenargument (in den schwachen Raeumen gibt es keinen besseren AP, # auf den ein Boden steuern koennte). Erst kommen (1) `2g-probe-delay=yes` und # (2) `ft-over-ds=no`, jeder einzeln und als Transaktion, mit dem seit 2026-08-30 # laufenden wireless-Logging als Messgrundlage. Siehe teilprojekte.md TP25 Fund E. # 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="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.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 # [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 # [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=` 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. # Die Liste kommt seit 2026-08-30 aus `ap.extra_ports` statt hartkodiert: ap1/ap2 # sind hAP ax2 mit ether3/4/5, ap3 ist ein cAP ax mit nur zwei Ports (Liste leer). # Die alte Hartkodierung war die stille Annahme "AP == hAP ax2". {% for p in ap.extra_ports %} /interface bridge port add bridge={{ ap.bridge }} comment=defconf interface={{ p }} {% endfor %} {% 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 }} {% if ap.container %} # [7] CONTAINER-NETZ des cloudflared-Connectors (TP16.3 ยง5, ins Soll gehoben 2026-08-31) # Bis hierher standen diese Zeilen per DIREKT-WRITE am Geraet (Runbook ยง3.4) und # erzeugten je AP zwei extra-Drifts: Runbook und GC-Governance widersprachen sich, # /ip address und /ip firewall stehen unter Drift-Kontrolle. Sie gehoeren ins Soll # und NICHT in eine gc-transaktion: eine Transaktion ist ein befristeter Versuch, # das hier ein Dauerzustand. # # ๐Ÿ”ด Das masquerade ist Pflicht, nicht Kosmetik. Der Container spricht aus 172.17.0.2; # dem Core ist 172.17.0.0/24 unbekannt, es gibt weder Route noch Rueckweg. Erst # hinter dem masquerade kommt der Verkehr als vlan99 an und wird von der Freigabe # "TP31: AP-cloudflared -> caddy-bf1" (vlan99 -> vlan20:443) getragen. # # ๐Ÿ”‘ <> steht hier NICHT fuer ein Geheimnis, sondern fuer zwei # geraetevergebene MACs, die auf jedem AP anders lauten und in Nautobot bewusst # nicht modelliert sind (gleiche Entscheidung wie bei den VAP-MACs, s. [1]). # Die ConfigReplace-Regeln 60-/61-veth-* normalisieren sie im Backup # (nautobot/ingest-veth-rule.py). Zweiter, willkommener Effekt: die Maske ist die # Push-Sperre aus TP22-Regel 1 -- eine Zeile mac-address=<<...>> darf nie aufs Geraet. # # Ein kuenftiger AP ohne Connector setzt "container": {} per local_config_context_data; # dann rendert dieser Block nichts und die Regel interface-veth vergleicht leer gegen leer. {% set c = ap.container %} /interface veth add address={{ c.veth_address }} container-mac-address=<> dhcp=no gateway={{ c.gateway }} gateway6="" mac-address=<> name={{ c.veth }} /ip address add address={{ c.ap_address }} interface={{ c.veth }} network={{ c.network }} /ip firewall nat add action=masquerade chain=srcnat comment="{{ c.nat_comment }}" src-address={{ c.subnet }} {% endif %} {% endif %}