Files
RouterOS/HANDOFF.md
T
KayandClaude Sonnet 5 9e6a38a5c5 Handoff + Chatlog auf aktuellen Stand gebracht (Session-Ende)
HANDOFF.md komplett überarbeitet: M5/M6-Live-Test-Ergebnisse, den
kritischen Interface-Parser-Bug (Bug 6) inkl. Ursache und Fix, neue
Zusatzfeatures (Tooltips, wählbarer Backup-Ordner, Schnell-Backup im
Verbinden-Tab, Werkseinstellungen-Reset, App-Icon), aktualisierte
Einschränkungen (Firewall-Regeln mit korrektem WAN-Port noch nicht
erneut kontrolliert, alte "lo"-Regeln evtl. noch auf dem Testgerät) und
konkrete nächste Schritte.

CHATLOG.md um die komplette Fortsetzung ergänzt: M5/M6-Bau, Live-Test
mit dem hEX-Gerät, das gemeinsame Debugging des Interface-Bugs
(PTY-Verdacht widerlegt durch Nutzer-Test, echte Ursache im Parser
gefunden), sowie alle Nutzer-Wünsche danach (Tooltips, Backup-Pfad,
Schnell-Backup, Werksreset, macOS-Deploy, App-Icon).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
2026-09-12 21:49:50 +02:00

13 KiB
Raw Blame History

RouterOS Assistant — Handoff

Stand: App-Icon fertig, alle Kern-Features (M1M6) gebaut und mindestens teilweise gegen zwei echte physische Mikrotik-Testgeräte verifiziert (ein generisches Gerät für M1M4, 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()
      RouterOSCliParser.swift          — parst `/system resource print` und `/interface print terse`
      CertificateTrustStore.swift / CertificateFingerprint.swift  — TOFU nur für REST bisher
    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" (16px1024px)
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)

  1. ConnectView zeigte nie VerbindungsstatusconnectionService war nur über viewModel.connectionService erreichbar (zwei Ebenen tief verschachteltes ObservableObject), SwiftUI abonniert das nicht automatisch. Fix: ConnectView hält connectionService zusätzlich selbst als @ObservedObject.
  2. NIOSSHError.localizedDescription nutzlos — bridged auf generisches NSError ("error 1"). Fix: String(describing:).
  3. SSH scheiterte mit keyExchangeNegotiationFailure — RouterOS bietet nur Legacy-Algorithmen an, Citadel-Default enthält sie nicht. Fix: algorithms: .all.
  4. Citadel.SSHClient.CommandFailed zeigte nur den Exit-CodeexecuteCommand() verwirft die gesammelte Ausgabe bei Exit-Code ≠ 0. Fix: eigene Sammlung über executeCommandStream().
  5. Picker-Warnungen durch Platzhalter-InterfacenamenprepareDefaults() korrigiert jetzt WAN und LAN, läuft schon im init.
  6. Kritisch: CLI-Parser verlor 6 von 7 Interfaces auf einem hEX-RouterparseInterfaces/parseDeviceInfo splitteten 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 auf lo statt 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 in SetupView jetzt grundsätzlich aus allen WAN/LAN/VLAN- Auswahllisten gefiltert. Regressionstest mit echten \r\n-getrennten hEX-Daten in RouterOSCliParserTests.

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)

  • SSH-Hostkey-TOFU fehltSSHTransport nutzt .acceptAnything(), kein Trust-on-first-use wie bei REST. M7-Punkt, im Code kommentiert.
  • 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, findItemID fü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-.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.
  • Alte, fehlerhafte "lo"-Regeln stehen eventuell noch auf dem hEX-Testgerät — Aufräum-Befehle wurden dem Nutzer gegeben (/ip firewall nat remove [find out-interface=lo], /ip firewall filter remove [find in-interface=lo]), Ausführung nicht bestätigt.
  • 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 /Applications kopierten Version zeigt macOS die "nicht verifizierter Entwickler"-Warnung (Rechtsklick → Öffnen nötig).

Stand der Milestones

  • M1M4: 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, Fehlerzustände, Politur, REST- Schreibpfad gegen ein Gerät mit aktivem www-ssl verifizieren.

Nächste Schritte

  1. Auf dem hEX-Testgerät die alten "lo"-Firewallregeln aufräumen (siehe oben) und den Firewall-Schritt mit jetzt korrekt erkanntem WAN-Port erneut anwenden + Ergebnis per /ip firewall filter print / /ip firewall nat print kontrollieren.
  2. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
  3. M7-Härtung, insbesondere SSH-Hostkey-TOFU und ein 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.