6 Commits

Author SHA1 Message Date
e87619e89b TP38: bridge-port comment ungequotet + core place-before->move
RouterOS exportiert comment=TP38-OOB-MGMT ohne Quotes (kein Leerzeichen),
das Template hatte sie gesetzt -> bridge-port Drift. Fix: unquoten.

Der Deploy-`place-before=[find ...]` wurde von Netmiko ignoriert (Regel
landete hinter dem drop); im Job jetzt add + move.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-15 19:33:22 +02:00
177ba100eb TP38: OOB-MGMT-Port (ether23 untagged VLAN99) + Branch-Input-Firewall
Template mikrotik_routeros.j2: ether23 auf core+branch als OOB-Port
(pvid=99, admit-only-untagged-and-priority-tagged) + VLAN99 untagged=ether23.
Branch bekommt einen deny-by-default-Input-Filter (8 Regeln, src-Rahmen
spiegelt /ip service available-from), Core eine Regel "OOB->core mgmt".

Neue Skripte: ingest-tp38-oob.py (seedet firewall_filter in beiden
local_config_context_data) und der gc-configplan.py-Job tp38-oob (Deploy).
ConfigPlans angelegt+approved, Deploy steht noch aus.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-15 19:23:59 +02:00
bce40d182d TP25: Handoff Steering-Threshold + Terrasse-Verifikation
st-main-5g deployed (140/140), Terrasse am iPhone verifiziert (2.4G statt 5G/-80).
MacBook-Klebrigkeit dokumentiert, transition-time=immediate wird nicht umgesetzt.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-15 18:40:22 +02:00
1d86983f63 TP25: st-main-5g Parameter-Reihenfolge gefixt + Commands-File
RouterOS exportiert transition-threshold vor wnm; Template-Reihenfolge ans
Export-Format angepasst. Deploy über manuellen Plan (Plan cc8340a8) — 140/140.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-15 18:30:24 +02:00
27effe937e TP25: AP-Zuordnung definitiv + Doku-Drift geschlossen
ap1=Sideboard/core ether3, ap2=Technik UG/core ether21, ap3=Technik OG/branch
ether24 (live per IP+MAC+Switch-Port). ap2 ether1 = :39. Nautobot Location
gesetzt (vorher None). Kanäle ap1/ap2 mitkorrigiert.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-15 18:27:19 +02:00
2cf32bde14 TP25: Steering 5G bricht früher ab (st-main-5g, -72/5s)
Terrasse saß am Rundgang 09-15 auf der -80/5G-Kante. transition-threshold
-80→-72, transition-threshold-time 10s→5s — nur für 5G; 2,4G bleibt auf
st-main (unset -80) für Reichweite draußen.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-15 18:27:16 +02:00
7 changed files with 287 additions and 13 deletions

View File

