diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index b58d9af..ede17da 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -421,7 +421,16 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - **(b) Live-Torch als Ad-hoc-Observability:** DNS ist bei direktem 1.1.1.1 blind, **Flows nicht** (NATen über ap1). `torch` auf `vlan30`/`vlan50` gefiltert auf die Geräte-IP zeigt Ziel-IPs live (Versuni → AWS `eu-west-1` IoT erkennbar). Voraussetzung: Gerät aktiv. Merkregel: Airfryer nur online wenn er kocht → Capture ans Einschalten koppeln. - **Abh.:** UNTRUSTED-Move des Airfryers (Kandidat, s. VLAN50) + TP5-Deny-by-default. Kein Blocker. -### TP6 — QoS/CAKE + Call-Quality-A/B-Test ▸ ✅ **GESCHLOSSEN 2026-08-23 — gebaut, gemessen, wieder ausgebaut. CAKE wirkt, reicht aber nicht an die Ursache.** +### TP6 — QoS/CAKE + Call-Quality-A/B-Test ▸ ✅ **GESCHLOSSEN 2026-08-23 — CAKE gebaut, gemessen, wieder ausgebaut.** ⚠️ **Die Erklaerung darin ist von TP6.1 ueberholt: nicht DOCSIS, sondern fehlende Klassentrennung.** +> 🔴 **ZUERST LESEN — diese Analyse ist teilweise widerlegt (2026-08-23, durch TP6.1).** +> Unten steht, die ~30 ms entstuenden im DOCSIS-Uplink und seien unabaenderlich. **Sie sind es +> nicht.** TP6.1 holt dieselbe Strecke unter derselben Saettigung auf **11,0 ms** — allein durch +> Klassentrennung mit Prioritaet. Der Fehler lag in der **Methode** hier: Bulk und Messverkehr +> kamen vom **selben Host**, damit gab es fuer CAKEs `triple-isolate` nichts zu trennen, und +> blosses Shapen aendert keine Reihenfolge. **Weiter gueltig bleibt:** dass CAKE als *eine* +> Queue ueber allem nichts brachte (dreimal gemessen), und der Befund zur Queue-Platzierung +> (Interface-HTB tot, Global-HTB wirkt). Der Rest dieses Abschnitts ist als Messprotokoll zu +> lesen, nicht als Ursachenanalyse. > 🔴 **A/B gemessen, gleicher Client, gleiche Methode, Minuten auseinander:** > > | Konfiguration | avg Latenz unter Upload-Last | @@ -549,50 +558,54 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". Ohne diese Zahl ist die übliche CAKE-Vorgabe „85–95 % Linerate" geraten. - **Abh.:** TP5 ✅ (keine Blocker mehr). Vorgelagert: die Messung oben. -### TP6.1 — Saettigung vermeiden statt Bufferbloat bekaempfen (Bulk-Uploads zuruecknehmen) ▸ 🟠 **VORSCHLAG 2026-08-23, aus dem TP6-Abschluss heraus** -> **Der Gedanke kommt von Christoph** („dann koennten wir zumindest den Sync von der NAS -> zurueckhalten“) und er trifft eine Luecke im TP6-Urteil. TP6 hat gezeigt: die ~30 ms -> Zusatzlatenz sind **nicht** wegzushapen, weil sie im DOCSIS-Uplink entstehen. Aber sie -> treten **nur unter Saettigung** auf — im Leerlauf misst dieselbe Strecke **12,9 ms**. +### TP6.1 — Saettigung vermeiden statt Bufferbloat bekaempfen ▸ ✅ **LIVE UND GEMESSEN (2026-08-23): 40,8 → 11,0 ms unter Last** +> **Der Gedanke kam von Christoph** beim TP6-Abschluss („dann koennten wir zumindest den Sync +> von der NAS zurueckhalten“) — und er hat mein TP6-Urteil zu Recht angezweifelt. > -> ⚖️ **Damit ist das Ziel ein anderes: nicht die Latenz unter Last senken, sondern die Last -> gar nicht erst entstehen lassen.** Ein NAS-Sync, der bei laufendem Call zurueckweicht, -> haelt die Leitung aus der Saettigung — und dann liegt der Call bei 12,9 statt 42 ms. Das -> ist die Verbesserung, die CAKE nicht liefern konnte, und sie ist **groesser** als alles, was -> Bufferbloat-Bekaempfung hier je gebracht haette. +> 🟢 **Abnahme, echte Saettigung, Kontrollmessung inklusive:** > -> ✅ **Die Vorarbeit ist erledigt und bewiesen** (TP6): der **Global-HTB-Weg funktioniert** auf -> diesem Geraet — `mangle chain=postrouting out-interface=vlan100 action=mark-packet` plus -> `/queue tree parent=global packet-mark=…`. Gemessen: zaehlt 6 MB, drosselt 9,3 → 7,78 Mbit. -> Der Interface-Weg (`parent=`) ist dagegen tot, s. TP6. +> | gleiche NAS-Last, gleicher Messclient | avg | max | +> |---|---|---| +> | Priorisierung **aus** | **40,8 ms** | 53,4 ms | +> | Priorisierung **an** | **11,0 ms** | 14,6 ms | > -> **Entwurf (zu bauen, nicht gebaut):** ein Parent-Tree mit `max-limit` knapp unter der -> Linerate — nur so muss HTB ueberhaupt entscheiden — und darunter zwei Klassen: -> - `priority=8` **Bulk**: Upload **von der NAS** (`src-address=10.40.20.10`). Bewusst die -> Host-IP, **nicht** ganz VLAN20 — dort haengen auch HA, Nautobot und pve, die nicht -> pauschal hinten anstehen sollen. -> - `priority=1` **Rest**: alles andere, insbesondere Calls. +> **−73 %.** Und die Last war echt, nicht behauptet: `wan-up-total` fuhr **9,0 Mbit** +> (das max-limit greift exakt), `wan-up-bulk` 8,87 Mbit mit **1581 Drops** — der Sync wurde +> aktiv gebremst, waehrend `wan-up-rest` frei durchlief. `vlan100 tx = 9,0 Mbps`. +> Die Kontrolle mit abgeschalteter Markierung war noetig, um auszuschliessen, dass die +> Verbesserung bloss daher kommt, dass Bulk und Ping jetzt von verschiedenen Hosts stammen. > -> 🔴 **Priorisierung, kein hartes Limit.** Ein festes Cap wuerde den Sync auch dann bremsen, -> wenn die Leitung frei ist. Prioritaet nimmt ihm die Bandbreite nur, solange jemand anders -> sie braucht — das ist genau die Bedingung, unter der es hier ueberhaupt weh tut. +> 🔴 **Damit ist die DOCSIS-Erklaerung aus TP6 widerlegt.** Dort stand, die ~30 ms +> entstuenden stromabwaerts im Uplink und seien unabaenderlich. Sie sind es nicht — sie +> loesen sich auf, sobald der Verkehr in **getrennte Klassen** faellt. Der Fehler steckte in +> der TP6-**Methode**: Bulk und Messverkehr kamen vom **selben Host**, also gab es fuer CAKEs +> Host-Fairness (`triple-isolate`) nichts zu trennen — alles lag in derselben Klasse, und +> Shapen allein aendert an einer Reihenfolge nichts. Was CAKE nicht schaffte, schafft +> **explizite Klassentrennung mit Prioritaet**. Merkregel: nicht die Bandbreite ist das +> Problem, sondern **wer vor wem drankommt**. > -> **Preis, ehrlich:** der Parent muss unter die Linerate shapen (~1 Mbit Spitze weniger, -> derselbe Preis wie bei CAKE), und Mangle kostet CPU pro Paket — bei 9,6 Mbit WAN irrelevant. +> **Was gebaut ist** (Template [11a], via GC, 104/104 compliant): +> - zwei `mangle`-Regeln in `postrouting` (**vor** src-nat — nur dort ist die LAN-Quelle noch +> sichtbar): NAS → `bulkup`, alles andere → `restup` +> - `wan-up-total` (`parent=global`, `max-limit=9M`) mit zwei Klassen darunter: +> `wan-up-rest` `priority=1 limit-at=6M` und `wan-up-bulk` `priority=8 limit-at=1M` > -> 🔴 **Governance-Vorbedingung, dieselbe wie bei TP6:** `/ip firewall mangle` steht unter -> **keiner ComplianceRule**. Die Regeln waeren also unsichtbarer Drift. Vor dem Bau eine Rule -> `ip-firewall-mangle` anlegen — Muster liegt mit `nautobot/ingest-queue-rule.py` vor. Die -> Rule `queue` existiert bereits und deckt den Tree-Teil ab. +> **Drei Entscheidungen, die begruendet bleiben sollen:** +> 1. **Prioritaet statt hartem Cap** — ein Limit haette die NAS auch nachts gebremst und +> Bandbreite verschenkt (DSM drosselt von sich aus nicht, User-bestaetigt 2026-08-23). +> `limit-at=1M` sorgt dafuer, dass der Sync nie ganz verhungert. +> 2. **`src-address=10.40.20.10`, nicht VLAN20** — in INFRA haengen auch HA, Nautobot und +> pve. Wer das je auf `10.40.20.0/24` erweitert, verlangsamt seine eigenen Werkzeuge. +> 3. **`parent=global`, nicht `parent=vlan100`** — Interface-HTB ist auf diesem Geraet tot +> (hw-offloadetes out-interface, s. TP6). > -> 🔴 **Abnahme misst den Call, nicht die Queue:** Ping-Latenz waehrend eines echten -> NAS-Syncs, gegen die 12,9 ms idle. Ein Queue-Zaehler beweist hier gar nichts — genau der -> Fehler, der in TP6 eine Nacht gekostet hat (`feedback_abnahme_wirkung_nicht_anzeige`). -> **Vorher pruefen, ob der Fall ueberhaupt auftritt:** laeuft der Synology-Sync tatsaechlich -> ins offene Messer, oder drosselt DSM ihn ohnehin schon? Ohne diesen Nachweis waere TP6.1 -> eine Loesung auf Verdacht — und davon hatte TP6 genug. +> ✅ **Governance-Vorbedingung erfuellt:** ComplianceRule **`ip-firewall-mangle`** angelegt +> (`nautobot/ingest-mangle-rule.py`), **`config_ordered=True`** — die NAS-Regel muss vor dem +> Catch-all stehen, sonst waere die Priorisierung **still** wirkungslos. Zaehler jetzt +> **26 Rules x 4 Geraeten = 104 Checks**. > -> **Abh.:** keine harten. Erbt die Erkenntnisse aus TP6; blockiert nichts. +> 💡 **Offen, klein:** ob weitere Bulk-Quellen dazugehoeren (pve-Backups? Time Machine?). +> Erst messen, ob sie ueberhaupt saettigen — die Regel dafuer ist eine Zeile. ### TP7 — Media-Discovery (mDNS) für VLAN 40 ▸ ✅ FERTIG (2026-07-02) — `docs/tp7-mdns.md` - ✅ **Built-in mDNS-Repeater** aktiv — **Live-Stand `crs326-core` (verifiziert 2026-08-14):** @@ -2120,9 +2133,8 @@ den Weg, über den man den Fehler zurücknehmen wollte. Datenpfad (im TP5-Log mit 137 Zeilen belegt); Container braucht nur noch der **öffentliche** Ingress. Vorschlag: `nautobot` als Probe aus dem CF-Ingress nehmen. - ~~**DoH-Cloudflare auf `crs326-core`**~~ ✅ **erledigt 2026-08-18** (TP23 Stufe 1 = TP21.1 TODO 1). -- **TP6.1 — Saettigung vermeiden (NEU 2026-08-23):** NAS-Sync per Prioritaet zuruecknehmen, - damit ein Call gar nicht erst in die Saettigung geraet (12,9 statt 42 ms). Vorarbeit aus TP6 - ist bewiesen; Vorbedingung ist eine ComplianceRule fuer `/ip firewall mangle`. +- ~~**TP6.1 — Saettigung vermeiden**~~ ✅ **LIVE 2026-08-23: 40,8 → 11,0 ms unter Last** (−73 %). + NAS-Sync weicht bei Last zurueck, ComplianceRule `ip-firewall-mangle` angelegt, 104/104. - **TP24 — DC-USV** (seit 2026-08-16 eigener TP, aus TP15 herausgelöst). - ~~**TP19 — VLAN1-Client-Retirement / cudy-Teardown**~~ ✅ **geschlossen 2026-08-18** (VLAN1-ARP = 0). Die zwei Reste sind dorthin verschoben, wo sie hingehören: tote CF-Tunnel → TP16, HA-Portainer `setup_retry` → TP11. - ~~**TP3.3 — WLAN-Coverage-Kur**~~ → **in TP25 aufgegangen** (2026-08-17). 802.11k/v ist repariert (08-13); @@ -2157,7 +2169,7 @@ steht unten unter „Durch". Nachfolger ist **TP16.3**, das oben unter Follow-up TP1 · TP2 · TP2.1 · TP3 (Rebuild 2026-08-08) · TP3.1 · TP4 (Rolle seit 08-08 auf dem Core) · TP7 · TP7.2 · TP10 (Kern) · TP14 (+14.1/14.2/14.2a/14.3/14.5) · TP15 · TP16 (Kern) · TP17 · TP19.1 · TP20 · TP21 · TP21.1 · -TP22 · TP-R · **TP3.2** (08-17) · **TP5** (08-18) · **TP7.1** (08-18) · **TP19** (08-18) · **TP23 Stufe 1** (08-18) · **TP6** (08-23, geschlossen statt gebaut) · **TP26** (08-17) · **TP27** (08-17/18) · **TP28** (08-17, GC-Modell 08-18). **Verworfen:** TP9 (Gateway-HA, User-Entscheid 2026-08-08). **Obsolet:** Omada-PVID-Fix +TP22 · TP-R · **TP3.2** (08-17) · **TP5** (08-18) · **TP7.1** (08-18) · **TP19** (08-18) · **TP23 Stufe 1** (08-18) · **TP6** (08-23, geschlossen statt gebaut) · **TP6.1** (08-23, Upload-Prio live) · **TP26** (08-17) · **TP27** (08-17/18) · **TP28** (08-17, GC-Modell 08-18). **Verworfen:** TP9 (Gateway-HA, User-Entscheid 2026-08-08). **Obsolet:** Omada-PVID-Fix (User-Entscheid 2026-07-22 — „nie wieder aufgreifen").
📜 Historische Fassung dieses Abschnitts (bis 2026-08-14 fortgeschrieben, hier bewusst erhalten) diff --git a/nautobot/ingest-mangle-rule.py b/nautobot/ingest-mangle-rule.py new file mode 100755 index 0000000..085f38a --- /dev/null +++ b/nautobot/ingest-mangle-rule.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +ComplianceRule `ip-firewall-mangle` anlegen (idempotent). + +Die Luecke, die das schliesst +----------------------------- +`/ip firewall mangle` stand unter **keiner** ComplianceRule. Fuer TP6.1 ist das nicht +egal, sondern zentral: die Upload-Priorisierung besteht aus **zwei** Teilen, und der +Queue-Tree allein ist wirkungslos. Faellt die Markierungsregel weg oder driftet sie, +landet aller Verkehr in derselben Klasse -- und **nichts** meldet das, waehrend die +Queue weiter zufrieden aussieht. + +Dieselbe blinde Stelle, die TP26 fuer `/ip firewall filter` und TP6 fuer `/queue` +geschlossen hat. + +⚠️ `config_ordered=True`, anders als bei `queue`: Mangle ist eine Kette, die von oben +nach unten ausgewertet wird. Die spezifische NAS-Regel MUSS vor dem Catch-all stehen -- +sonst bekaeme der Sync die Rest-Prioritaet und die ganze Konstruktion waere still +wirkungslos. Genau wie bei `ip-firewall-filter`. + + export NAUTOBOT_TOKEN=... # aus docs/nas-vlan20/stacks/210-nautobot.yml + python3 nautobot/ingest-mangle-rule.py # Dry-Run + python3 nautobot/ingest-mangle-rule.py --apply +""" +import json +import os +import sys +import urllib.error +import urllib.parse +import urllib.request + +BASE = "http://10.40.20.30:8080/api" +TOKEN = os.environ.get("NAUTOBOT_TOKEN", "") +APPLY = "--apply" in sys.argv + +FEATURE = "ip-firewall-mangle" +BESCHREIBUNG = ("TP6.1: packet-marks fuer die Upload-Priorisierung. Ohne Rule waere die " + "Markierung unsichtbarer Drift -- und ohne sie ist der Queue-Tree wirkungslos.") +MATCH = "/ip firewall mangle" +PLATTFORM = "RouterOS" + + +def req(method, path, data=None): + url = path if path.startswith("http") else BASE + path + body = json.dumps(data).encode() if data is not None else None + r = urllib.request.Request(url, data=body, method=method) + r.add_header("Authorization", "Token " + TOKEN) + r.add_header("Content-Type", "application/json") + r.add_header("Accept", "application/json") + try: + with urllib.request.urlopen(r, timeout=60) as resp: + raw = resp.read() + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + # 🔴 Fehlerkoerper ausgeben: Nautobot begruendet 400er praezise, und ohne den + # Text raet man an Feldnamen herum (TP22-Erfahrung). + sys.exit("!! %s %s -> %s\n %s" % (method, url, e.code, e.read().decode()[:600])) + + +def eins(pfad, **filter): + q = urllib.parse.urlencode(filter) + res = req("GET", "%s?%s" % (pfad, q)).get("results", []) + if len(res) > 1: + sys.exit("FEHLER: %s?%s liefert %d Treffer, erwartet 0 oder 1" % (pfad, q, len(res))) + return res[0] if res else None + + +def main(): + if not TOKEN: + sys.exit("NAUTOBOT_TOKEN fehlt.") + print("Modus:", "APPLY" if APPLY else "DRY-RUN (nichts wird geschrieben)") + + plat = eins("/dcim/platforms/", name=PLATTFORM) + if not plat: + sys.exit("Plattform %r nicht gefunden." % PLATTFORM) + print("Plattform : %s (%s)" % (plat["display"], plat["id"])) + + feat = eins("/plugins/golden-config/compliance-feature/", name=FEATURE) + if feat: + print("Feature : existiert bereits (%s)" % feat["id"]) + else: + print("Feature : anlegen -> %s" % FEATURE) + if APPLY: + feat = req("POST", "/plugins/golden-config/compliance-feature/", + {"name": FEATURE, "slug": FEATURE, "description": BESCHREIBUNG}) + print(" angelegt (%s)" % feat["id"]) + + if not feat: + print("\nDry-Run: ohne Feature kann die Rule nicht geprueft werden. Mit --apply erneut.") + return + + rule = eins("/plugins/golden-config/compliance-rule/", + feature=feat["id"], platform=plat["id"]) + if rule: + print("Rule : existiert bereits (%s), match_config=%r" + % (rule["id"], rule.get("match_config"))) + if rule.get("match_config") != MATCH: + print(" ⚠️ match_config weicht ab -- erwartet %r" % MATCH) + return + + nutzlast = { + "feature": feat["id"], + "platform": plat["id"], + "description": BESCHREIBUNG, + # Reihenfolge ist SEMANTISCH: die spezifische NAS-Regel muss vor dem + # Catch-all stehen, sonst ist die Priorisierung still wirkungslos. + "config_ordered": True, + "config_type": "cli", + "match_config": MATCH, + "custom_compliance": False, + "config_remediation": False, + } + print("Rule : anlegen -> %s auf %s, match_config=%r" % (FEATURE, PLATTFORM, MATCH)) + if APPLY: + r = req("POST", "/plugins/golden-config/compliance-rule/", nutzlast) + print(" angelegt (%s)" % r["id"]) + else: + print(" Nutzlast: %s" % json.dumps(nutzlast)) + + +if __name__ == "__main__": + main() diff --git a/nautobot/mikrotik_routeros.j2 b/nautobot/mikrotik_routeros.j2 index c2ec8ef..aa7466e 100644 --- a/nautobot/mikrotik_routeros.j2 +++ b/nautobot/mikrotik_routeros.j2 @@ -312,7 +312,46 @@ # ⚖️ 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` BLEIBT — sie bewacht diesen Abschnitt weiter als leer. +# 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.