Firewall-NAT/Filter-Regeln beim Wizard-Apply idempotent machen
Wiederholter Wizard-Lauf gegen einen bereits konfigurierten Router hat bisher jedes Mal dieselben NAT-/Filter-Regeln erneut angelegt (RouterOS lehnt Duplikate hier nicht ab). SetupViewModel.apply holt jetzt einmalig eine Live-Momentaufnahme der bestehenden Regeln und überspringt geplante .add-Befehle mit identischem Argument-Set (ohne das rein schreibseitige place-before). Live bestätigt: zweimaliger Wizard-Lauf, Regelanzahl blieb beim zweiten Mal unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+22
-8
@@ -1329,14 +1329,28 @@ Claude-Memory `future-language-support.md` für Details.
|
|||||||
Peer "Keepalive", Scheduler "Intervall", Netwatch "Prüf-Intervall".
|
Peer "Keepalive", Scheduler "Intervall", Netwatch "Prüf-Intervall".
|
||||||
Optional auch für den Geräte-Tab, falls dort künftig Zeitfelder
|
Optional auch für den Geräte-Tab, falls dort künftig Zeitfelder
|
||||||
dazukommen.
|
dazukommen.
|
||||||
6. **Firewall-NAT-/Filter-`add`-Befehle sind bei wiederholtem Wizard-Lauf
|
6. ~~Firewall-NAT-/Filter-`add`-Befehle sind bei wiederholtem Wizard-Lauf
|
||||||
weiterhin nicht idempotent** — im Gegensatz zu den bei Bug 22–24
|
weiterhin nicht idempotent~~ — behoben (2026-09-16): im Gegensatz zu den bei Bug 22–24
|
||||||
gefixten Menüs lässt RouterOS identische NAT-/Filter-Regeln mehrfach
|
gefixten Menüs (dort per `.set`-Retry auf einem eindeutigen Feld wie
|
||||||
zu (kein Fehler, kein Abbruch), aber jeder erneute Wizard-Durchlauf
|
`name`/`interface` gelöst) haben Firewall-Filter/NAT-Regeln kein
|
||||||
häuft doppelte Regeln an. Nicht blockierend (heute so live
|
eindeutiges Identitätsfeld — "dieselbe Regel" heißt hier "identisches
|
||||||
beobachtet, Apply lief trotzdem durch), aber auf Dauer Regel-Bloat —
|
Argument-Set". `SetupViewModel.apply(credentials:)` holt deshalb
|
||||||
bei Gelegenheit auf "vorhandene identische Regel überspringen"
|
einmalig (nicht pro Befehl) eine Live-Momentaufnahme der bestehenden
|
||||||
umstellen.
|
`/ip firewall filter`- und `/ip firewall nat`-Regeln
|
||||||
|
(`connectionService.fetchMenuItems`), und `isDuplicateFirewallRule`
|
||||||
|
vergleicht jeden geplanten `.add`-Befehl (ohne das rein schreibseitige
|
||||||
|
`"place-before"`-Argument, das nie ein gespeichertes RouterOS-Feld
|
||||||
|
ist) gegen diese Momentaufnahme — bei Treffer wird der Befehl
|
||||||
|
übersprungen und im Ablauf-Log als "bereits vorhanden, übersprungen"
|
||||||
|
vermerkt statt erneut ausgeführt. Einmalige Momentaufnahme statt
|
||||||
|
Nachfrage pro Befehl, damit in derselben Wizard-Ausführung neu
|
||||||
|
hinzugefügte Regeln (z.B. eine zweite Isolationsregel) nicht
|
||||||
|
versehentlich gegen sich selbst als Duplikat erkannt werden, aber
|
||||||
|
Regeln aus einem früheren Wizard-Lauf trotzdem erkannt werden.
|
||||||
|
Live bestätigt: kompletter Wizard mit aktivierter Firewall-Sektion
|
||||||
|
zweimal hintereinander gegen denselben Router angewendet, Filter-/
|
||||||
|
NAT-Regelanzahl blieb beim zweiten Durchlauf unverändert
|
||||||
|
("passt, anzahlen ändern sich nicht").
|
||||||
7. ~~`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
|
7. ~~`.id`-Positions-Überlagerung (`fetchMenuItems`) auf weitere Menüs
|
||||||
prüfen~~ — am 2026-09-16 live an `/ip address`/`/ip route`/erneut
|
prüfen~~ — am 2026-09-16 live an `/ip address`/`/ip route`/erneut
|
||||||
`/ip dhcp-server lease` geprüft, keine erneute Fehlzuordnung
|
`/ip dhcp-server lease` geprüft, keine erneute Fehlzuordnung
|
||||||
|
|||||||
@@ -275,6 +275,27 @@ final class SetupViewModel: ObservableObject {
|
|||||||
prepareDefaults(from: connectionService.interfaces)
|
prepareDefaults(from: connectionService.interfaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Firewall filter/NAT menus have no unique identifying field (name, interface, …) to retry
|
||||||
|
/// an `.add` as a `.set` against — unlike the menus in `retryAsSetMenuPaths` below, a rule is
|
||||||
|
/// only "the same rule" if its whole argument set matches. RouterOS itself happily accepts
|
||||||
|
/// identical filter/NAT rules added repeatedly (no error, no dedup) — live-observed: a
|
||||||
|
/// second full wizard run against an already-configured router just keeps appending the same
|
||||||
|
/// rules again. Checked once per `apply()` against a live snapshot (see `apply(credentials:)`),
|
||||||
|
/// not per-command, since fetching after every single add would make an already-configured
|
||||||
|
/// router's rules invisible to the check for rules added earlier in the same run.
|
||||||
|
private static let firewallRuleMenuPaths: Set<String> = ["/ip firewall filter", "/ip firewall nat"]
|
||||||
|
|
||||||
|
/// `RouterOSCommand.add(...)`'s `"place-before"` argument is a write-time positional
|
||||||
|
/// instruction (where to insert), not a stored RouterOS property — it never appears in
|
||||||
|
/// `fetchMenuItems`' parsed fields, so it must be excluded here, or the comparison would
|
||||||
|
/// spuriously fail on it for every single command.
|
||||||
|
private func isDuplicateFirewallRule(_ command: RouterOSCommand, in existingItems: [RouterOSMenuItem]) -> Bool {
|
||||||
|
let comparableArguments = command.arguments.filter { $0.key != "place-before" }
|
||||||
|
return existingItems.contains { item in
|
||||||
|
comparableArguments.allSatisfy { key, value in item.fields[key] == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func apply(credentials: RouterOSCredentials) {
|
func apply(credentials: RouterOSCredentials) {
|
||||||
isApplying = true
|
isApplying = true
|
||||||
applyError = nil
|
applyError = nil
|
||||||
@@ -286,7 +307,23 @@ final class SetupViewModel: ObservableObject {
|
|||||||
applyLog.append("Sichere aktuelle Konfiguration…")
|
applyLog.append("Sichere aktuelle Konfiguration…")
|
||||||
_ = try await backupService.createBackup(for: credentials)
|
_ = try await backupService.createBackup(for: credentials)
|
||||||
|
|
||||||
|
var existingFirewallItems: [String: [RouterOSMenuItem]] = [:]
|
||||||
|
if plannedCommands.contains(where: { Self.firewallRuleMenuPaths.contains($0.menuPath) }) {
|
||||||
|
for menuPath in Self.firewallRuleMenuPaths {
|
||||||
|
existingFirewallItems[menuPath] = try await connectionService.fetchMenuItems(
|
||||||
|
menuPath: menuPath,
|
||||||
|
restPath: menuPath == "/ip firewall filter" ? "ip/firewall/filter" : "ip/firewall/nat"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for command in plannedCommands {
|
for command in plannedCommands {
|
||||||
|
if case .add = command.operation,
|
||||||
|
let existing = existingFirewallItems[command.menuPath],
|
||||||
|
isDuplicateFirewallRule(command, in: existing) {
|
||||||
|
applyLog.append("\(command.summary) — bereits vorhanden, übersprungen")
|
||||||
|
continue
|
||||||
|
}
|
||||||
applyLog.append(command.summary)
|
applyLog.append(command.summary)
|
||||||
try await applyIdempotently(command)
|
try await applyIdempotently(command)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user