Files
RouterOS/RouterOSAssistant/Core/Models/DhcpServerCommandBuilder.swift
T
KayandClaude Sonnet 5 fc3b2ca039 M8: Netzwerk-Isolation live verifiziert (manuell + über den Wizard)
Isolation zuerst manuell per SSH nachgebaut (ether5), danach über den
App-Wizard (Experte-Modus, ether4), um die eigentliche Abnahme-
Bedingung ("kompletter Wizard-Durchlauf") zu erfüllen. Dabei drei
App-Bugs gefunden und gefixt:

- Bug 22: neues LAN-/VLAN-Interface wurde nie der defconf-Interface-
  Liste "LAN" hinzugefügt, wodurch DNS-Anfragen an den Router selbst
  blockiert blieben (Werks-Firewall droppt Input von allem außerhalb
  dieser Liste).
- Bug 23: ein voller Wizard-Durchlauf gegen einen bereits konfigurierten
  Router brach am ersten nicht-idempotenten Add-Befehl ab
  (/ip address, /ip pool, /ip dhcp-server, /ip dhcp-server network).
- Bug 24: ein als eigenes isoliertes Netz konfiguriertes Interface
  blieb Bridge-"Slave" (Werks-Bridging), wodurch RouterOS die
  generierten Isolationsregeln selbst als ungültig verwarf.

Alle drei in DhcpServerCommandBuilder/SetupViewModel gefixt, 52 Unit-
Tests grün, Isolation+DNS+Internet am echten Gerät bestätigt. M8 auf
live verifiziert gesetzt. Nebenbei zwei veraltete Doku-Stellen zum
Gitea-Remote korrigiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
2026-09-15 10:28:09 +02:00

98 lines
4.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Shared command set for "IP address + DHCP pool + DHCP server + DHCP network on one
/// interface" — used by the LAN step directly and, per entry, by the VLAN step.
enum DhcpServerCommandBuilder {
static func buildCommands(
interfaceName: String,
routerAddress: String,
networkAddress: String,
poolRangeStart: String,
poolRangeEnd: String,
leaseTimeHours: Int,
dnsServers: String,
context: String
) -> [RouterOSCommand] {
let poolName = "dhcp_pool_\(interfaceName)"
let serverName = "dhcp_\(interfaceName)"
let routerIP = routerAddress.components(separatedBy: "/").first ?? routerAddress
// Detach from any bridge this physical port is still a member of before treating it as
// its own network. Skipped for "bridge" itself (the app's own shared-LAN interface, never
// a bridge port). Confirmed live (2026-09-15): factory-default routers have ether2-5
// pre-bridged, and configuring one of them as a separate isolated network without this
// produces a broken result RouterOS itself refuses to run — the interface stays a bridge
// "slave", so its forward-chain isolation rules come back flagged invalid ("in/out-
// interface matcher not possible when interface is slave - use master instead"). No-op if
// the interface was never bridged: SSH's `remove [find ...]` is a silent no-op on no
// match, and REST's not-found is tolerated by `SetupViewModel.applyIdempotently`.
let bridgeDetachCommands: [RouterOSCommand] = interfaceName == "bridge" ? [] : [
RouterOSCommand.remove(
menuPath: "/interface bridge port",
restPath: "interface/bridge/port",
matchField: "interface",
matchValue: interfaceName,
summary: "\(interfaceName) aus evtl. bestehender Bridge lösen"
)
]
return bridgeDetachCommands + [
RouterOSCommand.add(
menuPath: "/ip address",
restPath: "ip/address",
arguments: ["address": routerAddress, "interface": interfaceName],
summary: "IP-Adresse \(routerAddress) \(context) setzen"
),
RouterOSCommand.add(
menuPath: "/ip pool",
restPath: "ip/pool",
arguments: ["name": poolName, "ranges": "\(poolRangeStart)-\(poolRangeEnd)"],
summary: "Adressbereich \(poolRangeStart)\(poolRangeEnd) \(context) anlegen"
),
RouterOSCommand.add(
menuPath: "/ip dhcp-server",
restPath: "ip/dhcp-server",
arguments: [
"name": serverName,
"interface": interfaceName,
"address-pool": poolName,
"lease-time": "\(leaseTimeHours)h",
"disabled": "no"
],
summary: "DHCP-Server \(context) aktivieren"
),
RouterOSCommand.add(
menuPath: "/ip dhcp-server network",
restPath: "ip/dhcp-server/network",
arguments: [
"address": networkAddress,
"gateway": routerIP,
"dns-server": dnsServers
],
summary: "DHCP-Netzwerk \(networkAddress) \(context) konfigurieren"
),
// RouterOS' factory-default firewall (present on most out-of-box routers) has an
// input-chain rule dropping everything not from the "LAN" interface list. Without
// this interface as a member, devices on it get DHCP/routing/internet fine (that's
// forward-chain, untouched) but can never reach the router itself for DNS, Winbox,
// etc. — confirmed live (2026-09-15): a manually-isolated port had a bound DHCP
// lease and a working default route, yet every DNS query to the router timed out
// until it was added to "LAN". Both commands are tolerated as already-satisfied by
// `SetupViewModel.applyIdempotently` if the list/membership already exists (e.g. the
// default "bridge" interface, already a defconf LAN member).
RouterOSCommand.add(
menuPath: "/interface list",
restPath: "interface/list",
arguments: ["name": "LAN"],
summary: "Interface-Liste \"LAN\" sicherstellen"
),
RouterOSCommand.add(
menuPath: "/interface list member",
restPath: "interface/list/member",
arguments: ["list": "LAN", "interface": interfaceName],
summary: "\(interfaceName) der Interface-Liste \"LAN\" hinzufügen \(context)"
)
]
}
}