Bisher wirkten die neuen Firewall-Isolationsregeln nur auf neue Verbindungen - eine bereits offene Verbindung zwischen zwei gerade isolierten Netzen lief unbeeinflusst weiter (Standard-Verhalten jeder stateful Firewall). Neue SSHTransport.flushConnections/ ConnectionService.flushConnections entfernen per /ip firewall connection remove [find where (src-address in A) and (dst-address in B)] bereits getrackte Verbindungen zwischen isolierten Netzpaaren, aufgerufen direkt nach den Firewall-Befehlen in SetupViewModel.apply(). FirewallConfig.NetworkSegment um networkAddress (CIDR) erweitert, Paar-Logik in eine wiederverwendbare isolatedNetworkPairs-Property extrahiert. networkA/networkB werden vor der SSH-Interpolation als reine CIDR-Notation validiert (dieselbe Vorsicht wie bei der zuvor gefixten CLI-Injection). Ehrlicher Verifikationsstand dokumentiert statt Überclaiming: die kombinierte remove-Bedingung ließ sich mangels zweier echter Testnetze nicht end-to-end beweisen - ein erster scheinbarer Erfolg stellte sich als Messfehler heraus (natürlicher ICMP-Conntrack-Timeout, nicht der remove-Befehl selbst). Details in bugs.md #7. Build + alle 103 Unit-Tests grün (1 neuer Regressionstest). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
106 lines
4.6 KiB
Swift
106 lines
4.6 KiB
Swift
import XCTest
|
|
@testable import RouterOSAssistant
|
|
|
|
final class FirewallConfigTests: XCTestCase {
|
|
func testBuildCommandsCoversNatAndSafeFilterDefaults() {
|
|
let config = FirewallConfig(wanInterface: "ether1")
|
|
let commands = config.buildCommands()
|
|
|
|
XCTAssertEqual(commands.count, 8)
|
|
|
|
XCTAssertEqual(commands[0].menuPath, "/ip firewall nat")
|
|
XCTAssertEqual(commands[0].arguments["action"], "masquerade")
|
|
XCTAssertEqual(commands[0].arguments["out-interface"], "ether1")
|
|
XCTAssertEqual(commands[0].arguments["place-before"], "0")
|
|
|
|
for command in commands.dropFirst() {
|
|
XCTAssertEqual(command.menuPath, "/ip firewall filter")
|
|
}
|
|
}
|
|
|
|
func testFilterRulesGetIncrementingPlaceBeforeInDeclaredOrder() {
|
|
let config = FirewallConfig(wanInterface: "ether1")
|
|
let filterCommands = config.buildCommands().dropFirst()
|
|
|
|
let placeBeforeValues = filterCommands.map { $0.arguments["place-before"] }
|
|
XCTAssertEqual(placeBeforeValues, ["0", "1", "2", "3", "4", "5", "6"])
|
|
}
|
|
|
|
func testFinalRuleBlocksUnsolicitedWanTrafficUnlessPortForwarded() {
|
|
let config = FirewallConfig(wanInterface: "ether1")
|
|
let commands = config.buildCommands()
|
|
|
|
let dropRule = commands.last!
|
|
XCTAssertEqual(dropRule.arguments["chain"], "forward")
|
|
XCTAssertEqual(dropRule.arguments["connection-nat-state"], "!dstnat")
|
|
XCTAssertEqual(dropRule.arguments["in-interface"], "ether1")
|
|
XCTAssertEqual(dropRule.arguments["action"], "drop")
|
|
}
|
|
|
|
func testIsolatedNetworkGetsBidirectionalDropRulesAgainstOtherNetworks() {
|
|
let config = FirewallConfig(
|
|
wanInterface: "ether1",
|
|
networks: [
|
|
FirewallConfig.NetworkSegment(interfaceName: "bridge", isolated: true),
|
|
FirewallConfig.NetworkSegment(interfaceName: "vlan20", isolated: false)
|
|
]
|
|
)
|
|
let commands = config.buildCommands()
|
|
let isolationCommands = commands.dropFirst(8)
|
|
|
|
XCTAssertEqual(isolationCommands.count, 2)
|
|
XCTAssertTrue(isolationCommands.allSatisfy { $0.arguments["chain"] == "forward" && $0.arguments["action"] == "drop" })
|
|
|
|
let directions = Set(isolationCommands.map { "\($0.arguments["in-interface"]!)->\($0.arguments["out-interface"]!)" })
|
|
XCTAssertEqual(directions, ["bridge->vlan20", "vlan20->bridge"])
|
|
|
|
XCTAssertEqual(isolationCommands.map { $0.arguments["place-before"] }, ["7", "8"])
|
|
}
|
|
|
|
func testMutuallyIsolatedNetworksProduceOnlyOnePairOfRules() {
|
|
let config = FirewallConfig(
|
|
wanInterface: "ether1",
|
|
networks: [
|
|
FirewallConfig.NetworkSegment(interfaceName: "bridge", isolated: true),
|
|
FirewallConfig.NetworkSegment(interfaceName: "vlan20", isolated: true)
|
|
]
|
|
)
|
|
let isolationCommands = config.buildCommands().dropFirst(8)
|
|
|
|
XCTAssertEqual(isolationCommands.count, 2)
|
|
}
|
|
|
|
func testNonIsolatedNetworksProduceNoIsolationRules() {
|
|
let config = FirewallConfig(
|
|
wanInterface: "ether1",
|
|
networks: [
|
|
FirewallConfig.NetworkSegment(interfaceName: "bridge", isolated: false),
|
|
FirewallConfig.NetworkSegment(interfaceName: "vlan20", isolated: false)
|
|
]
|
|
)
|
|
|
|
XCTAssertEqual(config.buildCommands().count, 8)
|
|
}
|
|
|
|
/// Regression test for bugs.md #7 / Gitea #19: `isolatedNetworkPairs` is the shared source of
|
|
/// truth `SetupViewModel.flushIsolatedNetworkConnections()` uses to know which network pairs
|
|
/// to flush already-open connections between — must report the same pairs
|
|
/// `buildIsolationCommands` derives its drop rules from, including the network's address
|
|
/// range (not used by the drop rules themselves, only by the connection flush).
|
|
func testIsolatedNetworkPairsCarriesAddressesForConnectionFlush() {
|
|
let config = FirewallConfig(
|
|
wanInterface: "ether1",
|
|
networks: [
|
|
FirewallConfig.NetworkSegment(interfaceName: "bridge", isolated: true, networkAddress: "192.168.88.0/24"),
|
|
FirewallConfig.NetworkSegment(interfaceName: "vlan20", isolated: false, networkAddress: "192.168.20.0/24")
|
|
]
|
|
)
|
|
|
|
let pairs = config.isolatedNetworkPairs
|
|
|
|
XCTAssertEqual(pairs.count, 1)
|
|
XCTAssertEqual(Set([pairs[0].0.interfaceName, pairs[0].1.interfaceName]), ["bridge", "vlan20"])
|
|
XCTAssertEqual(Set([pairs[0].0.networkAddress, pairs[0].1.networkAddress]), ["192.168.88.0/24", "192.168.20.0/24"])
|
|
}
|
|
}
|