Files
RouterOS/HANDOFF.md
T
KayandClaude Sonnet 5 da5133245e Handoff: M6-Stand + Gatekeeper-Testrunner-Hang dokumentiert
Neuer Abschnitt erklärt den xcodebuild-test-Hang (amfid/syspolicyd
Gatekeeper-Netzwerk-Check bei ad-hoc-signierten Binaries) als
Tooling-Eigenheit, nicht Code-Bug, und wie man ihn umgeht
(build-for-testing zum Compile-Check, echte Verifikation über Xcode).

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

11 KiB
Raw Blame History

RouterOS Assistant — Handoff

Stand: nach M6 (Firewall-Schritt). M1M4 gegen ein echtes physisches Mikrotik-Testgerät (RouterOS 7.24.2) verifiziert. M5: nur der "kein-WLAN-erkannt"-Zweig ist bestätigt (siehe Einschränkungen unten). M6 ist gebaut, Build + Test-Compile grün, aber noch nicht gegen echte Hardware angewendet — der Nutzer verifiziert diesen Schritt direkt in Xcode (Cmd+R), da xcodebuild test von der Kommandozeile aktuell an einem macOS-Gatekeeper-Netzwerk-Check hängt (siehe eigener Abschnitt unten, kein Code-Bug).

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, (geplant: Firewall). Vollständiger Plan/Kontext: ~/.claude/plans/purrfect-puzzling-bunny.md (ursprüngliche Architekturentscheidung — seither an mehreren Stellen weiterentwickelt, siehe unten).

Setup / Bauen

cd RouterOS
xcodegen generate                 # erzeugt RouterOSAssistant.xcodeproj (nicht in git)
open RouterOSAssistant.xcodeproj  # oder direkt per xcodebuild

xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
  -destination 'platform=macOS' build
xcodebuild -project RouterOSAssistant.xcodeproj -scheme RouterOSAssistant \
  -destination 'platform=macOS' test

xcodegen ist via Homebrew installiert (brew install xcodegen). 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 sowie die vorhandenen Firmware-Dateien (*.npk, *.cpgz) 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/apply/disconnect
      RestTransport.swift              — REST-API (RouterOS ≥7.1), Zertifikats-TOFU
      SSHTransport.swift                — SSH-Fallback via Citadel, CLI-Text-Parsing
      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
      KeychainService.swift            — Passwort-Speicherung
  Features/
    Wizard/Steps/Connect/              — Verbinden-Tab
    Wizard/Steps/Setup/                — Einrichten-Tab: Wan → Lan → Vlan → Wifi → Firewall → Review/Apply
    Backup/                            — Sicherungen-Tab
RouterOSAssistantTests/                 — reine Unit-Tests (Command-Builder, CLI-Parser, Fallback-Logik via Mock-Transport)

Wichtiges Architekturprinzip (seit M5 erweitert): RouterOSCommand kennt zwei Operationen: .add (neuer Eintrag — CLI ... add key=value, REST POST) und .set (bestehenden Eintrag ändern, per matchField/ matchValue gefunden — CLI ... set [find field=value] key=value löst das inline, REST muss dafür erst per GET das Item suchen, seine RouterOS-interne .id auslesen, dann PATCH restPath/<id> schicken, siehe RestTransport.findItemID). Eingeführt für M5 (WLAN), weil Wireless-Interfaces schon vor jeder Konfiguration existieren — kein "add" möglich. VLAN (M4) hat trotzdem weiterhin keine Port-Zuweisung (Access/Trunk): das bräuchte zusätzlich RouterOS Bridge-VLAN-Filtering, technisch jetzt machbar, aber bewusst nicht rückwirkend nachgezogen (Scope-Entscheidung, nicht mehr Architektur-Zwang).