@@ -104,11 +104,11 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`,
|---|---|---|
| ~~ALLNET Core-Switch (sw1)~~ | — | ❌ **außer Betrieb (2026-08-08)** — durch `crs326-core` (L3-Router + Core-Switch, Technik UG) ersetzt; physisch abgelöst. (Telnet-Runner `switch/` = nur noch historisch.) |
| ~~Omada Access-Switch (sw2)~~ | — | ❌ **abgebaut 2026-07-23** (TP15 Branch-Tausch) — durch `crs326-branch` in Technik OG ersetzt, physisch entfernt |
| **crs326-branch** (CRS326, Technik OG) | **10.40.99.2** (VLAN99) · Bootstrap Rev4 2026-08-08 | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.2/rest` · MikroMCP `crs326-branch` ✅ **live** (2026-08-16 verifiziert) · **TP15 ✅: ersetzt sw2** (L2-Switch), volle Portbelegung (ether1 Uplink-Trunk, 2/8 NAS, 3 Drucker, 4 Dock, 5/6 Sonos, 7 macmini, 24 ap2) · sw-id RGZJ-UK0T, MAC 04:F4:1C:B4:15:58 |
| **crs326-branch** (CRS326, Technik OG) | **10.40.99.2** (VLAN99) · Bootstrap Rev4 2026-08-08 | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.2/rest` · MikroMCP `crs326-branch` ✅ **live** (2026-08-16 verifiziert) · **TP15 ✅: ersetzt sw2** (L2-Switch), volle Portbelegung (ether1 Uplink-Trunk, 2/8 NAS, 3 Drucker, 4 Dock, 5/6 Sonos, 7 macmini, 24 ap3) · sw-id RGZJ-UK0T, MAC 04:F4:1C:B4:15:58 |
| **crs326-core** (CRS326, Technik UG) | **10.40.99.1** (VLAN99-SVI = L3-GW; same-subnet-Fallback `10.40.10.1`) · Identity `crs326-core` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_CORE_*`) · MikroMCP routerId `crs326-core` ✅ **live** (2026-08-16 verifiziert) · **TP15/TP21.1 ✅ 2026-08-08: PRODUKTIVER L3-Router** (ersetzt ap1) — Routing/DHCP (7 Server + 54 Leases)/DNS (**DoH zum CF-Gateway mit `verify-doh-cert=yes`**, TP23 Stufe 1 ✅ 2026-08-18; `servers=1.1.1.1` nur noch Bootstrap) · **NTP-Server für den Standort**/NAT/WG-Hub (fm+dw live); WAN `vlan100`=Liwest `90.146.33.177` · sw-id 4W04-URHT, MAC 04:F4:1C:B4:17:76 |
| hAP ax² #1 (ap1) | **10.40.99.11** (VLAN99) · Identity `ap1` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP1_*`) · REST `http://10.40.99.11/rest` · MikroMCP `ap1` ✅ live · **TP28 ✅ 2026-08-17: reiner CAP** (Manager = `crs326-core`) · 🔴 **FUNKSTILL seit 2026-08-30**: keine Provisioning-Regel am Manager, weil ap3 daneben steht und denselben Kanal 5180 fährt. Gerät bleibt erreichbar. Beim Umzug ans Sideboard → `5260/Ce` (Band B, DFS) und Uplink auf `core ether3` — Uplink `ether1` (PoE-Port, `poe-out=off`) an crs326-core `ether15`; SSIDs/Kanäle kommen vom Manager, ch36/ch1 · MAC D0:EA:11:4D:40:E0 · Config-Referenz `routeros/.secrets/ap1-prov-base.rsc` |
| hAP ax² #2 (ap2) | **10.40.99.12** (VLAN99) · Identity `ap2` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP2_*`) · REST `http://10.40.99.12/rest` · MikroMCP `ap2` · **TP28 ✅ 2026-08-17: reiner CAP** — Uplink `ether1` (PoE) an crs326-branch `ether24`; ch44/ch11 (TP25: ch149 war unter ETSI leistungsbegrenzt) · MAC D0:EA:11:4D:40:3A · Config `routeros/.secrets/ap2-prov-base.rsc` |
| **cAP ax (ap3)** | **10.40.99.13** (VLAN99) · Identity `ap3` | ✅ **LIVE seit 2026-08-30** (TP28-Nachtrag) · `cAPGi-5HaxD2HaxD`, RouterOS **7.24.1** + RouterBOOT 7.24.1 (**seit 2026-08-31 ist die ganze Flotte auf 7.24.1**, TP27-Angleichung erledigt) · reiner CAP, Manager `crs326-core` · Uplink `ether1` → **core `ether21`** (Trunk) · Kanäle **`5180/ax/Ce/I`** (5 GHz) + **`2437/ax`** (ch6) nach TP25-Plan · 🔴 **nur 2 Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out, **kein** Bridge-Port) → `ap.extra_ports=[]` · `device-mode=basic` (scheduler/fetch/romon **erlaubt**, anders als ap1/ap2) · Dateien `routeros/tp28-ap3-phase{1,2}-*.rsc` + `-service-accounts.rsc` + `-werksreste.rsc` |
| hAP ax² #1 (ap1) | **10.40.99.11** (VLAN99) · Identity `ap1` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP1_*`) · REST `http://10.40.99.11/rest` · MikroMCP `ap1` ✅ live · **TP28 ✅ 2026-08-17: reiner CAP** (Manager = `crs326-core`) · ✅ **aktiv am Sideboard** (Wohnzimmer; Uplink `ether1` → core `ether3`, PoE-Port `poe-out=off`) — Kanäle `5260/Ce` (Band B, DFS, Fallback 5280) + `2412`/ch1, vom Manager · MAC D0:EA:11:4D:40:E0 · Config-Referenz `routeros/.secrets/ap1-prov-base.rsc` |
| hAP ax² #2 (ap2) | **10.40.99.12** (VLAN99) · Identity `ap2` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP2_*`) · REST `http://10.40.99.12/rest` · MikroMCP `ap2` · **TP28 ✅ 2026-08-17: reiner CAP** — **Technik UG**, Uplink `ether1` (PoE) an core `ether21`; Kanäle `5300/Ce` (Band B, DFS, Fallback 5320) + `2462`/ch11 · MAC D0:EA:11:4D:40:39 (ether1; Bridge/Mgmt `:3A`) · Config `routeros/.secrets/ap2-prov-base.rsc` |
| **cAP ax (ap3)** | **10.40.99.13** (VLAN99) · Identity `ap3` | ✅ **LIVE seit 2026-08-30** (TP28-Nachtrag) · `cAPGi-5HaxD2HaxD`, RouterOS **7.24.1** + RouterBOOT 7.24.1 (**seit 2026-08-31 ist die ganze Flotte auf 7.24.1**, TP27-Angleichung erledigt) · reiner CAP, Manager `crs326-core` · **Technik OG**, Uplink `ether1` → **branch `ether24`** (Trunk) · Kanäle **`5180/ax`** (Band A, non-DFS, 5 GHz) + **`2437`** (ch6) nach TP25-Plan · 🔴 **nur 2 Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out, **kein** Bridge-Port) → `ap.extra_ports=[]` · `device-mode=basic` (scheduler/fetch/romon **erlaubt**, anders als ap1/ap2) · Dateien `routeros/tp28-ap3-phase{1,2}-*.rsc` + `-service-accounts.rsc` + `-werksreste.rsc` |
| **🔑 Einheitliches Admin-PW (2026-08-08)** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). Werks-Defaults ap1/ap2 damit obsolet. |
| NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) |
| platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅ 2026-07-16: PG16, DB `nautobot`, Creds `postgres/.secrets`** |
@@ -282,7 +282,7 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`,
danach die Ports gegen vorher vergleichen**, sonst verliert man still einen Dienst.
- **VLAN20-Docker-Hosts: Host-DNS nach dem Umzug prüfen (2026-07-10, TP14.2a):** NAS + pve-LXC zeigten `/etc/resolv.conf` noch auf totes `.41.254` → `docker pull` ghcr scheitert (`EAI_AGAIN`, aber Egress ok!). Fix = `.20.1`: LXC `pct set 400 --nameserver 10.40.20.1`; **Synology-Falle: DSM-„Allgemein"-DNS ≠ was `dockerd` liest** — Interface zieht DNS per DHCP → Fix an der *Netzwerkschnittstelle* „DNS manuell". Laufende Container frieren den `127.0.0.11`-Upstream zum Erstellzeitpunkt ein (Recreate nötig). Merkregel: `docker-pull-hängt` + `dns.lookup EAI_AGAIN` aber `TCP zu ghcr-IP:443 ok` = toter Resolver, nicht Egress.
- 📜 **~~ALLNET VLAN-DB (TP1)~~ — historisch.** Die VLAN-Semantik gilt weiter (**40=MEDIA, 50=UNTRUSTED**), die Portangaben (GE1/GE2/GE8) beziehen sich auf ein ausgebautes Gerät.
- **ap2 hängt an `crs326-branch ether24`** (Trunk, native VLAN1 + tagged 10-99); **branch-Uplink `ether1` → `crs326-core ether8`** (korrigiert 2026-08-16 — die frühere Angabe „→ sw1 GE8" stammt aus der Zeit vor dem Core-Tausch; MAC-hart belegt in TP22 Schritt 5).
- **ap3 hängt an `crs326-branch ether24`** (Trunk, native VLAN1 + tagged 10-99); **branch-Uplink `ether1` → `crs326-core ether8`** (korrigiert 2026-08-16 — die frühere Angabe „→ sw1 GE8" stammt aus der Zeit vor dem Core-Tausch; MAC-hart belegt in TP22 Schritt 5). ap2 hängt seit dem TP25-Tausch (2026-09-15) an `core ether21` (Technik UG), ap1 an `core ether3` (Sideboard).
- **Mesh = NEIN** (beide APs haben wired Trunk-Uplink → Loop-Risiko).
- **Beide APs = CAPs, Manager ist `crs326-core` (TP28 ✅ 2026-08-17).** Der Entscheid „kein CAPsMAN" von 2026-07-22 ist **revidiert**: er galt der Konstellation ap1=Manager+AP, in der ein Manager seine eigenen Radios nicht provisionieren kann — mit dem Core als Manager sind beide APs symmetrisch. Der Core braucht dafür **kein** `wifi-qcom`, `capsman` steckt im Basispaket.
- 🔴 **`/interface wifi scan` geht am CAP NICHT — nur am Manager** (Fund 2026-08-23). Das Gerät

View File

@@ -0,0 +1,72 @@
# Handoff 2026-09-15 — TP25: Steering-Threshold 5G deployed, Terrasse verifiziert, MacBook-Klebrigkeit „wird nicht umgesetzt"
**Anlass:** Folgesession zum TP25-Handoff „zuordnung-und-redundanz" ([[handoff-20260915-tp25-zuordnung-und-redundanz]]). Ziele: (1) Doku-Drift schließen, (2) Roaming-Threshold für die Terrasse (5G bricht früher zur 2,4G ab), (3) Terrasse-Nachmessung nach dem Deploy.
## 1. Steering-Threshold 5G — DEPLOYED, 140/140
**Problem:** Am Rundgang 09-15 saß der Terrasse-Client auf der **−80/5G-Kante** (genau der Default
`transition-threshold`); auf 2,4 GHz wäre das nicht passiert.
**Umsetzung (nur 5G):** neues Steering-Profil `st-main-5g` mit `transition-threshold=-72`
(statt Default −80) und `transition-threshold-time=5s` (statt 10s). Die drei 5G-Configs
(`cfg-main-5g-ap1/ap2/ap3`) zeigen darauf; die drei 2G-Configs bleiben auf `st-main` (unset −80),
weil 2,4 GHz draußen (Terrasse/außerhalb) Reichweite behalten soll. **8 dB Abstand = kleine Hysterese.**
Weg: Template-Edit (`nautobot/mikrotik_routeros.j2`) → `push-gc-template.py` (GC-Commits `6a4b504f`,
`242ed108`) → manueller Plan (`gc-manual-plan.py`, Plan `cc8340a8`, Completed) → Refresh:
**140/140, 0 Drifts.** Commands-File:
[`docs/transaktionen/TP25-steering-5g-threshold.commands.rsc`](transaktionen/TP25-steering-5g-threshold.commands.rsc).
## 2. Gotcha: Parameter-Reihenfolge im Template
Nach dem ersten Push meldete Compliance **139/140** mit einem `wifi-steering`-Drift: `extra`/`missing`
derselben Zeile, aber mit unterschiedlicher Feld-Reihenfolge. **RouterOS exportiert `transition-threshold`
vor `wnm` (alphabetisch)** — mein Template hatte `wnm` davor. Fix = Template-Zeile auf die Export-Reihenfolge
gebracht (`… rrm=yes transition-threshold=-72 transition-threshold-time=5s wnm=yes`). Kein Re-Deploy nötig
(das Gerät war schon korrekt, nur das Template war es nicht). **Merkregel: bei neuen Parametern im
`/interface wifi steering add` die RouterOS-Export-Reihenfolge (alphabetisch) verwenden, sonst Drift ohne
Fehlfunktion.**
## 3. Terrasse-Nachmessung — Threshold wirkt am iPhone, nicht am MacBook
Zwei Apple-Geräte 20 cm auseinander auf der Terrasse, beide auf `dirtyvader-main`, beide physisch
rein/raus bewegt (A/B):
| Gerät | Band | Signal | Roam-Verhalten |
|---|---|---|---|
| iPhone `F2:62:4C:E1:AF:09` | **2,4 GHz** | −55…−66 | ✅ roamt sauber (rein→5G, raus→2,4G) |
| MacBook `56:71:6C:22:E6:A9` | **5 GHz** | −73…−75 | ❌ kein Roam (uptime 1h15m unverändert) |
**Befund:** gleiche Bewegung, gegensätzliches Verhalten. iOS folgt dem Band-Wechsel; **macOS hält 5 GHz
klebrig fest** und ignoriert die 802.11v-Transition-Request — selbst unter der −72-Schwelle und beim
physischen Umhertragen. Christoph: das MacBook hatte an diesem Ort nie Probleme (−73/5G reicht ihm).
## 4. Entscheidung: MacBook-Klebrigkeit = ❌ WIRD NICHT UMGESETZT
Der einzige Hebel dagegen wäre `transition-time=immediate` (deauth-Erzwingung nach der
Transition-Request statt bloßem Vorschlag) — ein schärferer Eingriff mit Abbruch-Risiko für aktive
Sitzungen. **Christoph-Entscheid 2026-09-15: nicht umsetzen.** Kein Handlungsbedarf, da das MacBook
dort funktioniert. Nur dokumentiert, falls es später doch wieder aufkommt.
## 5. Doku-Drift geschlossen (diese Session)
- **CLAUDE.md:** ap1 „FUNKSTILL"→aktiv am Sideboard (`core ether3`), ap2→Technik UG (`core ether21`,
ether1-MAC `:39`), ap3→Technik OG (`branch ether24`), Branch-Portliste `24 ap3`, Gotcha-Zeile
ap2/ap3-Uplink. Kanäle mitkorrigiert (ap1 `5260/Ce`, ap2 `5300/Ce`).
- **teilprojekte.md:** neuer Stand-Block 2026-09-15 (definitive Zuordnung + Tausch-Befund ap2↔ap3).
- **Nautobot Location** (war `None`): ap1→`Wohnzimmer Sideboard`, ap2→`Technik UG`, ap3→`Technik OG`.
## 6. Handoff-Punkte (aus dem Vorgänger) — aufgelöst
1. **ap1 behalten/abbauen** → **behalten** (Christoph). APs bleiben an ihren jetzigen Orten.
2. **Tausch korrigieren** (cAP ax → Sideboard) → **verworfen**; aktueller Zustand bleibt.
3. **Drei-Räume-Schatten** nachmessen → **verworfen**; Signal war beim letzten Rundgang gut.
4. **Doku-Drift** → ✅ geschlossen (s. §5).
## Übergabeprompt (nächste Session)
> TP25: Steering-Threshold 5G deployed (st-main-5g, −72/5s, 140/140), Terrasse am iPhone verifiziert
> (2,4G statt 5G/−80). MacBook-Klebrigkeit auf 5G dokumentiert und per Christoph-Entscheid
> „wird nicht umgesetzt" (transition-time=immediate). Doku-Drift geschlossen, Handoff-Punkte aufgelöst
> (ap1 bleibt, APs bleiben, Drei-Räume verworfen). Siehe
> [[handoff-20260915-tp25-steering-threshold-und-terrasse]].

View File

@@ -1108,6 +1108,8 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au
> Merkregeln.** Wer an TP25 weiterarbeitet, kommt mit diesem Abschnitt aus; wer wissen will
> *warum* etwas so entschieden wurde, liest die Langfassung.
**Stand 2026-09-15 — AP-Zuordnung DEFINITIV geklärt, Tausch-Befund ap2↔ap3** ([Handoff](handoff-20260915-tp25-zuordnung-und-redundanz.md)). Positionen per IP+MAC+Switch-Port (MNDP/Bridge-Host/Port-Link) live belegt: **ap1 = Sideboard/Wohnzimmer (`core ether3`), ap2 = Technik UG (`core ether21`), ap3 = Technik OG (`branch ether24`)**. Der geplante Tausch (ap1↔ap3, cAP ax → Sideboard) hat faktisch **ap2↔ap3** gedreht — ap1 blieb stehen, der cAP ax steht jetzt in OG. MAC-Korrektur: ap2 `ether1` ist `:39`, nicht `:3A` (`:3A` = Bridge/Mgmt-MAC). Rundgang (natürlicher Zustand, 1-s-Takt): **0 Disconnects**, −45…−77 dBm, Roaming über alle 3 APs; **Terrasse −58 stabil** (jetzt cAP ax/OG, 2,4G), ap1 nur kurz gestreift. ap1-Aus-Test: Main-Client voll abgedeckt (tiefster Wert −80), **alle 27 IOT kehren zurück**, aber ~4 Wohnzimmer-IOT marginal −77…−80 ⇒ ap1 technisch redundant, nicht ausfallkritisch. ap1 wieder an (Revert). Offen: (1) ap1 behalten/abbauen, (2) Tausch auf Plan korrigieren (cAP ax → Sideboard?), (3) Drei-Räume-Schatten (Schlafzimmer/Bad/Küche) gezielt nachmessen — noch nicht verifiziert, (4) Doku-Drift (diese Session).
**Stand 2026-09-14, 20:05 — ap2-In-situ: Hardware entlastet, Terrasse = 5-GHz-Zellkante,
Band-Boden nicht sauber ausdrückbar** ([Handoff](handoff-20260914-tp25-ap2-insitu-terrassse.md)).
Live-Session auf der Terrasse (Christoph kooperierte physisch — der Test, der am 08-31 „nachts

View File

@@ -0,0 +1,10 @@
# TP25 — 5G bricht frueher zur 2.4G ab (Terrasse sass am Rundgang 09-15 auf der -80/5G-Kante)
# Go: Christoph 2026-09-15 ("committen und threshold deployen")
# Nur 5G: neues Steering-Profil st-main-5g mit transition-threshold=-72 (statt Default -80) und
# transition-threshold-time=5s (statt 10s). 2G bleibt auf st-main (unset -80) — Reichweite draussen.
# Nur am Manager noetig: steering/config werden per CAPsMAN an die CAPs verteilt.
# Rollback = steering der drei Configs zurueck auf st-main + st-main-5g loeschen.
/interface wifi steering add name=st-main-5g neighbor-group=bf-main rrm=yes wnm=yes transition-threshold=-72 transition-threshold-time=5s
/interface wifi configuration set [find name=cfg-main-5g-ap1] steering=st-main-5g
/interface wifi configuration set [find name=cfg-main-5g-ap2] steering=st-main-5g
/interface wifi configuration set [find name=cfg-main-5g-ap3] steering=st-main-5g

View File

@@ -353,10 +353,76 @@ def job_tp42_dhcp_domain():
return [(["crs326-core"], core_set, "TP42-dhcp-domain-bf")]
def job_tp38_oob():
"""TP38 (2026-09-15): OOB-MGMT-Port (ether23 untagged VLAN99) + Branch-Input-Firewall.
Zwei Geraete, zwei config_sets:
* branch: ether23 untaggen, VLAN99 um untagged=ether23 erweitern, 8 Input-Regeln
(deny-by-default). Die Regeln werden in der Reihenfolge angelegt, in der das
Template sie rendert — established zuerst, drop zuletzt — damit kein LOCKOUT:
die laufende Nautobot-SSH-Session (10.40.20.30) matcht auf Regel 1 (established),
bevor Regel 8 (drop) greift.
* core: ether23 untaggen, VLAN99 untagged=ether23, EINE Regel "OOB->core mgmt" mit
place-before vor die Log-Regel (sonst landet sie hinter dem finalen drop und
waere wirkungslos + non-compliant).
KEIN LOCKOUT-RISIKO: ether23 ist auf beiden Geraeten frei (running=false); der
VLAN99-Umbau aendert nur `untagged`, nicht `tagged`; der Deploy laeuft von Nautobot
(10.40.20.30), das in jeder gesetzten Firewall-Freigabe enthalten bleibt.
"""
bridge_set = (
':do { /interface bridge port set [find interface=ether23] pvid=99 '
'frame-types=admit-only-untagged-and-priority-tagged comment="TP38-OOB-MGMT" } '
'on-error={ :log info "tp38: ether23 set geworfen" }'
)
vlan_set = (
':do { /interface bridge vlan set [find vlan-ids=99] untagged=ether23 } '
'on-error={ :log info "tp38: vlan99 untagged geworfen" }'
)
branch_fw = "\n".join([
'add action=accept chain=input comment="TP38: established" connection-state=established,related,untracked',
'add action=drop chain=input comment="TP38: invalid" connection-state=invalid',
'add action=accept chain=input comment="TP38: icmp" protocol=icmp',
'add action=accept chain=input comment="TP38: mndp" dst-port=5678 protocol=udp',
'add action=accept chain=input comment="TP38: mgmt LAN" dst-port=22,80,8291,8728 protocol=tcp src-address=10.40.0.0/16',
'add action=accept chain=input comment="TP38: mgmt mesh" dst-port=22,80,8291,8728 protocol=tcp src-address=100.96.0.0/12',
'add action=log chain=input comment="TP38: input log-only" log-prefix=TP38-BRANCH-WOULD-DROP',
'add action=drop chain=input comment="TP38: input deny-by-default"',
])
branch_set = "\n".join([
bridge_set,
vlan_set,
*[f"/ip firewall filter {ln}" for ln in branch_fw.splitlines()],
':log info ("tp38: branch pvid=" . [:tostr [/interface bridge port get [find interface=ether23] pvid]]'
' . " fw=" . [:tostr [:len [/ip firewall filter find where comment~"TP38"]]] . "/8")',
])
# ⚠️ `add ... place-before=[find ...]` wird ueber Netmiko IGNORIERT (Regel landet am
# Ketten-Ende, also HINTER dem finalen drop = wirkungslos; beim Deploy 2026-09-15 so
# aufgeschlagen). Deshalb: erst add (appends), dann `move` vor die Log-Regel.
core_set = "\n".join([
bridge_set,
vlan_set,
'/ip firewall filter add action=accept chain=input comment="TP38: OOB->core mgmt" '
'dst-port=22,80,8291,8728 in-interface=vlan99 protocol=tcp',
'/ip firewall filter move [find where comment="TP38: OOB->core mgmt"] '
'destination=[find where comment="TP5: input log-only"]',
':log info ("tp38: core pvid=" . [:tostr [/interface bridge port get [find interface=ether23] pvid]]'
' . " oob=" . [:tostr [:len [/ip firewall filter find where comment="TP38: OOB->core mgmt"]]])',
])
return [
(["crs326-branch"], branch_set, "TP38-oob-branch"),
(["crs326-core"], core_set, "TP38-oob-core"),
]
JOBS = {
"drift-cleanup": job_drift_cleanup,
"mgmt-acl": job_mgmt_acl,
"tp42-dhcp-domain": job_tp42_dhcp_domain,
"tp38-oob": job_tp38_oob,
}

105
nautobot/ingest-tp38-oob.py Normal file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
r"""TP38: OOB-Port + Input-Firewall als Daten nach Nautobot schreiben.
Zwei getrennte Aenderungen, ein Skript:
1. BRANCH (crs326-branch): `firewall_filter` im local_config_context_data ANLEGEN
(8 Input-Regeln, deny-by-default). Der src-Rahmen 10.40.0.0/16 + 100.96.0.0/12
spiegelt bewusst das `available-from` der /ip service-Zeilen — kein neues
Fenster, nur stateful + deny-by-default + WOULD-DROP-Log.
2. CORE (crs326-core): EINE Regel "TP38: OOB->core mgmt" in die bestehende
firewall_filter-Liste EINFUEGEN (vor log/drop), damit der OOB-Port (untagged
ether23, VLAN99) auch den Core selbst verwalten kann.
Attributreihenfolge = alphabetisch, exakt wie RouterOS `/export` sie schreibt
(gleiche Konvention wie ingest-core-firewall.py).
python3 nautobot/ingest-tp38-oob.py # Dry-Run
python3 nautobot/ingest-tp38-oob.py --apply
"""
import json
import os
import sys
import urllib.request
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
BRANCH_RULES = [
'add action=accept chain=input comment="TP38: established" connection-state=established,related,untracked',
'add action=drop chain=input comment="TP38: invalid" connection-state=invalid',
'add action=accept chain=input comment="TP38: icmp" protocol=icmp',
'add action=accept chain=input comment="TP38: mndp" dst-port=5678 protocol=udp',
'add action=accept chain=input comment="TP38: mgmt LAN" dst-port=22,80,8291,8728 protocol=tcp src-address=10.40.0.0/16',
'add action=accept chain=input comment="TP38: mgmt mesh" dst-port=22,80,8291,8728 protocol=tcp src-address=100.96.0.0/12',
'add action=log chain=input comment="TP38: input log-only" log-prefix=TP38-BRANCH-WOULD-DROP',
'add action=drop chain=input comment="TP38: input deny-by-default"',
]
CORE_NEW_RULE = 'add action=accept chain=input comment="TP38: OOB->core mgmt" dst-port=22,80,8291,8728 in-interface=vlan99 protocol=tcp'
def nb(path, method="GET", payload=None):
tok = os.environ.get("NAUTOBOT_TOKEN") or sys.exit("NAUTOBOT_TOKEN nicht gesetzt")
req = urllib.request.Request(
f"{NB}{path}", method=method,
data=json.dumps(payload).encode() if payload is not None else None,
headers={"Authorization": f"Token {tok}", "Content-Type": "application/json",
"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def get_device(name):
return nb(f"/api/dcim/devices/?name={name}&depth=1")["results"][0]
def patch_device(dev, ctx, label):
dev_id = dev["id"]
nb(f"/api/dcim/devices/{dev_id}/", "PATCH", {"local_config_context_data": ctx})
print(f" ~ {label} geschrieben.")
def main():
apply_ = "--apply" in sys.argv
# --- BRANCH ---
branch = get_device("crs326-branch")
bctx = dict(branch.get("local_config_context_data") or {})
if bctx.get("firewall_filter") == BRANCH_RULES:
print("== branch: firewall_filter unveraendert ==")
else:
bctx["firewall_filter"] = BRANCH_RULES
print(f"== branch: {len(BRANCH_RULES)} Input-Regeln ==")
for ln in BRANCH_RULES:
print(" " + ln)
if apply_:
patch_device(branch, bctx, "branch firewall_filter")
# --- CORE ---
core = get_device("crs326-core")
cctx = dict(core.get("local_config_context_data") or {})
ff = list(cctx.get("firewall_filter") or [])
if CORE_NEW_RULE in ff:
print("\n== core: Regel schon vorhanden ==")
else:
# vor der Log-Regel einsortieren (die sitzt direkt vor dem finalen drop).
insert_at = len(ff)
for i, ln in enumerate(ff):
if "log-prefix=TP5-INPUT-WOULD-DROP" in ln:
insert_at = i
break
ff.insert(insert_at, CORE_NEW_RULE)
cctx["firewall_filter"] = ff
print(f"\n== core: fuege OOB-Regel an Position {insert_at} ein ==")
print(" " + CORE_NEW_RULE)
if apply_:
patch_device(core, cctx, "core firewall_filter")
if not apply_:
print("\nDRY-RUN — mit --apply schreiben. Danach: "
"push-gc-template.py + gc-configplan.py --refresh-compliance")
if __name__ == "__main__":
main()

View File

@@ -73,9 +73,11 @@
/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) %}
{% for n in range(17, 23) %}
/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }}
{% endfor %}
/interface bridge port add bridge=bridge comment=TP38-OOB-MGMT frame-types=admit-only-untagged-and-priority-tagged interface=ether23 pvid=99
/interface bridge port add bridge=bridge comment=defconf interface=ether24
/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).
@@ -94,7 +96,7 @@
/interface bridge vlan add bridge=bridge tagged=ether1,ether3,ether8,ether13,ether15,ether21 untagged=ether2,ether4,ether5 vlan-ids=40
/interface bridge vlan add bridge=bridge tagged=ether1,ether3,ether8,ether15,ether21 vlan-ids=50
/interface bridge vlan add bridge=bridge tagged=ether1,ether3,ether8,ether15,ether21 vlan-ids=90
/interface bridge vlan add bridge=bridge tagged=ether3,ether8,ether15,ether21,bridge vlan-ids=99
/interface bridge vlan add bridge=bridge tagged=ether3,ether8,ether15,ether21,bridge untagged=ether23 vlan-ids=99
/interface bridge vlan add bridge=bridge tagged=ether1 untagged=ether16 vlan-ids=100
# [1] INTERFACE-LISTS
@@ -241,11 +243,16 @@
# 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
# st-main-5g (TP25, 2026-09-15): transition-threshold=-72 statt Default -80 -- 5G bricht frueher
# zur 2.4G ab (Terrasse sass am Rundgang 09-15 auf der -80/5G-Kante). transition-threshold-time=5s
# statt 10s. NUR 5G: die drei 2G-Configs bleiben auf st-main (unset=-80), weil 2.4G draussen
# (Terrasse/ausserhalb der Wohnung) Reichweite behalten soll. 8 dB Abstand = kleine Hysterese.
/interface wifi steering add name=st-main-5g neighbor-group=bf-main rrm=yes transition-threshold=-72 transition-threshold-time=5s 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-5g
/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-5g
/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-5g-ap3 country=Austria datapath=dp-main mode=ap name=cfg-main-5g-ap3 security=sec-main ssid=dirtyvader-main steering=st-main-5g
/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
@@ -417,9 +424,20 @@
/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
# INPUT-FIREWALL (TP38, 2026-09-15) — deny-by-default fuer die Mgmt-Ebene (10.40.99.2).
# Der Branch ist L2-only: ALLE Mgmt-Anfragen kommen auf der einen vlan99-SVI an, egal ob
# VLAN10 (ueber den Core geroutet), cf-mesh (100.96.0.0/12) oder der OOB-Port (untagged
# ether23, 10.40.99.x). Deshalb matcht die Quelle per src-address, nicht per in-interface.
# Der src-Rahmen 10.40.0.0/16 + 100.96.0.0/12 spiegelt bewusst das `available-from` der
# /ip service-Zeilen — kein neues Fenster, nur stateful + deny-by-default + WOULD-DROP-Log.
{% for line in obj.get_config_context().get("firewall_filter", []) %}
/ip firewall filter {{ line }}
{% endfor %}
# L2-FABRIC — wie beim Core ein Literal-Block (Begruendung dort).
# ether9-23 tragen pvid=20 OHNE VLAN-Membership (Bootstrap-Rev4-Rest, funktionslos) —
# ether9-22 tragen pvid=20 OHNE VLAN-Membership (Bootstrap-Rev4-Rest, funktionslos) —
# bewusst so abgebildet, damit eine spaetere Bereinigung als Drift sichtbar wird.
# ether23 ist seit TP38 der OOB-MGMT-Port (untagged VLAN99), s. unten.
/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
@@ -428,9 +446,10 @@
/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) %}
{% for n in range(9, 23) %}
/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }} pvid=20
{% endfor %}
/interface bridge port add bridge=bridge comment=TP38-OOB-MGMT frame-types=admit-only-untagged-and-priority-tagged interface=ether23 pvid=99
/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
@@ -440,7 +459,7 @@
/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
/interface bridge vlan add bridge=bridge comment=MGMT tagged=ether1,ether24,bridge untagged=ether23 vlan-ids=99
{% elif obj.role.name == "Wireless AP" %}
{% set ap = obj.get_config_context()["ap"] %}
{% set mgmt_host = obj.primary_ip4.host %}