forked from kay/RouterOS
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
98 lines
4.8 KiB
Swift
98 lines
4.8 KiB
Swift
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)"
|
||
)
|
||
]
|
||
}
|
||
}
|