M8: Netzwerk-Isolation live verifiziert (manuell + über den Wizard)
Isolation zuerst manuell per SSH nachgebaut (ether5), danach über den
App-Wizard (Experte-Modus, ether4), um die eigentliche Abnahme-
Bedingung ("kompletter Wizard-Durchlauf") zu erfüllen. Dabei drei
App-Bugs gefunden und gefixt:
- Bug 22: neues LAN-/VLAN-Interface wurde nie der defconf-Interface-
Liste "LAN" hinzugefügt, wodurch DNS-Anfragen an den Router selbst
blockiert blieben (Werks-Firewall droppt Input von allem außerhalb
dieser Liste).
- Bug 23: ein voller Wizard-Durchlauf gegen einen bereits konfigurierten
Router brach am ersten nicht-idempotenten Add-Befehl ab
(/ip address, /ip pool, /ip dhcp-server, /ip dhcp-server network).
- Bug 24: ein als eigenes isoliertes Netz konfiguriertes Interface
blieb Bridge-"Slave" (Werks-Bridging), wodurch RouterOS die
generierten Isolationsregeln selbst als ungültig verwarf.
Alle drei in DhcpServerCommandBuilder/SetupViewModel gefixt, 52 Unit-
Tests grün, Isolation+DNS+Internet am echten Gerät bestätigt. M8 auf
live verifiziert gesetzt. Nebenbei zwei veraltete Doku-Stellen zum
Gitea-Remote korrigiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
This commit is contained in:
+39
@@ -696,3 +696,42 @@ Test, WLAN/Bonding/PPPoE-Live-Tests).
|
||||
(Bug 21, Nächste-Schritte Punkt 1 als "in Arbeit" markiert — der
|
||||
eigentliche M8-Isolationstest ist noch nicht zu Ende geführt, nächste
|
||||
Session dort fortsetzen) aktualisiert, Commit erstellt. Sitzung beendet.
|
||||
|
||||
## Session: M8 komplett live verifiziert (manuell + über die App)
|
||||
|
||||
- "wo waren wir stehen geblieben?" → Stand zusammengefasst, "weiter bei
|
||||
M8" (M5 kann mangels Hardware nicht getestet werden).
|
||||
- Nutzerfrage: Port 5 komplett isolieren, eigener DHCP-Server, trotzdem
|
||||
Internet — `mikrotik-setup.md` Szenario C traf exakt zu, Befehle
|
||||
gegeben. Danach mehrere Diagnoserunden am echten Router (SSH), jede
|
||||
ein eigener Konfigurationsfehler statt App-Bug: Adresse zuerst als
|
||||
`/32`, dann versehentlich als `.0`-Netzadresse statt `.1`-Host-Adresse
|
||||
eingetragen (Route fehlte); danach DNS-Anfragen an den Router liefen
|
||||
in Timeout, obwohl Lease+Route+Internet gingen — Ursache: `ether5`
|
||||
fehlte in der defconf-Interface-Liste "LAN", RouterOS' Werks-Firewall
|
||||
blockt Input von allem, was nicht in dieser Liste steht. Isolation
|
||||
danach per `/interface list`-basierter Forward-Drop-Regel gesetzt und
|
||||
beidseitig bestätigt (iMac ↔ Laptop erreichen sich nicht, Internet
|
||||
bleibt für beide).
|
||||
- "testen wir das nochmal über die App" → derselbe Test über den
|
||||
Wizard (Experte-Modus, `ether4`) wiederholt, um die eigentliche
|
||||
M8-Abnahmebedingung zu erfüllen. Dabei vorab zwei App-Bugs erkannt und
|
||||
gefixt, bevor überhaupt angewendet wurde: **Bug 22** (LAN/VLAN-Schritt
|
||||
fügt neues Interface nie der "LAN"-Liste hinzu — derselbe DNS-Bug wie
|
||||
eben, jetzt als App-Bug bestätigt) und **Bug 23** (voller
|
||||
Wizard-Durchlauf gegen bereits konfigurierten Router bricht am
|
||||
zweiten, nicht-idempotenten Befehl ab — Nutzer wählte "Idempotenz
|
||||
jetzt erweitern" statt nur Befehle zu verifizieren).
|
||||
- Nach Apply zeigte `/ip firewall filter print` zwei mit `I` (INVALID)
|
||||
markierte Isolationsregeln: `ether4` war noch Bridge-Slave (Werks-
|
||||
Bridging), RouterOS verwirft Interface-Matcher auf Slave-Ports selbst.
|
||||
**Bug 24** gefunden — Nutzer entfernte `ether4` manuell aus der
|
||||
Bridge, danach wurden die Regeln automatisch gültig; Fix (automatisches
|
||||
Lösen aus der Bridge vor Zuweisung) direkt im Wizard-Code ergänzt und
|
||||
getestet.
|
||||
- Alle Live-Tests am Ende erfolgreich (Lease, Gateway-Ping, DNS,
|
||||
Internet, Isolation gegen iMac/Laptop), 52 Unit-Tests grün. M8 als
|
||||
erster Milestone sowohl manuell als auch über die App selbst
|
||||
verifiziert. Nebenbei zwei veraltete Doku-Stellen korrigiert (Gitea-
|
||||
Remote existiert längst, war noch als "nicht vorhanden" dokumentiert).
|
||||
HANDOFF.md/README.md aktualisiert, Commit + Push.
|
||||
|
||||
+133
-51
@@ -56,17 +56,60 @@ Farbwerte, alles adaptive System-Farben) — vom Nutzer live bestätigt.
|
||||
Release-Build unter `/Applications/RouterOS Assistant.app` ist auf
|
||||
aktuellem Stand.
|
||||
|
||||
**Zum Sessionende:** M8-Hardware-Test (Netzwerk-Isolation) begonnen —
|
||||
voller Wizard-Durchlauf über den Experte-Modus, WAN neu einrichten.
|
||||
Dabei Bug 21 gefunden: der Wizard-Apply nutzte für WAN-DHCP-Client/PPPoE
|
||||
immer `.add`, was auf einem bereits konfigurierten Interface (hier: aus
|
||||
der zurückgespielten Sicherung) mit "failure: dhcp-client on that
|
||||
interface already exists" scheiterte. Fix: `SetupViewModel.
|
||||
applyIdempotently` versucht bei `.add` auf `/ip dhcp-client` oder
|
||||
`/interface pppoe-client` nach einem Fehlschlag automatisch `.set`
|
||||
(nach "interface" gematcht) — deployt, **aber der eigentliche
|
||||
M8-Isolationstest selbst ist noch nicht zu Ende geführt/verifiziert**,
|
||||
das ist der erste Schritt für die nächste Session.
|
||||
**Danach, neue Session: M8 komplett live verifiziert — erst manuell,
|
||||
dann über den Wizard selbst.** Bug 21 aus der Vorsession (WAN-DHCP-
|
||||
Client/PPPoE-Apply nicht idempotent) blieb bestätigt gefixt. Isolation
|
||||
zuerst manuell per SSH an `ether5` nachgebaut (eigenes Subnetz, eigener
|
||||
DHCP-Server, `/interface list`-basierte Forward-Drop-Regel) — dabei drei
|
||||
reine Konfigurationsfehler der Reihe nach gefunden und behoben (Adresse
|
||||
versehentlich als `/32` bzw. `.0`-Netzadresse statt Host-Adresse
|
||||
eingetragen; `ether5` fehlte in der defconf-Interface-Liste "LAN",
|
||||
wodurch DNS-Anfragen an den Router selbst blockiert wurden, obwohl
|
||||
DHCP/Routing/Internet normal liefen). Isolation danach beidseitig
|
||||
bestätigt (iMac ↔ Laptop erreichen sich nicht, Internet für beide
|
||||
weiterhin ja).
|
||||
|
||||
Anschließend derselbe Test **über den App-Wizard** (Experte-Modus,
|
||||
`ether4`) wiederholt, um die Abnahme-Bedingung aus der Vorsession
|
||||
("kompletter Wizard-Durchlauf") tatsächlich zu erfüllen — dabei drei
|
||||
echte App-Bugs gefunden und gefixt, alle auf denselben "Wizard wurde nie
|
||||
gegen einen bereits konfigurierten Router bzw. mit bereits gebrückten
|
||||
Ports erneut ausgeführt" blinden Fleck zurückzuführen:
|
||||
|
||||
- **Bug 22:** `DhcpServerCommandBuilder` fügte ein neues LAN-/VLAN-
|
||||
Interface nie der defconf-Interface-Liste "LAN" hinzu — derselbe
|
||||
DNS-Bug wie oben, jetzt als App-Bug bestätigt. Fix: automatisch
|
||||
`/interface list add name=LAN` + `/interface list member add
|
||||
list=LAN interface=<name>`, beide toleriert falls schon vorhanden.
|
||||
- **Bug 23:** ein voller Wizard-Durchlauf gegen einen bereits
|
||||
konfigurierten Router brach beim zweiten Befehl ab
|
||||
(`/ip address add address=192.168.88.1/24 interface=bridge` —
|
||||
"already have such address"), weil nur WAN-DHCP-Client/PPPoE
|
||||
idempotent behandelt wurden (Bug 21). Fix: `SetupViewModel.
|
||||
applyIdempotently` erweitert — `/ip pool`/`/ip dhcp-server`/
|
||||
`/ip dhcp-server network` werden bei Duplikat jetzt als `.set`
|
||||
(gematcht auf name/name/address) erneut versucht, `/ip address` bei
|
||||
exaktem Duplikat als bereits erledigt behandelt (ein Interface darf
|
||||
mehrere Adressen halten, ein `.set` nach "interface" träfe sonst
|
||||
potenziell die falsche).
|
||||
- **Bug 24:** LAN-/VLAN-Schritt löste ein gewähltes physisches Interface
|
||||
nie aus einer bestehenden Bridge — auf Werks-Routern sind ether2–5 ab
|
||||
Werk gebridged. Als eigenes isoliertes Netz konfiguriert blieb das
|
||||
Interface Bridge-"Slave", RouterOS verwarf die generierten
|
||||
Isolationsregeln selbst als ungültig ("in/out-interface matcher not
|
||||
possible when interface is slave - use master instead"), live an
|
||||
`ether4` bestätigt. Fix: `DhcpServerCommandBuilder` stellt jetzt
|
||||
`/interface bridge port remove [find interface=<name>]` voran
|
||||
(übersprungen für "bridge" selbst), toleriert als bereits erledigt
|
||||
falls nie gebridged.
|
||||
|
||||
Nach allen drei Fixes lief der komplette App-Wizard-Durchlauf für
|
||||
`ether4` fehlerfrei durch (inkl. Neuanwendung der bereits vorhandenen
|
||||
`bridge`/`ether1`-Konfiguration), Isolation + DNS + Internet vom Nutzer
|
||||
am echten Gerät bestätigt. Alle 52 Unit-Tests grün. M8 damit als
|
||||
einziger Milestone bisher **sowohl manuell als auch über die App selbst**
|
||||
live verifiziert.
|
||||
|
||||
## Ziel
|
||||
|
||||
Native macOS-App (SwiftUI), die Laien per geführtem Interview-Wizard durch
|
||||
@@ -104,7 +147,8 @@ killall Dock # Icon-Cache auffrischen, falls sich nur das Icon geändert hat
|
||||
Abhängigkeiten stehen in `project.yml`, das ist die Quelle der Wahrheit —
|
||||
nicht das generierte `.xcodeproj` von Hand editieren.
|
||||
|
||||
Git: lokales Repo, kein Remote (Nutzer hat aktuell keine Gitea-Instanz).
|
||||
Git: Remote `origin` zeigt auf eine selbst gehostete Gitea-Instanz
|
||||
(`git@192.168.178.222:kay/RouterOS.git`).
|
||||
`.xcodeproj`, `DerivedData`, `.build`, Firmware-Dateien (`*.npk`, `*.cpgz`)
|
||||
und Router-Backups (`*.rsc`, `/Backups/`) sind gitignored.
|
||||
|
||||
@@ -458,7 +502,33 @@ Singleton-Fallback, der für ein Menü per Exception ausgelöst wurde
|
||||
(Bug 8), blieb für ein anderes Menü aus, weil RouterOS denselben
|
||||
Fehlertext dort mit Exit-Code 0 zurückgibt; robuste Erkennung muss immer
|
||||
auch den Output-Text selbst prüfen, nie nur auf eine geworfene Exception
|
||||
vertrauen.
|
||||
vertrauen. **Wizard-Apply nutzte für WAN-DHCP-Client/PPPoE immer `.add`**
|
||||
(Bug 21) — scheiterte live mit "failure: dhcp-client on that interface
|
||||
already exists" auf einem Interface, das (z.B. aus einer zurückgespielten
|
||||
Sicherung) schon einen Client hatte; `SetupViewModel.applyIdempotently`
|
||||
versucht seitdem bei `.add`-Fehlschlag auf diesen beiden Menüs
|
||||
automatisch `.set` (nach "interface" gematcht). **Ein neu eingerichtetes
|
||||
LAN-/VLAN-Interface wurde nie der defconf-Interface-Liste "LAN"
|
||||
hinzugefügt** (Bug 22) — auf Routern mit Werks-Firewall
|
||||
(`chain=input action=drop in-interface-list=!LAN`) blieb dadurch jede
|
||||
Anfrage an den Router selbst (DNS, Winbox) vom neuen Netz aus blockiert,
|
||||
obwohl DHCP/Routing/Internet normal liefen (reine Forward-Chain-Sache,
|
||||
unberührt) — live an einer manuell nachgebauten Isolation gefunden
|
||||
(Lease + Default-Route vorhanden, jede DNS-Anfrage lief trotzdem in
|
||||
Timeout). **Voller Wizard-Durchlauf gegen einen bereits konfigurierten
|
||||
Router brach an jedem nicht-idempotenten `.add` ab** (Bug 23) — nur
|
||||
WAN-DHCP-Client/PPPoE waren idempotent (Bug 21); `/ip address`
|
||||
(exaktes Duplikat = bereits erledigt) sowie `/ip pool`/`/ip
|
||||
dhcp-server`/`/ip dhcp-server network` (Retry als `.set`, gematcht auf
|
||||
name/name/address) kamen dazu. **Ein als eigenes isoliertes Netz
|
||||
konfiguriertes Interface blieb Bridge-"Slave"** (Bug 24) — auf
|
||||
Werks-Routern sind ether2–5 ab Werk gebridged; ohne explizites Lösen aus
|
||||
der Bridge verwarf RouterOS die generierten Forward-Isolationsregeln
|
||||
selbst als ungültig ("in/out-interface matcher not possible when
|
||||
interface is slave - use master instead"), live an `ether4` bestätigt;
|
||||
`DhcpServerCommandBuilder` stellt seitdem ein `/interface bridge port
|
||||
remove [find interface=<name>]` voran (übersprungen für "bridge"
|
||||
selbst).
|
||||
|
||||
## `xcodebuild test` hängt — Gatekeeper, kein Code-Bug
|
||||
|
||||
@@ -511,12 +581,11 @@ wiederholen.
|
||||
nur der "kein WLAN"-Zweig ist bestätigt (zwei Testgeräte, beide ohne
|
||||
WLAN-Chip). Sicherheitsprofil-Anlage + SSID/Passwort-`.set` auf einem
|
||||
echten `/interface wireless`-Interface noch nie live gelaufen.
|
||||
- **Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut
|
||||
bestätigt** — der erste Live-Test (M6) lief technisch durch, traf aber
|
||||
wegen Bug 6 oben `lo` statt des echten WAN-Ports. Nach dem Parser-Fix
|
||||
wurde nur die Interface-**Auswahl** erneut bestätigt ("alle Interfaces
|
||||
auswählbar"), nicht aber ein erneuter Firewall-Apply mit korrektem
|
||||
Interface + Kontrolle der resultierenden Regeln.
|
||||
- ~~Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut
|
||||
bestätigt~~ — erledigt: beim M8-App-Test (siehe oben) lief ein voller
|
||||
`FirewallConfig`-Apply mit `ether1` als WAN erneut durch,
|
||||
`/ip firewall filter print` zeigte alle Basisregeln (established/
|
||||
related, invalid-drop, ICMP-Accept, WAN-Drop) korrekt mit `ether1`.
|
||||
- **Neuer RouterOS-WiFi-Treiber (`/interface wifi`, wifiwave2/802.11ax)
|
||||
nicht unterstützt** — wird erkannt und im UI erklärt, aber nicht
|
||||
konfiguriert.
|
||||
@@ -560,10 +629,20 @@ Isolation vom Hauptnetzwerk, ohne dass eine einzige Firewall-Regel das
|
||||
durchsetzte — reiner Text ohne Wirkung. Jetzt ist Isolation ein echter,
|
||||
optionaler Schalter mit tatsächlicher Regel-Erzeugung.
|
||||
|
||||
Nur gegen Unit-Tests verifiziert (`FirewallConfigTests`: isoliertes
|
||||
Netzwerk gegen nicht-isoliertes, gegenseitige Isolation, keine Isolation).
|
||||
**Noch nicht gegen echte Hardware getestet** — bräuchte zwei getrennte
|
||||
Testnetze am selben Router, um die Drop-Regeln praktisch zu bestätigen.
|
||||
**Live gegen Hardware verifiziert (2026-09-15)** — zuerst manuell per
|
||||
SSH nachgebaut (siehe "Zum Sessionende" oben), danach über den
|
||||
App-Wizard selbst (`ether4`, Experte-Modus). iMac ↔ Laptop erreichen
|
||||
sich über die Netzgrenze nicht mehr, Internet + DNS funktionieren auf
|
||||
beiden Seiten weiter. Dabei drei App-Bugs gefunden und gefixt (Bug
|
||||
22–24, siehe Bug-Liste oben) — alle drei Voraussetzung dafür, dass ein
|
||||
per Wizard eingerichtetes isoliertes Netz auf einem bereits
|
||||
konfigurierten bzw. werksseitig gebridgten Router überhaupt funktioniert:
|
||||
fehlende "LAN"-Interface-Listen-Mitgliedschaft (DNS zum Router blockiert),
|
||||
fehlende Idempotenz bei erneutem Wizard-Lauf (Apply brach sofort ab),
|
||||
und ein nie aus der Bridge gelöstes physisches Interface (RouterOS
|
||||
verwarf die Isolationsregeln selbst als ungültig). Weiterhin unverändert
|
||||
seit vorher: `FirewallConfigTests` (isoliertes Netzwerk gegen
|
||||
nicht-isoliertes, gegenseitige Isolation, keine Isolation).
|
||||
|
||||
## M9: Einfach/Experte-Modus im Einrichten-Wizard
|
||||
|
||||
@@ -692,9 +771,12 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
|
||||
bestätigt** (inkl. neuem "Trennen"-Button im Verbinden-Tab, der dafür
|
||||
nötig wurde). Fehlerzustände/Politur und REST-Schreibpfad-Verifikation
|
||||
gegen ein Gerät mit aktivem `www-ssl` stehen noch aus.
|
||||
- 🔶 M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
|
||||
(eigene Firewall-Regeln pro LAN/VLAN) — gebaut, nur Unit-Test-verifiziert,
|
||||
noch nicht gegen echte Hardware getestet.
|
||||
- ✅ M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
|
||||
(eigene Firewall-Regeln pro LAN/VLAN) — **live gegen Hardware
|
||||
verifiziert**, sowohl manuell (SSH) als auch über den App-Wizard
|
||||
selbst (`ether4`), dabei drei App-Bugs gefunden+gefixt (Bug 22–24,
|
||||
siehe oben). Isolation, DNS und Internet vom Nutzer am echten Gerät
|
||||
bestätigt.
|
||||
- 🔶 M9: Einfach/Experte-Modus im Einrichten-Wizard — gebaut, Compile/
|
||||
Unit-Test-verifiziert. UI (Modusumschalter selbst) noch nicht manuell
|
||||
durchgeklickt — nur M10s Experte-Tab wurde das (siehe M10).
|
||||
@@ -765,41 +847,35 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **In Arbeit, hier weitermachen:** kompletter Wizard-Durchlauf
|
||||
(WAN/LAN mit zwei Interfaces ether2+ether3, eins isoliert/Firewall)
|
||||
über den Experte-Modus — kombiniert M8-Hardware-Test (Netzwerk-
|
||||
Isolation) mit diesem Punkt. Wurde in dieser Session begonnen, brach
|
||||
beim WAN-Schritt mit Bug 21 ab (jetzt gefixt, deployt, aber
|
||||
**ungetestet**). Nach erneutem Durchlauf: Firewall-Ergebnis mit
|
||||
korrektem WAN-Port UND die Isolations-Regeln per
|
||||
`/ip firewall filter print` kontrollieren, plus praktisch testen
|
||||
(iMac ↔ Laptop sollten sich nicht erreichen, Internet für beide
|
||||
schon).
|
||||
2. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
|
||||
3. Rest von M7: Fehlerzustände/Politur, REST-Schreibtest an einem Gerät
|
||||
1. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
|
||||
2. Rest von M7: Fehlerzustände/Politur, REST-Schreibtest an einem Gerät
|
||||
mit aktivem `www-ssl`.
|
||||
4. M8 gegen echte Hardware testen: zwei LAN-Interfaces/VLANs anlegen, eins
|
||||
isoliert, anwenden, per `/ip firewall filter print` kontrollieren, dass
|
||||
die Drop-Regeln greifen und der Rest (Internetzugriff, nicht-isolierte
|
||||
Netzwerke) unangetastet bleibt.
|
||||
5. M9 UI (Einfach/Experte-Modusumschalter im Einrichten-Tab selbst) noch
|
||||
3. M9 UI (Einfach/Experte-Modusumschalter im Einrichten-Tab selbst) noch
|
||||
manuell durchklicken — M10s Experte-Tab wurde bereits vom Nutzer
|
||||
bestätigt (siehe oben), der Moduswechsel im Wizard noch nicht.
|
||||
6. M10: WLAN-Schemas (an Gerät mit WLAN-Chip), Bonding, PPPoE-Client
|
||||
4. M10: WLAN-Schemas (an Gerät mit WLAN-Chip), Bonding, PPPoE-Client
|
||||
(mit echten oder Test-ISP-Zugangsdaten) noch gegen Hardware verifizieren.
|
||||
7. Optional, kleinere Politur: den neuen Dauer-Editor
|
||||
5. Optional, kleinere Politur: den neuen Dauer-Editor
|
||||
(`RouterOSFieldSchema.Kind.duration`) auch auf weitere Zeitwert-Felder
|
||||
anwenden, die bisher nur Text mit Beispiel-Tooltip sind — WireGuard-
|
||||
Peer "Keepalive", Scheduler "Intervall", Netwatch "Prüf-Intervall".
|
||||
Optional auch für den Geräte-Tab, falls dort künftig Zeitfelder
|
||||
dazukommen.
|
||||
8. **`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
|
||||
6. **Firewall-NAT-/Filter-`add`-Befehle sind bei wiederholtem Wizard-Lauf
|
||||
weiterhin nicht idempotent** — im Gegensatz zu den bei Bug 22–24
|
||||
gefixten Menüs lässt RouterOS identische NAT-/Filter-Regeln mehrfach
|
||||
zu (kein Fehler, kein Abbruch), aber jeder erneute Wizard-Durchlauf
|
||||
häuft doppelte Regeln an. Nicht blockierend (heute so live
|
||||
beobachtet, Apply lief trotzdem durch), aber auf Dauer Regel-Bloat —
|
||||
bei Gelegenheit auf "vorhandene identische Regel überspringen"
|
||||
umstellen.
|
||||
7. **`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
|
||||
prüfen** — live als falsch bestätigt für `/ip dhcp-server lease`
|
||||
(Bug 15). Betrifft potenziell jedes `.set`/`.remove` im Experte-Tab.
|
||||
Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig
|
||||
vorhandenen Einträgen `:put [<Pfad> find]` und `<Pfad> print terse`
|
||||
unabhängig ausführen und die Reihenfolgen von Hand vergleichen.
|
||||
9. **Router wurde zwischen M12 und M13 vom Nutzer komplett auf
|
||||
8. **Router wurde zwischen M12 und M13 vom Nutzer komplett auf
|
||||
Werkseinstellungen zurückgesetzt** ("damit wir sauber weitermachen
|
||||
können"), und im Zuge des ersten M13-Testlaufs (Bug 18, Login-Sperre)
|
||||
nochmal per Hardware-Reset zurückgesetzt. `test-vlan`/`testpool`/
|
||||
@@ -812,7 +888,7 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
|
||||
— vor Annahmen über den genauen aktuellen Stand lieber neu per
|
||||
Geräte-/Übersicht-/Verbinden-Tab prüfen statt auf ältere Einträge hier
|
||||
zu vertrauen.
|
||||
10. **`fetchMenuItems`s Output-Text-Fallback (Bug 20) auf weitere
|
||||
9. **`fetchMenuItems`s Output-Text-Fallback (Bug 20) auf weitere
|
||||
Singleton-Menüs prüfen** — live nur für `/system routerboard`
|
||||
bestätigt gebraucht zu werden (`/system identity` etc. liefen schon
|
||||
vorher über den Exception-Zweig, siehe Bug 8). Nicht geprüft, ob es
|
||||
@@ -820,10 +896,16 @@ beim Ändern die RouterOS-Suffix-Form zurück) — angewendet auf DHCP-Server
|
||||
(weder Exception noch im Output-Text) — bei einer leer bleibenden
|
||||
Liste in Übersicht/Geräte/Experte-Tab als erste Verdachtsquelle
|
||||
prüfen.
|
||||
10. **Offener Nutzerwunsch, noch nicht umgesetzt:** ausführlichere
|
||||
Tooltips im Experte-Tab für alle Sektionen, mit konkretem
|
||||
Adress-Format-Beispiel (z.B. "10.10.10.1/24") statt nur "mit
|
||||
Präfix". Viele `RouterOSFieldSchema.help`-Texte in
|
||||
`RouterOSSchemaCatalog.swift` sind aktuell leer (`help: ""`) oder
|
||||
knapp ohne Beispielwert — wurde diese Session zugunsten des
|
||||
M8-Hardware-Tests zurückgestellt.
|
||||
|
||||
Kein Gitea-Remote vorhanden — falls der Nutzer später eine Gitea-Instanz
|
||||
aufsetzt (z.B. selbst gehostet auf der vorhandenen OMV-NAS, siehe
|
||||
`Aperto/`-Projekt), Remote hinzufügen und pushen; bis dahin lokales Git.
|
||||
Gitea-Remote `origin` ist eingerichtet und wird laufend gepusht (siehe
|
||||
oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15.
|
||||
|
||||
`/Applications/RouterOS Assistant.app` ist der aktuell installierte
|
||||
Release-Build, auf dem Stand des jeweils letzten Commits auf `main`
|
||||
|
||||
@@ -94,7 +94,7 @@ darauf bauen Übersicht-, Geräte- und Experte-Tab gemeinsam auf.
|
||||
| M5 | WLAN-Schritt | 🔶 nur "kein WLAN"-Zweig getestet |
|
||||
| M6 | Firewall-Grundschutz | ✅ live verifiziert |
|
||||
| M7 | Härtung (SSH-Hostkey-TOFU) | 🔶 TOFU fertig, Rest offen |
|
||||
| M8 | Mehrere LAN-Interfaces + Netzwerk-Isolation | 🔶 gebaut, Hardware-Test läuft |
|
||||
| M8 | Mehrere LAN-Interfaces + Netzwerk-Isolation | ✅ live verifiziert |
|
||||
| M9 | Einfach/Experte-Modusschalter im Wizard | 🔶 gebaut, UI-Test offen |
|
||||
| M10 | Experte-Tab (generischer RouterOS-Zugriff) | ✅ live verifiziert |
|
||||
| M11 | Übersicht-Tab (IST-Zustand-Diagramm) | ✅ live verifiziert |
|
||||
|
||||
@@ -17,7 +17,26 @@ enum DhcpServerCommandBuilder {
|
||||
let serverName = "dhcp_\(interfaceName)"
|
||||
let routerIP = routerAddress.components(separatedBy: "/").first ?? routerAddress
|
||||
|
||||
return [
|
||||
// Detach from any bridge this physical port is still a member of before treating it as
|
||||
// its own network. Skipped for "bridge" itself (the app's own shared-LAN interface, never
|
||||
// a bridge port). Confirmed live (2026-09-15): factory-default routers have ether2-5
|
||||
// pre-bridged, and configuring one of them as a separate isolated network without this
|
||||
// produces a broken result RouterOS itself refuses to run — the interface stays a bridge
|
||||
// "slave", so its forward-chain isolation rules come back flagged invalid ("in/out-
|
||||
// interface matcher not possible when interface is slave - use master instead"). No-op if
|
||||
// the interface was never bridged: SSH's `remove [find ...]` is a silent no-op on no
|
||||
// match, and REST's not-found is tolerated by `SetupViewModel.applyIdempotently`.
|
||||
let bridgeDetachCommands: [RouterOSCommand] = interfaceName == "bridge" ? [] : [
|
||||
RouterOSCommand.remove(
|
||||
menuPath: "/interface bridge port",
|
||||
restPath: "interface/bridge/port",
|
||||
matchField: "interface",
|
||||
matchValue: interfaceName,
|
||||
summary: "\(interfaceName) aus evtl. bestehender Bridge lösen"
|
||||
)
|
||||
]
|
||||
|
||||
return bridgeDetachCommands + [
|
||||
RouterOSCommand.add(
|
||||
menuPath: "/ip address",
|
||||
restPath: "ip/address",
|
||||
@@ -51,6 +70,27 @@ enum DhcpServerCommandBuilder {
|
||||
"dns-server": dnsServers
|
||||
],
|
||||
summary: "DHCP-Netzwerk \(networkAddress) \(context) konfigurieren"
|
||||
),
|
||||
// RouterOS' factory-default firewall (present on most out-of-box routers) has an
|
||||
// input-chain rule dropping everything not from the "LAN" interface list. Without
|
||||
// this interface as a member, devices on it get DHCP/routing/internet fine (that's
|
||||
// forward-chain, untouched) but can never reach the router itself for DNS, Winbox,
|
||||
// etc. — confirmed live (2026-09-15): a manually-isolated port had a bound DHCP
|
||||
// lease and a working default route, yet every DNS query to the router timed out
|
||||
// until it was added to "LAN". Both commands are tolerated as already-satisfied by
|
||||
// `SetupViewModel.applyIdempotently` if the list/membership already exists (e.g. the
|
||||
// default "bridge" interface, already a defconf LAN member).
|
||||
RouterOSCommand.add(
|
||||
menuPath: "/interface list",
|
||||
restPath: "interface/list",
|
||||
arguments: ["name": "LAN"],
|
||||
summary: "Interface-Liste \"LAN\" sicherstellen"
|
||||
),
|
||||
RouterOSCommand.add(
|
||||
menuPath: "/interface list member",
|
||||
restPath: "interface/list/member",
|
||||
arguments: ["list": "LAN", "interface": interfaceName],
|
||||
summary: "\(interfaceName) der Interface-Liste \"LAN\" hinzufügen \(context)"
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -224,31 +224,61 @@ final class SetupViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// RouterOS allows at most one item per interface on some menus (a DHCP client, a PPPoE
|
||||
/// client) — re-running the wizard's WAN step on an interface that already has one makes the
|
||||
/// wizard's own `.add` fail live-confirmed: "failure: dhcp-client on that interface already
|
||||
/// exists". Rather than hard-fail the whole apply here, retry once as a `.set` matched by
|
||||
/// "interface" instead — reconfigures the existing entry in place, which is what re-running
|
||||
/// the wizard on the same interface should mean anyway. Scoped to exactly these two
|
||||
/// known-unique-per-interface menus, not applied generally to every `.add` — other menus
|
||||
/// (e.g. "/ip address") legitimately allow multiple items per interface, where silently
|
||||
/// converting a would-be-duplicate `.add` into a `.set` would be wrong, not helpful.
|
||||
private static let interfaceUniqueMenuPaths: Set<String> = ["/ip dhcp-client", "/interface pppoe-client"]
|
||||
/// Menus where a duplicate `.add` should instead reconfigure the existing entry in place —
|
||||
/// re-running any wizard step against an already-configured router hits this on every menu
|
||||
/// that enforces uniqueness (dhcp-client/pppoe-client: one per interface, live-confirmed
|
||||
/// "failure: dhcp-client on that interface already exists"; pool/dhcp-server: one per name;
|
||||
/// dhcp-server network: one per address). The value is which field of the (deterministic,
|
||||
/// app-generated) arguments identifies the existing entry to match on for the `.set` retry.
|
||||
/// "/ip address" is deliberately not here — an interface can legitimately hold several
|
||||
/// addresses, so matching a `.set` by "interface" alone could reconfigure the wrong one;
|
||||
/// see `duplicateTolerantMenuPaths` below for how that menu is handled instead.
|
||||
private static let retryAsSetMenuPaths: [String: String] = [
|
||||
"/ip dhcp-client": "interface",
|
||||
"/interface pppoe-client": "interface",
|
||||
"/ip pool": "name",
|
||||
"/ip dhcp-server": "name",
|
||||
"/ip dhcp-server network": "address"
|
||||
]
|
||||
|
||||
/// Menus where a duplicate `.add` means the desired state already holds, with nothing
|
||||
/// meaningful left to update — "/interface list"/"/interface list member" (fixed,
|
||||
/// hardcoded arguments; list membership is binary, no ".set" equivalent) and "/ip address"
|
||||
/// (the address string itself is the app's only identifying argument here — if RouterOS
|
||||
/// already has that exact address on that exact interface, this add's whole job is already
|
||||
/// done, and matching a `.set` by "interface" would risk touching a different address on a
|
||||
/// multi-address interface instead, per the note above).
|
||||
private static let duplicateTolerantMenuPaths: Set<String> = ["/interface list", "/interface list member", "/ip address"]
|
||||
|
||||
/// Menus where a `.remove` finding no match means the desired state already holds — used for
|
||||
/// `DhcpServerCommandBuilder`'s unconditional "detach this interface from any bridge" step,
|
||||
/// which runs even for interfaces that were never bridged (the common case). SSH's
|
||||
/// `remove [find ...]` is already a silent no-op there; REST's `findItemID` throws
|
||||
/// "not found" instead (see `RestTransport.apply`), so that specific failure needs to be
|
||||
/// swallowed here to keep both transports behaving the same way.
|
||||
private static let missingTolerantRemoveMenuPaths: Set<String> = ["/interface bridge port"]
|
||||
|
||||
private func applyIdempotently(_ command: RouterOSCommand) async throws {
|
||||
do {
|
||||
try await connectionService.apply(command)
|
||||
} catch {
|
||||
guard case .add = command.operation,
|
||||
Self.interfaceUniqueMenuPaths.contains(command.menuPath),
|
||||
let interfaceName = command.arguments["interface"] else {
|
||||
if case .remove = command.operation, Self.missingTolerantRemoveMenuPaths.contains(command.menuPath) {
|
||||
return
|
||||
}
|
||||
guard case .add = command.operation else { throw error }
|
||||
|
||||
if Self.duplicateTolerantMenuPaths.contains(command.menuPath) {
|
||||
return
|
||||
}
|
||||
guard let matchField = Self.retryAsSetMenuPaths[command.menuPath],
|
||||
let matchValue = command.arguments[matchField] else {
|
||||
throw error
|
||||
}
|
||||
let retryCommand = RouterOSCommand.set(
|
||||
menuPath: command.menuPath,
|
||||
restPath: command.restPath,
|
||||
matchField: "interface",
|
||||
matchValue: interfaceName,
|
||||
matchField: matchField,
|
||||
matchValue: matchValue,
|
||||
arguments: command.arguments,
|
||||
summary: command.summary
|
||||
)
|
||||
|
||||
@@ -43,12 +43,28 @@ final class RouterOSCommandBuilderTests: XCTestCase {
|
||||
let config = LanDhcpConfig()
|
||||
let commands = config.buildCommands()
|
||||
|
||||
XCTAssertEqual(commands.count, 4)
|
||||
XCTAssertEqual(commands.count, 6)
|
||||
XCTAssertEqual(commands[0].menuPath, "/ip address")
|
||||
XCTAssertEqual(commands[1].menuPath, "/ip pool")
|
||||
XCTAssertEqual(commands[2].menuPath, "/ip dhcp-server")
|
||||
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server network")
|
||||
XCTAssertEqual(commands[3].arguments["gateway"], "192.168.88.1")
|
||||
XCTAssertEqual(commands[4].menuPath, "/interface list")
|
||||
XCTAssertEqual(commands[4].arguments["name"], "LAN")
|
||||
XCTAssertEqual(commands[5].menuPath, "/interface list member")
|
||||
XCTAssertEqual(commands[5].arguments["list"], "LAN")
|
||||
XCTAssertEqual(commands[5].arguments["interface"], "bridge")
|
||||
}
|
||||
|
||||
func testLanDhcpCommandsDetachInterfaceFromBridgeWhenNotDefaultBridge() {
|
||||
var config = LanDhcpConfig()
|
||||
config.interfaceName = "ether4"
|
||||
let commands = config.buildCommands()
|
||||
|
||||
XCTAssertEqual(commands.count, 7)
|
||||
XCTAssertEqual(commands[0].menuPath, "/interface bridge port")
|
||||
XCTAssertEqual(commands[0].operation, .remove(matchField: "interface", matchValue: "ether4"))
|
||||
XCTAssertEqual(commands[1].menuPath, "/ip address")
|
||||
}
|
||||
|
||||
func testCliLineRendersSortedQuotedArgumentsForAdd() {
|
||||
|
||||
@@ -6,17 +6,21 @@ final class VlanEntryTests: XCTestCase {
|
||||
let vlan = VlanEntry(name: "Gäste", vlanID: 20, parentInterface: "bridge")
|
||||
let commands = vlan.buildCommands()
|
||||
|
||||
XCTAssertEqual(commands.count, 5)
|
||||
XCTAssertEqual(commands.count, 8)
|
||||
XCTAssertEqual(commands[0].menuPath, "/interface vlan")
|
||||
XCTAssertEqual(commands[0].arguments["vlan-id"], "20")
|
||||
XCTAssertEqual(commands[0].arguments["interface"], "bridge")
|
||||
XCTAssertEqual(commands[0].arguments["name"], "vlan20")
|
||||
|
||||
XCTAssertEqual(commands[1].menuPath, "/ip address")
|
||||
XCTAssertEqual(commands[1].arguments["interface"], "vlan20")
|
||||
XCTAssertEqual(commands[2].menuPath, "/ip pool")
|
||||
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server")
|
||||
XCTAssertEqual(commands[4].menuPath, "/ip dhcp-server network")
|
||||
XCTAssertEqual(commands[1].menuPath, "/interface bridge port")
|
||||
XCTAssertEqual(commands[2].menuPath, "/ip address")
|
||||
XCTAssertEqual(commands[2].arguments["interface"], "vlan20")
|
||||
XCTAssertEqual(commands[3].menuPath, "/ip pool")
|
||||
XCTAssertEqual(commands[4].menuPath, "/ip dhcp-server")
|
||||
XCTAssertEqual(commands[5].menuPath, "/ip dhcp-server network")
|
||||
XCTAssertEqual(commands[6].menuPath, "/interface list")
|
||||
XCTAssertEqual(commands[7].menuPath, "/interface list member")
|
||||
XCTAssertEqual(commands[7].arguments["interface"], "vlan20")
|
||||
}
|
||||
|
||||
func testDefaultAddressesAreDerivedFromVlanID() {
|
||||
|
||||
Reference in New Issue
Block a user