Handoff-Dokument: Architektur, gefixte Bugs, bekannte Einschränkungen
Fasst Stand nach M1-M4 zusammen: Aufbau, alle Live-Test-Fixes gegen echtes Testgerät (SwiftUI-Observation, NIOSSHError/CommandFailed- Fehlertexte, SSH-Algorithmen, Picker-Defaults), bewusste Scope- Entscheidungen (VLAN ohne Port-Tagging) und offene Punkte für M5-M7. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
# RouterOS Assistant — Handoff
|
||||
|
||||
Stand: nach M4 (VLAN-Schritt), alle Milestones M1–M4 gegen ein echtes
|
||||
physisches Mikrotik-Testgerät (RouterOS 7.24.2) verifiziert.
|
||||
|
||||
## 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, (geplant: WLAN, Firewall).
|
||||
Vollständiger Plan/Kontext: `~/.claude/plans/purrfect-puzzling-bunny.md`
|
||||
(ursprüngliche Architekturentscheidung — seither an mehreren Stellen
|
||||
weiterentwickelt, siehe unten).
|
||||
|
||||
## Setup / Bauen
|
||||
|
||||
```bash
|
||||
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 Renderer (CLI-Zeile / REST-JSON)
|
||||
WanConfig.swift, LanDhcpConfig.swift, VlanEntry.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 → Review/Apply
|
||||
Backup/ — Sicherungen-Tab
|
||||
RouterOSAssistantTests/ — reine Unit-Tests (Command-Builder, CLI-Parser, Fallback-Logik via Mock-Transport)
|
||||
```
|
||||
|
||||
**Wichtiges Architekturprinzip:** `RouterOSCommand` unterstützt nur "add"
|
||||
(CLI: `... add key=value ...`, REST: `POST`). Kein "set"/PATCH auf
|
||||
bestehende Einträge — deshalb hat der VLAN-Schritt keine Port-Zuweisung
|
||||
(Access/Trunk), das bräuchte RouterOS Bridge-VLAN-Filtering mit `set`.
|
||||
Mit dem Nutzer abgestimmt, bewusste Scope-Entscheidung.
|
||||
|
||||
**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 Verbindungsstatus** — `connectionService` 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.
|
||||
|
||||
## Bekannte Einschränkungen (bewusst, nicht vergessen)
|
||||
|
||||
- **SSH-Hostkey-TOFU fehlt** — `SSHTransport` 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).
|
||||
- **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` mit JSON-Body) ist nur gegen Mocks
|
||||
getestet, nicht gegen ein echtes Gerät mit aktiver REST-API.
|
||||
|
||||
## 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, nur bei erkannter WLAN-Hardware)
|
||||
- ⬜ M6: Firewall-Schritt (sicherer Standard: NAT/Masquerade, WAN→LAN blocken)
|
||||
- ⬜ M7: Härtung — SSH-Hostkey-TOFU, Fehlerzustände, Politur, ggf. REST-Schreibpfad
|
||||
gegen echtes Gerät mit aktivem `www-ssl` verifizieren
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
M5 (WLAN) oder M6 (Firewall) als Nächstes — Nutzer hat noch nicht
|
||||
festgelegt, was zuerst kommt. Vor Firewall-Arbeit besonders vorsichtig
|
||||
sein: falsch gesetzte Regeln können den Fernzugriff auf den Router kappen,
|
||||
unbedingt Backup-Pflicht vor Anwenden beibehalten und im Testgerät bleiben,
|
||||
nicht am Produktivrouter.
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user