forked from kay/RouterOS
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
|
(Bug 21, Nächste-Schritte Punkt 1 als "in Arbeit" markiert — der
|
||||||
eigentliche M8-Isolationstest ist noch nicht zu Ende geführt, nächste
|
eigentliche M8-Isolationstest ist noch nicht zu Ende geführt, nächste
|
||||||
Session dort fortsetzen) aktualisiert, Commit erstellt. Sitzung beendet.
|
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
|
Release-Build unter `/Applications/RouterOS Assistant.app` ist auf
|
||||||
aktuellem Stand.
|
aktuellem Stand.
|
||||||
|
|
||||||
**Zum Sessionende:** M8-Hardware-Test (Netzwerk-Isolation) begonnen —
|
**Danach, neue Session: M8 komplett live verifiziert — erst manuell,
|
||||||
voller Wizard-Durchlauf über den Experte-Modus, WAN neu einrichten.
|
dann über den Wizard selbst.** Bug 21 aus der Vorsession (WAN-DHCP-
|
||||||
Dabei Bug 21 gefunden: der Wizard-Apply nutzte für WAN-DHCP-Client/PPPoE
|
Client/PPPoE-Apply nicht idempotent) blieb bestätigt gefixt. Isolation
|
||||||
immer `.add`, was auf einem bereits konfigurierten Interface (hier: aus
|
zuerst manuell per SSH an `ether5` nachgebaut (eigenes Subnetz, eigener
|
||||||
der zurückgespielten Sicherung) mit "failure: dhcp-client on that
|
DHCP-Server, `/interface list`-basierte Forward-Drop-Regel) — dabei drei
|
||||||
interface already exists" scheiterte. Fix: `SetupViewModel.
|
reine Konfigurationsfehler der Reihe nach gefunden und behoben (Adresse
|
||||||
applyIdempotently` versucht bei `.add` auf `/ip dhcp-client` oder
|
versehentlich als `/32` bzw. `.0`-Netzadresse statt Host-Adresse
|
||||||
`/interface pppoe-client` nach einem Fehlschlag automatisch `.set`
|
eingetragen; `ether5` fehlte in der defconf-Interface-Liste "LAN",
|
||||||
(nach "interface" gematcht) — deployt, **aber der eigentliche
|
wodurch DNS-Anfragen an den Router selbst blockiert wurden, obwohl
|
||||||
M8-Isolationstest selbst ist noch nicht zu Ende geführt/verifiziert**,
|
DHCP/Routing/Internet normal liefen). Isolation danach beidseitig
|
||||||
das ist der erste Schritt für die nächste Session.
|
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
|
## Ziel
|
||||||
|
|
||||||
Native macOS-App (SwiftUI), die Laien per geführtem Interview-Wizard durch
|
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 —
|
Abhängigkeiten stehen in `project.yml`, das ist die Quelle der Wahrheit —
|
||||||
nicht das generierte `.xcodeproj` von Hand editieren.
|
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`)
|
`.xcodeproj`, `DerivedData`, `.build`, Firmware-Dateien (`*.npk`, `*.cpgz`)
|
||||||
und Router-Backups (`*.rsc`, `/Backups/`) sind gitignored.
|
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
|
(Bug 8), blieb für ein anderes Menü aus, weil RouterOS denselben
|
||||||
Fehlertext dort mit Exit-Code 0 zurückgibt; robuste Erkennung muss immer
|
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
|
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
|
## `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
|
nur der "kein WLAN"-Zweig ist bestätigt (zwei Testgeräte, beide ohne
|
||||||
WLAN-Chip). Sicherheitsprofil-Anlage + SSID/Passwort-`.set` auf einem
|
WLAN-Chip). Sicherheitsprofil-Anlage + SSID/Passwort-`.set` auf einem
|
||||||
echten `/interface wireless`-Interface noch nie live gelaufen.
|
echten `/interface wireless`-Interface noch nie live gelaufen.
|
||||||
- **Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut
|
- ~~Firewall-Regeln mit korrektem WAN-Interface noch nicht erneut
|
||||||
bestätigt** — der erste Live-Test (M6) lief technisch durch, traf aber
|
bestätigt~~ — erledigt: beim M8-App-Test (siehe oben) lief ein voller
|
||||||
wegen Bug 6 oben `lo` statt des echten WAN-Ports. Nach dem Parser-Fix
|
`FirewallConfig`-Apply mit `ether1` als WAN erneut durch,
|
||||||
wurde nur die Interface-**Auswahl** erneut bestätigt ("alle Interfaces
|
`/ip firewall filter print` zeigte alle Basisregeln (established/
|
||||||
auswählbar"), nicht aber ein erneuter Firewall-Apply mit korrektem
|
related, invalid-drop, ICMP-Accept, WAN-Drop) korrekt mit `ether1`.
|
||||||
Interface + Kontrolle der resultierenden Regeln.
|
|
||||||
- **Neuer RouterOS-WiFi-Treiber (`/interface wifi`, wifiwave2/802.11ax)
|
- **Neuer RouterOS-WiFi-Treiber (`/interface wifi`, wifiwave2/802.11ax)
|
||||||
nicht unterstützt** — wird erkannt und im UI erklärt, aber nicht
|
nicht unterstützt** — wird erkannt und im UI erklärt, aber nicht
|
||||||
konfiguriert.
|
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,
|
durchsetzte — reiner Text ohne Wirkung. Jetzt ist Isolation ein echter,
|
||||||
optionaler Schalter mit tatsächlicher Regel-Erzeugung.
|
optionaler Schalter mit tatsächlicher Regel-Erzeugung.
|
||||||
|
|
||||||
Nur gegen Unit-Tests verifiziert (`FirewallConfigTests`: isoliertes
|
**Live gegen Hardware verifiziert (2026-09-15)** — zuerst manuell per
|
||||||
Netzwerk gegen nicht-isoliertes, gegenseitige Isolation, keine Isolation).
|
SSH nachgebaut (siehe "Zum Sessionende" oben), danach über den
|
||||||
**Noch nicht gegen echte Hardware getestet** — bräuchte zwei getrennte
|
App-Wizard selbst (`ether4`, Experte-Modus). iMac ↔ Laptop erreichen
|
||||||
Testnetze am selben Router, um die Drop-Regeln praktisch zu bestätigen.
|
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
|
## 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
|
bestätigt** (inkl. neuem "Trennen"-Button im Verbinden-Tab, der dafür
|
||||||
nötig wurde). Fehlerzustände/Politur und REST-Schreibpfad-Verifikation
|
nötig wurde). Fehlerzustände/Politur und REST-Schreibpfad-Verifikation
|
||||||
gegen ein Gerät mit aktivem `www-ssl` stehen noch aus.
|
gegen ein Gerät mit aktivem `www-ssl` stehen noch aus.
|
||||||
- 🔶 M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
|
- ✅ M8: Mehrere LAN-Interfaces mit eigenem DHCP + Netzwerk-Isolation
|
||||||
(eigene Firewall-Regeln pro LAN/VLAN) — gebaut, nur Unit-Test-verifiziert,
|
(eigene Firewall-Regeln pro LAN/VLAN) — **live gegen Hardware
|
||||||
noch nicht gegen echte Hardware getestet.
|
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/
|
- 🔶 M9: Einfach/Experte-Modus im Einrichten-Wizard — gebaut, Compile/
|
||||||
Unit-Test-verifiziert. UI (Modusumschalter selbst) noch nicht manuell
|
Unit-Test-verifiziert. UI (Modusumschalter selbst) noch nicht manuell
|
||||||
durchgeklickt — nur M10s Experte-Tab wurde das (siehe M10).
|
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
|
## Nächste Schritte
|
||||||
|
|
||||||
1. **In Arbeit, hier weitermachen:** kompletter Wizard-Durchlauf
|
1. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
|
||||||
(WAN/LAN mit zwei Interfaces ether2+ether3, eins isoliert/Firewall)
|
2. Rest von M7: Fehlerzustände/Politur, REST-Schreibtest an einem Gerät
|
||||||
ü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
|
|
||||||
mit aktivem `www-ssl`.
|
mit aktivem `www-ssl`.
|
||||||
4. M8 gegen echte Hardware testen: zwei LAN-Interfaces/VLANs anlegen, eins
|
3. M9 UI (Einfach/Experte-Modusumschalter im Einrichten-Tab selbst) noch
|
||||||
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
|
|
||||||
manuell durchklicken — M10s Experte-Tab wurde bereits vom Nutzer
|
manuell durchklicken — M10s Experte-Tab wurde bereits vom Nutzer
|
||||||
bestätigt (siehe oben), der Moduswechsel im Wizard noch nicht.
|
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.
|
(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
|
(`RouterOSFieldSchema.Kind.duration`) auch auf weitere Zeitwert-Felder
|
||||||
anwenden, die bisher nur Text mit Beispiel-Tooltip sind — WireGuard-
|
anwenden, die bisher nur Text mit Beispiel-Tooltip sind — WireGuard-
|
||||||
Peer "Keepalive", Scheduler "Intervall", Netwatch "Prüf-Intervall".
|
Peer "Keepalive", Scheduler "Intervall", Netwatch "Prüf-Intervall".
|
||||||
Optional auch für den Geräte-Tab, falls dort künftig Zeitfelder
|
Optional auch für den Geräte-Tab, falls dort künftig Zeitfelder
|
||||||
dazukommen.
|
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`
|
prüfen** — live als falsch bestätigt für `/ip dhcp-server lease`
|
||||||
(Bug 15). Betrifft potenziell jedes `.set`/`.remove` im Experte-Tab.
|
(Bug 15). Betrifft potenziell jedes `.set`/`.remove` im Experte-Tab.
|
||||||
Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig
|
Am ehesten zu prüfen: bei einem Menü mit mehreren gleichzeitig
|
||||||
vorhandenen Einträgen `:put [<Pfad> find]` und `<Pfad> print terse`
|
vorhandenen Einträgen `:put [<Pfad> find]` und `<Pfad> print terse`
|
||||||
unabhängig ausführen und die Reihenfolgen von Hand vergleichen.
|
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
|
Werkseinstellungen zurückgesetzt** ("damit wir sauber weitermachen
|
||||||
können"), und im Zuge des ersten M13-Testlaufs (Bug 18, Login-Sperre)
|
können"), und im Zuge des ersten M13-Testlaufs (Bug 18, Login-Sperre)
|
||||||
nochmal per Hardware-Reset zurückgesetzt. `test-vlan`/`testpool`/
|
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
|
— vor Annahmen über den genauen aktuellen Stand lieber neu per
|
||||||
Geräte-/Übersicht-/Verbinden-Tab prüfen statt auf ältere Einträge hier
|
Geräte-/Übersicht-/Verbinden-Tab prüfen statt auf ältere Einträge hier
|
||||||
zu vertrauen.
|
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`
|
Singleton-Menüs prüfen** — live nur für `/system routerboard`
|
||||||
bestätigt gebraucht zu werden (`/system identity` etc. liefen schon
|
bestätigt gebraucht zu werden (`/system identity` etc. liefen schon
|
||||||
vorher über den Exception-Zweig, siehe Bug 8). Nicht geprüft, ob es
|
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
|
(weder Exception noch im Output-Text) — bei einer leer bleibenden
|
||||||
Liste in Übersicht/Geräte/Experte-Tab als erste Verdachtsquelle
|
Liste in Übersicht/Geräte/Experte-Tab als erste Verdachtsquelle
|
||||||
prüfen.
|
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
|
Gitea-Remote `origin` ist eingerichtet und wird laufend gepusht (siehe
|
||||||
aufsetzt (z.B. selbst gehostet auf der vorhandenen OMV-NAS, siehe
|
oben) — dieser Hinweis war veraltet, korrigiert am 2026-09-15.
|
||||||
`Aperto/`-Projekt), Remote hinzufügen und pushen; bis dahin lokales Git.
|
|
||||||
|
|
||||||
`/Applications/RouterOS Assistant.app` ist der aktuell installierte
|
`/Applications/RouterOS Assistant.app` ist der aktuell installierte
|
||||||
Release-Build, auf dem Stand des jeweils letzten Commits auf `main`
|
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 |
|
| M5 | WLAN-Schritt | 🔶 nur "kein WLAN"-Zweig getestet |
|
||||||
| M6 | Firewall-Grundschutz | ✅ live verifiziert |
|
| M6 | Firewall-Grundschutz | ✅ live verifiziert |
|
||||||
| M7 | Härtung (SSH-Hostkey-TOFU) | 🔶 TOFU fertig, Rest offen |
|
| 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 |
|
| M9 | Einfach/Experte-Modusschalter im Wizard | 🔶 gebaut, UI-Test offen |
|
||||||
| M10 | Experte-Tab (generischer RouterOS-Zugriff) | ✅ live verifiziert |
|
| M10 | Experte-Tab (generischer RouterOS-Zugriff) | ✅ live verifiziert |
|
||||||
| M11 | Übersicht-Tab (IST-Zustand-Diagramm) | ✅ live verifiziert |
|
| M11 | Übersicht-Tab (IST-Zustand-Diagramm) | ✅ live verifiziert |
|
||||||
|
|||||||
@@ -17,7 +17,26 @@ enum DhcpServerCommandBuilder {
|
|||||||
let serverName = "dhcp_\(interfaceName)"
|
let serverName = "dhcp_\(interfaceName)"
|
||||||
let routerIP = routerAddress.components(separatedBy: "/").first ?? routerAddress
|
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(
|
RouterOSCommand.add(
|
||||||
menuPath: "/ip address",
|
menuPath: "/ip address",
|
||||||
restPath: "ip/address",
|
restPath: "ip/address",
|
||||||
@@ -51,6 +70,27 @@ enum DhcpServerCommandBuilder {
|
|||||||
"dns-server": dnsServers
|
"dns-server": dnsServers
|
||||||
],
|
],
|
||||||
summary: "DHCP-Netzwerk \(networkAddress) \(context) konfigurieren"
|
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
|
/// Menus where a duplicate `.add` should instead reconfigure the existing entry in place —
|
||||||
/// client) — re-running the wizard's WAN step on an interface that already has one makes the
|
/// re-running any wizard step against an already-configured router hits this on every menu
|
||||||
/// wizard's own `.add` fail live-confirmed: "failure: dhcp-client on that interface already
|
/// that enforces uniqueness (dhcp-client/pppoe-client: one per interface, live-confirmed
|
||||||
/// exists". Rather than hard-fail the whole apply here, retry once as a `.set` matched by
|
/// "failure: dhcp-client on that interface already exists"; pool/dhcp-server: one per name;
|
||||||
/// "interface" instead — reconfigures the existing entry in place, which is what re-running
|
/// dhcp-server network: one per address). The value is which field of the (deterministic,
|
||||||
/// the wizard on the same interface should mean anyway. Scoped to exactly these two
|
/// app-generated) arguments identifies the existing entry to match on for the `.set` retry.
|
||||||
/// known-unique-per-interface menus, not applied generally to every `.add` — other menus
|
/// "/ip address" is deliberately not here — an interface can legitimately hold several
|
||||||
/// (e.g. "/ip address") legitimately allow multiple items per interface, where silently
|
/// addresses, so matching a `.set` by "interface" alone could reconfigure the wrong one;
|
||||||
/// converting a would-be-duplicate `.add` into a `.set` would be wrong, not helpful.
|
/// see `duplicateTolerantMenuPaths` below for how that menu is handled instead.
|
||||||
private static let interfaceUniqueMenuPaths: Set<String> = ["/ip dhcp-client", "/interface pppoe-client"]
|
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 {
|
private func applyIdempotently(_ command: RouterOSCommand) async throws {
|
||||||
do {
|
do {
|
||||||
try await connectionService.apply(command)
|
try await connectionService.apply(command)
|
||||||
} catch {
|
} catch {
|
||||||
guard case .add = command.operation,
|
if case .remove = command.operation, Self.missingTolerantRemoveMenuPaths.contains(command.menuPath) {
|
||||||
Self.interfaceUniqueMenuPaths.contains(command.menuPath),
|
return
|
||||||
let interfaceName = command.arguments["interface"] else {
|
}
|
||||||
|
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
|
throw error
|
||||||
}
|
}
|
||||||
let retryCommand = RouterOSCommand.set(
|
let retryCommand = RouterOSCommand.set(
|
||||||
menuPath: command.menuPath,
|
menuPath: command.menuPath,
|
||||||
restPath: command.restPath,
|
restPath: command.restPath,
|
||||||
matchField: "interface",
|
matchField: matchField,
|
||||||
matchValue: interfaceName,
|
matchValue: matchValue,
|
||||||
arguments: command.arguments,
|
arguments: command.arguments,
|
||||||
summary: command.summary
|
summary: command.summary
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,12 +43,28 @@ final class RouterOSCommandBuilderTests: XCTestCase {
|
|||||||
let config = LanDhcpConfig()
|
let config = LanDhcpConfig()
|
||||||
let commands = config.buildCommands()
|
let commands = config.buildCommands()
|
||||||
|
|
||||||
XCTAssertEqual(commands.count, 4)
|
XCTAssertEqual(commands.count, 6)
|
||||||
XCTAssertEqual(commands[0].menuPath, "/ip address")
|
XCTAssertEqual(commands[0].menuPath, "/ip address")
|
||||||
XCTAssertEqual(commands[1].menuPath, "/ip pool")
|
XCTAssertEqual(commands[1].menuPath, "/ip pool")
|
||||||
XCTAssertEqual(commands[2].menuPath, "/ip dhcp-server")
|
XCTAssertEqual(commands[2].menuPath, "/ip dhcp-server")
|
||||||
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server network")
|
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server network")
|
||||||
XCTAssertEqual(commands[3].arguments["gateway"], "192.168.88.1")
|
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() {
|
func testCliLineRendersSortedQuotedArgumentsForAdd() {
|
||||||
|
|||||||
@@ -6,17 +6,21 @@ final class VlanEntryTests: XCTestCase {
|
|||||||
let vlan = VlanEntry(name: "Gäste", vlanID: 20, parentInterface: "bridge")
|
let vlan = VlanEntry(name: "Gäste", vlanID: 20, parentInterface: "bridge")
|
||||||
let commands = vlan.buildCommands()
|
let commands = vlan.buildCommands()
|
||||||
|
|
||||||
XCTAssertEqual(commands.count, 5)
|
XCTAssertEqual(commands.count, 8)
|
||||||
XCTAssertEqual(commands[0].menuPath, "/interface vlan")
|
XCTAssertEqual(commands[0].menuPath, "/interface vlan")
|
||||||
XCTAssertEqual(commands[0].arguments["vlan-id"], "20")
|
XCTAssertEqual(commands[0].arguments["vlan-id"], "20")
|
||||||
XCTAssertEqual(commands[0].arguments["interface"], "bridge")
|
XCTAssertEqual(commands[0].arguments["interface"], "bridge")
|
||||||
XCTAssertEqual(commands[0].arguments["name"], "vlan20")
|
XCTAssertEqual(commands[0].arguments["name"], "vlan20")
|
||||||
|
|
||||||
XCTAssertEqual(commands[1].menuPath, "/ip address")
|
XCTAssertEqual(commands[1].menuPath, "/interface bridge port")
|
||||||
XCTAssertEqual(commands[1].arguments["interface"], "vlan20")
|
XCTAssertEqual(commands[2].menuPath, "/ip address")
|
||||||
XCTAssertEqual(commands[2].menuPath, "/ip pool")
|
XCTAssertEqual(commands[2].arguments["interface"], "vlan20")
|
||||||
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server")
|
XCTAssertEqual(commands[3].menuPath, "/ip pool")
|
||||||
XCTAssertEqual(commands[4].menuPath, "/ip dhcp-server network")
|
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() {
|
func testDefaultAddressesAreDerivedFromVlanID() {
|
||||||
|
|||||||
Reference in New Issue
Block a user