M7 als teilweise erledigt markiert (implementiert, aber noch nicht gegen echte Hardware getestet), alte lo-Regel-Einschränkung entfernt (durch Werksreset des Testgeräts erledigt), nächste Schritte um den neuen TOFU-Dialog beim ersten Verbinden ergänzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
14 KiB
RouterOS Assistant — Handoff
Stand: App-Icon fertig, alle Kern-Features (M1–M6) gebaut und mindestens
teilweise gegen zwei echte physische Mikrotik-Testgeräte verifiziert
(ein generisches Gerät für M1–M4, ein hEX-Router für M5/M6 — dabei ein
kritischer Parser-Bug gefunden und gefixt, siehe unten). Danach noch
mehrere Nutzer-Wünsche umgesetzt: Tooltips, wählbarer Backup-Ordner,
Schnell-Backup im Verbinden-Tab, "Werkseinstellungen wiederherstellen",
eigenes App-Icon. Release-Build liegt unter
/Applications/RouterOS Assistant.app.
Ziel
Native macOS-App (SwiftUI), die Laien per geführtem Interview-Wizard durch
die häufigsten Mikrotik-RouterOS-Konfigurationen führt: Internet-Anschluss,
Heimnetzwerk/DHCP, zusätzliche VLAN-Netzwerke, WLAN, Firewall-Grundschutz,
plus Backup/Wiederherstellung. Vollständiger ursprünglicher Plan/Kontext:
~/.claude/plans/purrfect-puzzling-bunny.md (seither an vielen Stellen
weiterentwickelt, siehe unten).
Setup / Bauen
cd RouterOS
xcodegen generate # erzeugt RouterOSAssistant.xcodeproj (nicht in git)
open RouterOSAssistant.xcodeproj # oder direkt per xcodebuild
# Debug (Xcode Cmd+R empfohlen für manuelle/Live-Tests, siehe Gatekeeper-Hinweis unten)
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
-destination 'platform=macOS' build
# Nur kompilieren, nicht ausführen (umgeht den Gatekeeper-Hang, siehe unten)
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
-destination 'platform=macOS' build-for-testing
# Release + Deploy nach /Applications
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
-configuration Release build
rm -rf "/Applications/RouterOS Assistant.app"
cp -R "<DerivedData>/Build/Products/Release/RouterOSAssistant.app" \
"/Applications/RouterOS Assistant.app"
killall Dock # Icon-Cache auffrischen, falls sich nur das Icon geändert hat
xcodegen ist via Homebrew installiert. Projekt-Struktur/Targets/Package-
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).
.xcodeproj, DerivedData, .build, Firmware-Dateien (*.npk, *.cpgz)
und Router-Backups (*.rsc, /Backups/) sind gitignored.
Architektur
RouterOSAssistant/
App/RouterOSAssistantApp.swift — 3 Tabs, teilen sich EINE ConnectionService-Instanz
Core/
Models/
RouterOSCommand.swift — eine Änderung, zwei Operationen (.add/.set), zwei Renderer (CLI-Zeile / REST-JSON)
WanConfig.swift, LanDhcpConfig.swift, VlanEntry.swift, WifiNetworkConfig.swift, FirewallConfig.swift — bauen je RouterOSCommand-Listen
DhcpServerCommandBuilder.swift — geteilte "Adresse+Pool+Server+Netzwerk"-Logik (LAN + VLAN)
RouterOSModels.swift — Credentials, DeviceInfo, Interface, RouterOSError
Networking/
RouterOSTransport.swift — Protocol: connect/fetchDeviceInfo/fetchInterfaces/fetchFirewallRuleCounts/apply/disconnect
RestTransport.swift — REST-API (RouterOS ≥7.1), Zertifikats-TOFU, .set via GET+PATCH (findItemID)
SSHTransport.swift — SSH-Fallback via Citadel, CLI-Text-Parsing, resetToFactoryDefaults(), eigene Hostkey-TOFU
RouterOSCliParser.swift — parst `/system resource print` und `/interface print terse`
CertificateTrustStore.swift / CertificateFingerprint.swift — TOFU für REST-Zertifikate
SSHHostKeyTrustStore.swift / SSHHostKeyFingerprint.swift — TOFU für SSH-Hostkeys (M7)
Services/
ConnectionService.swift — zentraler App-State: REST-zuerst-SSH-Fallback, hält credentials/interfaces/deviceInfo
BackupService.swift — Config-Export (`/export terse`) über eigene SSH-Verbindung, wählbarer Zielordner
FactoryResetService.swift — /system reset-configuration über eigene SSH-Verbindung ("Gefahrenzone")
KeychainService.swift — Passwort-Speicherung
Features/
Wizard/Steps/Connect/ — Verbinden-Tab (inkl. Schnell-Backup-Button nach erfolgreicher Verbindung)
Wizard/Steps/Setup/ — Einrichten-Tab: Wan → Lan → Vlan → Wifi → Firewall → Review/Apply
Backup/ — Sicherungen-Tab (Ordner wählen, Gefahrenzone: Werkseinstellungen wiederherstellen)
Resources/Assets.xcassets/AppIcon.appiconset/ — App-Icon "Signal Router" (16px–1024px)
RouterOSAssistantTests/ — reine Unit-Tests (Command-Builder, CLI-Parser, Fallback-Logik via Mock-Transport)
Architekturprinzip RouterOSCommand (.add/.set): .add legt neue
Einträge an (CLI ... add key=value, REST POST). .set ändert
bestehende Einträge, per matchField/matchValue gefunden — CLI löst das
inline über set [find field=value] ..., REST muss dafür erst per GET das
Item suchen, seine RouterOS-interne .id auslesen, dann
PATCH restPath/<id> schicken (RestTransport.findItemID). Eingeführt für
WLAN (M5), weil Wireless-Interfaces schon vor jeder Konfiguration
existieren. VLAN (M4) hat trotzdem weiterhin keine Port-Zuweisung
(Access/Trunk) — technisch jetzt machbar, aber bewusste Scope-Entscheidung,
nicht nachgezogen.
System-Aktionen (Backup, Werksreset) laufen immer über eine eigene
SSH-Verbindung, unabhängig vom aktiven Live-Transport — BackupService
und FactoryResetService folgen demselben Muster: es sind einmalige
System-Kommandos, kein Menü-Item zum Hinzufügen/Ändern, passen also nicht
ins RouterOSCommand-Modell und haben keine verifizierte REST-Entsprechung.
ConnectionService ist der einzige geteilte State. Alle drei Tabs
bekommen dieselbe Instanz von der App-Ebene injiziert (kein
@EnvironmentObject, explizite Übergabe im Init). SwiftUI-Falle: jede
View, die connectionService-Felder liest, muss ihn selbst als
@ObservedObject halten — nicht nur transitiv über ein anderes ViewModel
erreichen (siehe Bug 1 unten).
Gefundene Bugs (chronologisch, alle gegen echte Geräte)
- ConnectView zeigte nie Verbindungsstatus —
connectionServicewar nur überviewModel.connectionServiceerreichbar (zwei Ebenen tief verschachteltes ObservableObject), SwiftUI abonniert das nicht automatisch. Fix:ConnectViewhältconnectionServicezusätzlich selbst als@ObservedObject. NIOSSHError.localizedDescriptionnutzlos — bridged auf generisches NSError ("error 1"). Fix:String(describing:).- SSH scheiterte mit
keyExchangeNegotiationFailure— RouterOS bietet nur Legacy-Algorithmen an, Citadel-Default enthält sie nicht. Fix:algorithms: .all. Citadel.SSHClient.CommandFailedzeigte nur den Exit-Code —executeCommand()verwirft die gesammelte Ausgabe bei Exit-Code ≠ 0. Fix: eigene Sammlung überexecuteCommandStream().- Picker-Warnungen durch Platzhalter-Interfacenamen —
prepareDefaults()korrigiert jetzt WAN und LAN, läuft schon iminit. - Kritisch: CLI-Parser verlor 6 von 7 Interfaces auf einem hEX-Router —
parseInterfaces/parseDeviceInfosplitteten nur auf"\n"; die SSH-Ausgabe dieses Geräts trennte Zeilen anders, sodass alle Interface-Zeilen zu einer verschmolzen — beim key=value-Parsen überschrieb jedes Feld den vorherigen Wert, übrig blieb nur das letzte Interface im Text (lo, Loopback) mit vermischten Werten. Folge: jede WAN-Interface-Referenz in den angewendeten Firewall-Regeln zeigte auflostatt den echten WAN-Port — auf diesem Testgerät folgenlos, weil RouterOS bereits eine vollständige eigene Standard-Firewall (defconf) mitbrachte, die weiterhin schützte; auf einem Gerät ohne bestehende Firewall wäre das eine wirkungslose, aber als aktiv angezeigte Firewall gewesen. Fix:split(whereSeparator: \.isNewline)(robust gegen\n/\r/\r\n) + zusätzliches Sicherheitsnetz: Loopback-Interfaces werden inSetupViewjetzt grundsätzlich aus allen WAN/LAN/VLAN- Auswahllisten gefiltert. Regressionstest mit echten\r\n-getrennten hEX-Daten inRouterOSCliParserTests.
Lehren: Citadel/NIOSSH-Fehler immer mit String(describing:) loggen,
nie .localizedDescription. Jede View, die ein ObservableObject aus einem
ViewModel liest, muss es selbst separat als @ObservedObject halten.
Zeilen-Splitting bei SSH-Ausgaben grundsätzlich mit \.isNewline statt
festem "\n", da sich das je nach Gerät/Version unterscheiden kann.
xcodebuild test hängt — Gatekeeper, kein Code-Bug
xcodebuild ... test kann exakt beim App-Start für ~5 Minuten hängen,
dann "The test runner hung before establishing connection.". Ursache
(per log show, Filter auf syspolicyd/amfid): ad-hoc-signierte
Binaries lösen bei syspolicyd einen Live-Netzwerk-Check
(GK performScan) gegen Apples Gatekeeper-Server aus; jeder Neu-Build
ändert den Binary-Hash und kann einen neuen, manchmal langsamen Check
auslösen. Betrifft nur den CLI-Weg — Xcodes Cmd+R-Weg bekommt vom
Debugger-Start eine Gatekeeper-Ausnahme. Deshalb: bei Hängern erst
xcodebuild ... build-for-testing (kompiliert nur, kein App-Start, kein
Gatekeeper-Trigger) für den Compile-Check nutzen, echte Testläufe/manuelle
Verifikation dem Nutzer über Xcode überlassen statt CLI-Versuche zu
wiederholen.
Bekannte Einschränkungen (bewusst, nicht vergessen)
- REST-Pfad ungetestet für Schreibvorgänge — auf beiden bisherigen
Testgeräten war
www-ssl(Port 443) aus, jeder Schreibtest lief über SSH. Der REST-apply()-Pfad (POST/PATCH,findItemIDfür.set) ist nur gegen Mocks getestet. - WLAN-
.set-Pfad (M5) weiterhin ungetestet gegen echte Hardware — nur der "kein WLAN"-Zweig ist bestätigt (zwei Testgeräte, beide ohne WLAN-Chip). Sicherheitsprofil-Anlage + SSID/Passwort-.setauf 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
lostatt 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. - Neuer RouterOS-WiFi-Treiber (
/interface wifi, wifiwave2/802.11ax) nicht unterstützt — wird erkannt und im UI erklärt, aber nicht konfiguriert. - Kein garantiertes Auto-Rollback bei Verbindungsabbruch während "Jetzt anwenden" — nur Backup-vorher + Bestätigungspflicht.
- App ist ad-hoc signiert, nicht notarisiert — beim ersten Start einer
frisch nach
/Applicationskopierten Version zeigt macOS die "nicht verifizierter Entwickler"-Warnung (Rechtsklick → Öffnen nötig). - SSH-Hostkey-TOFU (neu) noch nicht gegen echte Hardware getestet —
Fingerprint-Berechnung ist per Unit-Test gegen
ssh-keygenverifiziert, aber der komplette Verbindungs-Flow (erste Verbindung zeigt den "Unbekannter SSH-Schlüssel"-Dialog, Bestätigen merkt sich den Fingerabdruck, zweite Verbindung läuft ohne Rückfrage durch) lief noch nie gegen ein reales Gerät. Erwartet: nach dem Werksreset des hEX-Testgeräts zeigt die nächste Verbindung diesen Dialog garantiert einmalig neu (kein Bug, sondern der erste TOFU-Moment für diesen Host).
Stand der Milestones
- ✅ M1–M4: Projektgerüst, Connect, Backup, WAN/LAN/DHCP, VLAN — gegen echtes Testgerät verifiziert.
- ✅ M5: WLAN-Schritt — nur "kein WLAN"-Zweig verifiziert,
.set-Pfad weiterhin ungetestet (kein WLAN-Chip auf beiden Testgeräten bisher). - ✅ M6: Firewall-Schritt — live angewendet, dabei Bug 6 gefunden+gefixt; Interface-Auswahl danach erneut bestätigt, Regel-Ergebnis mit korrektem WAN-Port aber noch nicht erneut kontrolliert.
- ✅ Zusatzfeatures nach M6: Tooltips auf allen Konfigurationsfeldern,
wählbarer Backup-Ordner (Standard:
~/Library/Application Support/...), Schnell-Backup-Button im Verbinden-Tab, "Werkseinstellungen wiederherstellen" (Gefahrenzone im Sicherungen-Tab,/system reset-configuration no-defaults=no), eigenes App-Icon ("Signal Router"-Motiv). - 🔶 M7: Härtung — SSH-Hostkey-TOFU implementiert, ungetestet gegen
echte Hardware (siehe Einschränkungen); Fehlerzustände/Politur und
REST-Schreibpfad-Verifikation gegen ein Gerät mit aktivem
www-sslstehen noch aus.
Nächste Schritte
- hEX-Testgerät wurde per Werksreset zurückgesetzt (räumt auch die alten
"lo"-Firewallregeln auf) — den kompletten Wizard einmal neu durchlaufen
(WAN/LAN/VLAN/WLAN/Firewall) und dabei gezielt den neuen
SSH-Hostkey-TOFU-Dialog beim ersten Verbinden bestätigen; danach
Firewall-Ergebnis mit korrektem WAN-Port per
/ip firewall filter print//ip firewall nat printkontrollieren. - WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
- Rest von M7: Fehlerzustände/Politur, REST-Schreibtest an einem Gerät
mit aktivem
www-ssl.
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.
/Applications/RouterOS Assistant.app ist der aktuell installierte
Release-Build (Stand: Commit 008ee65, App-Icon "Signal Router"). Nach
jeder weiteren Code-Änderung muss dieser Build-und-Kopier-Schritt (siehe
oben) wiederholt werden, damit die installierte App aktuell bleibt.