ConnectionService ist der einzige geteilte State. Alle drei Tabs bekommen dieselbe Instanz von der App-Ebene injiziert (kein @EnvironmentObject, explizite Übergabe im Init). Achtung SwiftUI-Falle (siehe unten): jede View, die connectionService-Felder liest, muss ihn selbst als @ObservedObject halten — nicht nur transitiv über ein anderes ViewModel erreichen.

Was während der Live-Tests kaputt war und wie es gefixt wurde

Reihenfolge der Bugs, alle gegen echtes Testgerät gefunden, jeweils als eigener Commit (git log zeigt Details):

  1. ConnectView zeigte nie VerbindungsstatusconnectionService war nur über viewModel.connectionService erreichbar (zwei Ebenen tief verschachteltes ObservableObject). SwiftUI abonniert automatisch nur Objekte, die eine View direkt per @StateObject/@ObservedObject hält. Fix: ConnectView hält connectionService jetzt zusätzlich selbst.
  2. NIOSSHError.localizedDescription ist nutzlos — bridged auf generisches NSError ("error 1"), echte Diagnose steckt in CustomStringConvertible. Fix: String(describing:) statt .localizedDescription.
  3. SSH-Verbindung scheiterte mit keyExchangeNegotiationFailure — RouterOS bietet nur Legacy-Algorithmen (diffie-hellman-group14-sha1, RSA-Hostkeys) an, Citadels Default-Algorithmenliste enthält die nicht. Fix: algorithms: .all beim SSHClient.connect(...).
  4. Citadel.SSHClient.CommandFailed zeigte nur den Exit-Code — Citadels executeCommand() verwirft die gesammelte Ausgabe, sobald der Exit-Code ≠ 0 ist — genau der Text, den RouterOS bei einem Fehler zurückgibt. Fix: eigene Sammlung über executeCommandStream() in SSHTransport.run(_:), damit die Ausgabe bis zum Fehlschlag erhalten bleibt.
  5. Picker-Warnungen ("selection is invalid and does not have an associated tag") — wanConfig/lanConfig starteten mit festen Platzhaltern ("ether1"/"bridge"), die nicht zu den echten Interface-Namen des Geräts passen mussten. Fix: prepareDefaults() korrigiert jetzt beide anhand eines "ist der Wert überhaupt in der Liste"-Checks, läuft schon im init.

Lehre für weitere Arbeit: Citadel/NIOSSH-Fehler grundsätzlich mit String(describing:) loggen, nie .localizedDescription vertrauen. Jede neue View, die ein ObservableObject aus einem ViewModel liest, muss es selbst separat als @ObservedObject halten.

xcodebuild test hängt — Gatekeeper, kein Code-Bug

Bei M6 hing xcodebuild ... test dreimal reproduzierbar exakt beim App-Start für ~5 Minuten, dann "The test runner hung before establishing connection.". Ursache laut log show (Filter auf RouterOSAssistant/ syspolicyd/amfid): amfid markiert das ad-hoc-signierte Binary als "signed by an unknown certificate chain", syspolicyd macht daraufhin einen GK performScan mit einer echten Netzwerkverbindung zu Apples Gatekeeper-Servern — normalerweise sehr schnell, an diesem Tag aber hängend/langsam. Jeder Neu-Build ändert den Binary-Hash, jeder Hash braucht (in der Theorie) einen neuen Check.

