From c1f9940eaef04f4ab501c1fe7f4f297393c23b19 Mon Sep 17 00:00:00 2001 From: Kay Date: Sat, 12 Sep 2026 20:17:26 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20CLI-Parser=20verlor=206=20von=207=20Inte?= =?UTF-8?q?rfaces=20auf=20echtem=20hEX-Ger=C3=A4t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kritischer Fund beim M6-Live-Test: RouterOSCliParser.parseInterfaces splittete nur auf "\n", aber die SSH-Ausgabe eines echten hEX-Routers trennt Zeilen anders -- alle Interface-Zeilen wurden zu EINER Zeile zusammengefasst. Beim key=value-Parsen dieser einen Riesenzeile überschrieb jedes Feld (name=, type=, ...) den vorherigen Wert, sodass am Ende nur das letzte Interface im Text ("lo", Loopback) übrig blieb -- mit den Feldwerten aller anderen Interfaces vermischt. Folge: WAN-Schritt zeigte nur "lo" zur Auswahl, wodurch alle WAN-Interface-Referenzen (NAT-Masquerade, ICMP-Regel, WAN-Block-Regel, finale Anti-Spoofing-Regel) fälschlich auf "lo" statt den echten WAN-Port zeigten. Auf diesem Testgerät blieb es folgenlos, weil RouterOS schon eine vollständige eigene Standard-Firewall (defconf) mitbrachte, die den echten Schutz weiterhin übernahm -- auf einem Gerät ohne bestehende Firewall hätte das eine wirkungslose Firewall bedeutet, die sich als aktiv ausgegeben hätte. Fix: split(whereSeparator: \.isNewline) statt split(separator: "\n"), robust gegen \n/\r/\r\n. Zusätzliches Sicherheitsnetz in SetupView: Loopback-Interfaces werden aus allen WAN/LAN/VLAN-Auswahllisten gefiltert, damit ein ähnlicher Parser-Fehler künftig nicht erneut zu einer sinnlosen Interface-Auswahl führen kann. Regressionstest mit realen \r\n-getrennten hEX-Daten ergänzt. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW --- .../Core/Networking/RouterOSCliParser.swift | 9 +++++++-- .../Wizard/Steps/Setup/SetupView.swift | 16 ++++++++++++---- .../RouterOSCliParserTests.swift | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/RouterOSAssistant/Core/Networking/RouterOSCliParser.swift b/RouterOSAssistant/Core/Networking/RouterOSCliParser.swift index eec87e7..fd71d84 100644 --- a/RouterOSAssistant/Core/Networking/RouterOSCliParser.swift +++ b/RouterOSAssistant/Core/Networking/RouterOSCliParser.swift @@ -6,7 +6,7 @@ enum RouterOSCliParser { /// Parses `/system resource print` output (colon-separated "key: value" lines). static func parseDeviceInfo(_ raw: String) -> RouterDeviceInfo { var fields: [String: String] = [:] - for line in raw.split(separator: "\n") { + for line in raw.split(whereSeparator: \.isNewline) { guard let colonIndex = line.firstIndex(of: ":") else { continue } let key = String(line[line.startIndex.. [NetworkInterface] { - raw.split(separator: "\n").compactMap { line in + raw.split(whereSeparator: \.isNewline).compactMap { line in let fields = keyValues(from: String(line)) guard let name = fields["name"] else { return nil } return NetworkInterface( diff --git a/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupView.swift b/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupView.swift index e388b98..492134c 100644 --- a/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupView.swift +++ b/RouterOSAssistant/Features/Wizard/Steps/Setup/SetupView.swift @@ -21,11 +21,11 @@ struct SetupView: View { } else { switch viewModel.step { case .wan: - WanStepView(viewModel: viewModel, availableInterfaces: connectionService.interfaces) + WanStepView(viewModel: viewModel, availableInterfaces: configurableInterfaces) case .lan: - LanStepView(viewModel: viewModel, availableInterfaces: connectionService.interfaces) + LanStepView(viewModel: viewModel, availableInterfaces: configurableInterfaces) case .vlan: - VlanStepView(viewModel: viewModel, availableInterfaces: connectionService.interfaces) + VlanStepView(viewModel: viewModel, availableInterfaces: configurableInterfaces) case .wifi: WifiStepView(viewModel: viewModel) case .firewall: @@ -37,9 +37,17 @@ struct SetupView: View { } } .onAppear { - viewModel.prepareDefaults(from: connectionService.interfaces) + viewModel.prepareDefaults(from: configurableInterfaces) } } + + /// Loopback is never something a person should pick as WAN/LAN/VLAN base interface — + /// excluded here (not just via defaults) so a parsing hiccup elsewhere can't offer it as + /// a selectable option, which on a real hEX device caused every WAN/firewall command to + /// silently reference "lo" instead of the real WAN port. Found via live testing. + private var configurableInterfaces: [NetworkInterface] { + connectionService.interfaces.filter { $0.type.lowercased() != "loopback" } + } } #Preview { diff --git a/RouterOSAssistantTests/RouterOSCliParserTests.swift b/RouterOSAssistantTests/RouterOSCliParserTests.swift index 1f12d31..1060607 100644 --- a/RouterOSAssistantTests/RouterOSCliParserTests.swift +++ b/RouterOSAssistantTests/RouterOSCliParserTests.swift @@ -34,4 +34,22 @@ final class RouterOSCliParserTests: XCTestCase { XCTAssertFalse(interfaces[1].running) XCTAssertTrue(interfaces[1].disabled) } + + /// Regression test for a real bug found on a physical hEX device: its SSH output didn't + /// split on bare "\n", collapsing every interface into one "line" whose repeated keys + /// (name=, type=, ...) overwrote each other, leaving only the last interface ("lo"). + func testParseInterfacesHandlesCarriageReturnLineEndings() { + let raw = [ + "0 R name=ether1 default-name=ether1 type=ether mtu=1500", + "1 RS name=ether2 default-name=ether2 type=ether mtu=1500", + "5 R comment=defconf name=bridge type=bridge mtu=auto", + "6 R name=lo type=loopback mtu=65536" + ].joined(separator: "\r\n") + + let interfaces = RouterOSCliParser.parseInterfaces(raw) + + XCTAssertEqual(interfaces.count, 4) + XCTAssertEqual(interfaces.map(\.name), ["ether1", "ether2", "bridge", "lo"]) + XCTAssertEqual(interfaces.map(\.type), ["ether", "ether", "bridge", "loopback"]) + } }