Files
RouterOS/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupViewModel.swift
T
KayandClaude Sonnet 5 772549c991 M6: Firewall-Schritt (opt-in Sicherheits-Grundschutz)
Standardmäßig aus (Toggle wie VLAN) -- höchstes Risiko aller bisherigen
Schritte, falsche Regeln können Fernzugriff kappen. Preset ist
Mikrotiks eigener Standard-Ansatz (unverändert seit Jahren in
RouterOS-Werkskonfigurationen): NAT/Masquerade auf WAN, established/
related erlauben, invalid verwerfen, unaufgeforderte WAN-Verbindungen
zu LAN-Geräten blocken (außer explizitem Port-Forward via
connection-nat-state=!dstnat).

Jede neue Regel bekommt ein place-before mit aufsteigendem Index,
damit sie vor eventuell schon vorhandenen Regeln des Routers landet --
sonst könnte eine bereits vorhandene "alles blocken"-Regel unsere
neuen Regeln wirkungslos machen. NAT und Filter sind getrennte,
unabhängig nummerierte RouterOS-Listen.

Vor dem Anwenden zeigt der Schritt die Anzahl bereits vorhandener
Filter-/NAT-Regeln (neuer fetchFirewallRuleCounts()-Aufruf in
RouterOSTransport/RestTransport/SSHTransport/ConnectionService) --
Transparenz, bevor auf einem möglicherweise schon konfigurierten
Router weitere Regeln landen. Nutzer-Entscheidung, extra Lese-Aufruf
in Kauf zu nehmen statt nur Warntext.

Build + Test-Compile (build-for-testing) sind grün. Der eigentliche
Testlauf (xcodebuild test) hängt aktuell an einem macOS-Gatekeeper-
Netzwerk-Check für ad-hoc-signierte Binaries (amfid: "adhoc signed or
signed by an unknown certificate chain", GK performScan über
syspolicyd) -- kein Code-Bug, tritt nur bei CLI-Testläufen auf, nicht
beim normalen Xcode-Cmd+R-Weg. Nutzer verifiziert M6 deshalb direkt in
Xcode.

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

154 lines
5.5 KiB
Swift

import Foundation
enum SetupStep: Int, CaseIterable {
case wan
case lan
case vlan
case wifi
case firewall
case review
}
@MainActor
final class SetupViewModel: ObservableObject {
@Published var step: SetupStep = .wan
@Published var wanConfig = WanConfig(interfaceName: "ether1")
@Published var lanConfig = LanDhcpConfig()
@Published private(set) var vlanSectionEnabled = false
@Published var vlans: [VlanEntry] = []
@Published var wifiNetworks: [WifiNetworkConfig] = []
@Published private(set) var unsupportedWifiInterfaces: [NetworkInterface] = []
@Published private(set) var firewallSectionEnabled = false
@Published private(set) var existingFirewallRuleCounts: FirewallRuleCounts?
@Published private(set) var isLoadingFirewallRuleCounts = false
@Published private(set) var firewallRuleCountsError: String?
@Published private(set) var isApplying = false
@Published private(set) var applyLog: [String] = []
@Published private(set) var applyError: String?
@Published private(set) var didApplySuccessfully = false
private let connectionService: ConnectionService
private let backupService: BackupService
init(connectionService: ConnectionService, backupService: BackupService = BackupService()) {
self.connectionService = connectionService
self.backupService = backupService
prepareDefaults(from: connectionService.interfaces)
}
var plannedCommands: [RouterOSCommand] {
wanConfig.buildCommands()
+ lanConfig.buildCommands()
+ vlans.flatMap { $0.buildCommands() }
+ wifiNetworks.flatMap { $0.buildCommands() }
+ (firewallSectionEnabled ? FirewallConfig(wanInterface: wanConfig.interfaceName).buildCommands() : [])
}
func setFirewallSectionEnabled(_ enabled: Bool) {
firewallSectionEnabled = enabled
if enabled {
loadExistingFirewallRuleCounts()
} else {
existingFirewallRuleCounts = nil
firewallRuleCountsError = nil
}
}
func loadExistingFirewallRuleCounts() {
guard firewallSectionEnabled, existingFirewallRuleCounts == nil, !isLoadingFirewallRuleCounts else { return }
isLoadingFirewallRuleCounts = true
firewallRuleCountsError = nil
Task {
do {
existingFirewallRuleCounts = try await connectionService.fetchFirewallRuleCounts()
} catch {
firewallRuleCountsError = error.localizedDescription
}
isLoadingFirewallRuleCounts = false
}
}
func setVlanSectionEnabled(_ enabled: Bool) {
vlanSectionEnabled = enabled
if !enabled { vlans = [] }
}
func addVlan() {
let usedIDs = Set(vlans.map(\.vlanID))
var nextID = 20
while usedIDs.contains(nextID) { nextID += 1 }
vlans.append(VlanEntry(vlanID: nextID, parentInterface: lanConfig.interfaceName))
}
func removeVlan(_ id: VlanEntry.ID) {
vlans.removeAll { $0.id == id }
}
/// Replaces the placeholder WAN/LAN interface names ("ether1"/"bridge") with real ones
/// from the connected device, whenever the current value isn't actually one of its
/// interfaces — otherwise the Picker selections don't match any of their tags.
func prepareDefaults(from interfaces: [NetworkInterface]) {
guard !interfaces.isEmpty else { return }
let names = Set(interfaces.map(\.name))
if !names.contains(wanConfig.interfaceName) {
if let firstEthernet = interfaces.first(where: { $0.type.lowercased().contains("ether") }) {
wanConfig.interfaceName = firstEthernet.name
} else {
wanConfig.interfaceName = interfaces[0].name
}
}
if !names.contains(lanConfig.interfaceName) {
if let bridge = interfaces.first(where: { $0.type.lowercased().contains("bridge") }) {
lanConfig.interfaceName = bridge.name
} else if let fallback = interfaces.first(where: { $0.name != wanConfig.interfaceName }) {
lanConfig.interfaceName = fallback.name
} else {
lanConfig.interfaceName = interfaces[0].name
}
}
if wifiNetworks.isEmpty {
let legacyWireless = interfaces.filter { $0.type.lowercased() == "wlan" }
wifiNetworks = legacyWireless.map { WifiNetworkConfig(interfaceName: $0.name) }
}
unsupportedWifiInterfaces = interfaces.filter { $0.type.lowercased() == "wifi" }
}
func goNext() {
guard let next = SetupStep(rawValue: step.rawValue + 1) else { return }
step = next
}
func goBack() {
guard let previous = SetupStep(rawValue: step.rawValue - 1) else { return }
step = previous
}
func apply(credentials: RouterOSCredentials) {
isApplying = true
applyError = nil
applyLog = []
didApplySuccessfully = false
Task {
do {
applyLog.append("Sichere aktuelle Konfiguration…")
_ = try await backupService.createBackup(for: credentials)
for command in plannedCommands {
applyLog.append(command.summary)
try await connectionService.apply(command)
}
didApplySuccessfully = true
} catch {
applyError = error.localizedDescription
}
isApplying = false
}
}
}