Betrifft nur den CLI-Weg (xcodebuild test, wodurch die App ohne Debugger startet). Xcodes eigener Cmd+R-Weg ist davon nicht betroffen (Debugger-gestützter Start bekommt von Xcode eine Gatekeeper-Ausnahme). Deshalb: bei hartnäckigen xcodebuild test-Hängern erst xcodebuild ... build-for-testing probieren (kompiliert nur, startet die App nicht, kein Gatekeeper-Trigger) um Compile-Fehler auszuschließen, und den eigentlichen Testlauf/die 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. Kommentiert im Code als M7-Punkt.
  • CLI-Parser (RouterOSCliParser) ist jetzt gegen ein reales Gerät verifiziert (Verbinden-Tab zeigte korrekte Modell/Version/Interfaces), aber nur gegen dieses eine Testgerät/diese RouterOS-Version.
  • Kein garantiertes Auto-Rollback bei Verbindungsabbruch während "Jetzt anwenden" — nur Backup-vorher + Bestätigungspflicht. Steht auch im UI-Text auf dem Übersichtsschritt.
  • VLAN ohne Port-Zuweisung (siehe oben, Architekturprinzip) — bewusste Scope-Entscheidung, nicht mehr technisch erzwungen.
  • REST-Pfad ungetestet für Schreibvorgänge — beim Testgerät ist www-ssl (Port 443) aus, daher lief jeder bisherige Schreibtest über SSH. Der REST-apply()-Pfad (POST/PATCH mit JSON-Body, findItemID für .set) ist nur gegen Mocks getestet, nicht gegen ein echtes Gerät mit aktiver REST-API.
  • WLAN-.set-Pfad (M5) ungetestet gegen echte Hardware — der "kein WLAN"-Zweig ist am echten Testgerät bestätigt (kein WLAN-Chip, korrekt erkannt), aber Sicherheitsprofil-Anlage + SSID/Passwort-.set auf einem echten /interface wireless-Interface noch nie gegen ein Gerät mit WLAN-Chip gelaufen. Vor Vertrauen in diesen Pfad unbedingt an einem Gerät mit Legacy-WLAN testen.
  • Neuer RouterOS-WiFi-Treiber (/interface wifi, wifiwave2/802.11ax) nicht unterstützt — wird erkannt und im UI erklärt, aber nicht konfiguriert. Eigener Umbau nötig (andere Menüstruktur), falls gebraucht.

Stand der Milestones

  • M1: Projektgerüst, Connect-Schritt (REST/SSH-Autodetect, Keychain, Zertifikats-TOFU)
  • M2: Backup-Service, Sicherungen-Tab
  • M3: WAN + LAN/DHCP-Wizard, Anwenden-Logik mit Vorab-Backup
  • M4: VLAN-Schritt (separates virtuelles Netz, kein Port-Tagging)
  • M5: WLAN-Schritt (SSID/Passwort, echte .set-Operation im Befehlsmodell, nur bei erkanntem Legacy-Wireless; neuer WiFi-Treiber erkannt aber nicht unterstützt) — "kein WLAN"-Zweig gegen echtes Gerät bestätigt, der eigentliche SSID/Passwort-.set-Pfad weiterhin ungetestet (kein WLAN-Chip am Testgerät)
  • M6: Firewall-Schritt (opt-in, Standard aus; NAT/Masquerade + sicherer Filter-Standard mit place-before-Sortierung; zeigt vorhandene Regelanzahl vor dem Anwenden) — gebaut, noch nicht gegen echte Hardware angewendet, Nutzer verifiziert selbst über Xcode
  • M7: Härtung — SSH-Hostkey-TOFU, Fehlerzustände, Politur, ggf. REST-Schreibpfad gegen echtes Gerät mit aktivem www-ssl verifizieren

Nächste Schritte

M6 (Firewall) am Testgerät (Ersatzgerät, nicht Produktiv-Router) anwenden und danach die Regel-Reihenfolge prüfen (/ip firewall filter print, /ip firewall nat print) — das ist der bisher riskanteste Schritt und noch nie live gelaufen. Danach M5 (WLAN) nachholen, sobald ein Gerät mit echtem WLAN-Chip verfügbar ist. Dann M7.

Kein Gitea-Remote vorhanden — falls der Nutzer später eine Gitea-Instanz aufsetzt (z.B. selbst gehostet auf der vorhandenen OMV-NAS, wo bereits andere Docker-Dienste laufen, siehe Aperto/-Projekt), Remote hinzufügen und pushen; bis dahin bleibt es bei lokalem Git.