From ef3bb94cb929890d06ca06c81ee1f88ea88342d8 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Tue, 29 Sep 2026 21:00:42 +0000 Subject: [PATCH] feat(fls): import switch, wireless, routes, RIP and firewall settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit importProject carries the system software settings into the model the core runs: switch SSID and learning time (an SSID Java would roll comes from the node id, as in Swift), a host's wireless network, manual routes, RIP and the firewall of every router and home router. The Swift oracle writes these settings too; they match on the 13 files Swift reads completely. A router's default gateway — its first interface's, which Java routes to (Weiterleitungstabelle.holeTabelle) — is imported into the router's device configuration. Swift's model drops it; in internetwork_komplett the way back from 141.99.5.10 depends on it. A test pings across three routers in the core, with and without it. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 6 +- corpus/corpus.swift.json | 1104 +++++++++++++++--- packages/core/src/runtime/config.ts | 6 + packages/fls/src/importProject.ts | 133 ++- packages/fls/test/importProject.test.ts | 199 +++- tools/swift-oracle/Sources/Oracle/main.swift | 35 + 6 files changed, 1277 insertions(+), 206 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d31a662..5555536 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) | | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | -| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, Firewall) | übertragen; Java-Beschriftungen der Oberfläche weggelassen | +| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, Firewall) | übertragen; Java-Beschriftungen der Oberfläche weggelassen | | `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, TCP, DHCP, DNS, RIP, Remote Link, WLAN | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | @@ -33,8 +33,8 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen | | `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene | | `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen | -| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard; gegen Swifts importiertes Modell auf 13 Dateien geprüft | -| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Ping über den Router im Kern | +| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen; gegen Swifts importiertes Modell auf 13 Dateien geprüft | +| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht); gegen Java geprüft | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen | | `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter | diff --git a/corpus/corpus.swift.json b/corpus/corpus.swift.json index e95c36c..7680c8e 100644 --- a/corpus/corpus.swift.json +++ b/corpus/corpus.swift.json @@ -215,7 +215,13 @@ "position" : [ 250, 181 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -240,7 +246,10 @@ "position" : [ 213, 16 - ] + ], + "system" : { + + } }, { "device" : { @@ -265,7 +274,10 @@ "position" : [ 63, 83 - ] + ], + "system" : { + + } }, { "device" : { @@ -290,7 +302,10 @@ "position" : [ 289, 313 - ] + ], + "system" : { + + } }, { "device" : { @@ -315,7 +330,10 @@ "position" : [ 735, 25 - ] + ], + "system" : { + + } }, { "device" : null, @@ -473,7 +491,13 @@ "position" : [ 483, 185 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -498,7 +522,10 @@ "position" : [ 726, 145 - ] + ], + "system" : { + + } }, { "device" : { @@ -523,7 +550,10 @@ "position" : [ 709, 276 - ] + ], + "system" : { + + } } ] }, @@ -849,7 +879,13 @@ "position" : [ 79, 124 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFi" + } + } }, { "device" : null, @@ -890,7 +926,10 @@ "position" : [ 521, 364 - ] + ], + "system" : { + + } }, { "device" : { @@ -915,7 +954,10 @@ "position" : [ 656, 419 - ] + ], + "system" : { + + } }, { "device" : { @@ -940,7 +982,10 @@ "position" : [ 383, 422 - ] + ], + "system" : { + + } }, { "device" : { @@ -965,7 +1010,13 @@ "position" : [ 168, 29 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFi" + } + } }, { "device" : null, @@ -1123,7 +1174,13 @@ "position" : [ 255, 167 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "MyWiFi" + } + } } ] }, @@ -1339,7 +1396,13 @@ "position" : [ 164, 178 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : null, @@ -1497,7 +1560,13 @@ "position" : [ 612, 104 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -1522,7 +1591,10 @@ "position" : [ 694, 19 - ] + ], + "system" : { + + } }, { "device" : { @@ -1547,7 +1619,10 @@ "position" : [ 53, 100 - ] + ], + "system" : { + + } }, { "device" : { @@ -1572,7 +1647,10 @@ "position" : [ 70, 235 - ] + ], + "system" : { + + } }, { "device" : null, @@ -1604,7 +1682,10 @@ "position" : [ 373, 106 - ] + ], + "system" : { + + } } ] }, @@ -1672,7 +1753,10 @@ "position" : [ 355, 77 - ] + ], + "system" : { + + } }, { "device" : { @@ -1697,7 +1781,10 @@ "position" : [ 44, 31 - ] + ], + "system" : { + + } }, { "device" : null, @@ -1855,7 +1942,13 @@ "position" : [ 212, 115 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -1880,7 +1973,10 @@ "position" : [ 45, 180 - ] + ], + "system" : { + + } } ] }, @@ -1948,7 +2044,10 @@ "position" : [ 507, 96 - ] + ], + "system" : { + + } }, { "device" : { @@ -1973,7 +2072,10 @@ "position" : [ 112, 49 - ] + ], + "system" : { + + } }, { "device" : { @@ -1998,7 +2100,10 @@ "position" : [ 97, 164 - ] + ], + "system" : { + + } }, { "device" : null, @@ -2156,7 +2261,13 @@ "position" : [ 307, 128 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "71253f" + } + } }, { "device" : { @@ -2181,7 +2292,10 @@ "position" : [ 94, 320 - ] + ], + "system" : { + + } }, { "device" : { @@ -2206,7 +2320,10 @@ "position" : [ 580, 293 - ] + ], + "system" : { + + } } ] }, @@ -2433,7 +2550,13 @@ "position" : [ 250, 181 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -2458,7 +2581,10 @@ "position" : [ 213, 16 - ] + ], + "system" : { + + } }, { "device" : { @@ -2483,7 +2609,10 @@ "position" : [ 63, 83 - ] + ], + "system" : { + + } }, { "device" : { @@ -2508,7 +2637,10 @@ "position" : [ 289, 313 - ] + ], + "system" : { + + } }, { "device" : { @@ -2533,7 +2665,10 @@ "position" : [ 735, 25 - ] + ], + "system" : { + + } }, { "device" : null, @@ -2691,7 +2826,13 @@ "position" : [ 483, 185 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -2716,7 +2857,10 @@ "position" : [ 726, 145 - ] + ], + "system" : { + + } }, { "device" : { @@ -2741,7 +2885,10 @@ "position" : [ 709, 276 - ] + ], + "system" : { + + } } ] }, @@ -2857,7 +3004,10 @@ "position" : [ 32, 97 - ] + ], + "system" : { + + } }, { "device" : { @@ -2882,7 +3032,10 @@ "position" : [ 30, 226 - ] + ], + "system" : { + + } }, { "device" : { @@ -2907,7 +3060,10 @@ "position" : [ 31, 364 - ] + ], + "system" : { + + } }, { "device" : { @@ -2932,7 +3088,10 @@ "position" : [ 36, 503 - ] + ], + "system" : { + + } }, { "device" : { @@ -2957,7 +3116,10 @@ "position" : [ 564, 39 - ] + ], + "system" : { + + } }, { "device" : null, @@ -3115,7 +3277,13 @@ "position" : [ 449, 180 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -3140,7 +3308,10 @@ "position" : [ 348, 512 - ] + ], + "system" : { + + } }, { "device" : { @@ -3165,7 +3336,10 @@ "position" : [ 498, 513 - ] + ], + "system" : { + + } }, { "device" : { @@ -3190,7 +3364,10 @@ "position" : [ 634, 510 - ] + ], + "system" : { + + } }, { "device" : { @@ -3215,7 +3392,10 @@ "position" : [ 768, 509 - ] + ], + "system" : { + + } }, { "device" : null, @@ -3373,7 +3553,13 @@ "position" : [ 601, 368 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : null, @@ -3531,7 +3717,13 @@ "position" : [ 224, 300 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } } ] }, @@ -3629,7 +3821,10 @@ "position" : [ 460, 147 - ] + ], + "system" : { + + } }, { "device" : { @@ -3654,7 +3849,10 @@ "position" : [ 81, 266 - ] + ], + "system" : { + + } }, { "device" : { @@ -3679,7 +3877,10 @@ "position" : [ 83, 375 - ] + ], + "system" : { + + } }, { "device" : { @@ -3704,7 +3905,10 @@ "position" : [ 82, 59 - ] + ], + "system" : { + + } }, { "device" : { @@ -3729,7 +3933,10 @@ "position" : [ 82, 161 - ] + ], + "system" : { + + } }, { "device" : null, @@ -3887,7 +4094,13 @@ "position" : [ 284, 238 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -3912,7 +4125,10 @@ "position" : [ 469, 30 - ] + ], + "system" : { + + } }, { "device" : { @@ -3937,7 +4153,10 @@ "position" : [ 432, 285 - ] + ], + "system" : { + + } }, { "device" : { @@ -3962,7 +4181,10 @@ "position" : [ 430, 414 - ] + ], + "system" : { + + } } ] }, @@ -4368,7 +4590,10 @@ "position" : [ 5, 169 - ] + ], + "system" : { + + } }, { "device" : null, @@ -4526,7 +4751,13 @@ "position" : [ 233, 173 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "[ausgeschaltet]" + } + } }, { "device" : { @@ -4551,7 +4782,10 @@ "position" : [ 2, 274 - ] + ], + "system" : { + + } }, { "device" : null, @@ -4592,7 +4826,10 @@ "position" : [ 419, 434 - ] + ], + "system" : { + + } }, { "device" : null, @@ -4642,7 +4879,10 @@ "position" : [ 548, 490 - ] + ], + "system" : { + + } }, { "device" : null, @@ -4683,7 +4923,10 @@ "position" : [ 294, 518 - ] + ], + "system" : { + + } }, { "device" : null, @@ -4742,7 +4985,10 @@ "position" : [ 427, 583 - ] + ], + "system" : { + + } }, { "device" : null, @@ -4783,7 +5029,10 @@ "position" : [ 632, 643 - ] + ], + "system" : { + + } }, { "device" : null, @@ -4815,7 +5064,10 @@ "position" : [ 781, 574 - ] + ], + "system" : { + + } }, { "device" : { @@ -4840,7 +5092,10 @@ "position" : [ 208, 606 - ] + ], + "system" : { + + } }, { "device" : { @@ -4865,7 +5120,10 @@ "position" : [ 675, 793 - ] + ], + "system" : { + + } }, { "device" : { @@ -4890,7 +5148,10 @@ "position" : [ 693, 417 - ] + ], + "system" : { + + } }, { "device" : { @@ -4915,7 +5176,10 @@ "position" : [ 823, 684 - ] + ], + "system" : { + + } }, { "device" : { @@ -4940,7 +5204,10 @@ "position" : [ 139, 268 - ] + ], + "system" : { + + } }, { "device" : { @@ -4965,7 +5232,10 @@ "position" : [ 73, 840 - ] + ], + "system" : { + + } }, { "device" : { @@ -4990,7 +5260,13 @@ "position" : [ 372, 5 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFiNetwork" + } + } }, { "device" : { @@ -5015,7 +5291,13 @@ "position" : [ 462, 38 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFiNetwork" + } + } }, { "device" : null, @@ -5173,7 +5455,13 @@ "position" : [ 11, 76 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "MyWiFiNetwork" + } + } }, { "device" : null, @@ -5205,7 +5493,10 @@ "position" : [ 433, 699 - ] + ], + "system" : { + + } }, { "device" : { @@ -5230,7 +5521,10 @@ "position" : [ 446, 791 - ] + ], + "system" : { + + } }, { "device" : null, @@ -5388,7 +5682,13 @@ "position" : [ 333, 760 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "[ausgeschaltet]" + } + } } ] }, @@ -5753,7 +6053,10 @@ "position" : [ 5, 165 - ] + ], + "system" : { + + } }, { "device" : null, @@ -5911,7 +6214,13 @@ "position" : [ 233, 173 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "[ausgeschaltet]" + } + } }, { "device" : null, @@ -5952,7 +6261,10 @@ "position" : [ 419, 434 - ] + ], + "system" : { + + } }, { "device" : null, @@ -6002,7 +6314,10 @@ "position" : [ 548, 490 - ] + ], + "system" : { + + } }, { "device" : null, @@ -6043,7 +6358,10 @@ "position" : [ 294, 518 - ] + ], + "system" : { + + } }, { "device" : null, @@ -6102,7 +6420,10 @@ "position" : [ 427, 583 - ] + ], + "system" : { + + } }, { "device" : null, @@ -6143,7 +6464,10 @@ "position" : [ 606, 629 - ] + ], + "system" : { + + } }, { "device" : null, @@ -6175,7 +6499,10 @@ "position" : [ 746, 561 - ] + ], + "system" : { + + } }, { "device" : { @@ -6200,7 +6527,10 @@ "position" : [ 208, 606 - ] + ], + "system" : { + + } }, { "device" : { @@ -6225,7 +6555,10 @@ "position" : [ 666, 777 - ] + ], + "system" : { + + } }, { "device" : { @@ -6250,7 +6583,10 @@ "position" : [ 699, 417 - ] + ], + "system" : { + + } }, { "device" : { @@ -6275,7 +6611,10 @@ "position" : [ 799, 649 - ] + ], + "system" : { + + } }, { "device" : { @@ -6300,7 +6639,10 @@ "position" : [ 130, 268 - ] + ], + "system" : { + + } }, { "device" : { @@ -6325,7 +6667,10 @@ "position" : [ 124, 826 - ] + ], + "system" : { + + } }, { "device" : { @@ -6350,7 +6695,13 @@ "position" : [ 372, 5 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFiNetwork" + } + } }, { "device" : { @@ -6375,7 +6726,13 @@ "position" : [ 462, 38 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFiNetwork" + } + } }, { "device" : null, @@ -6533,7 +6890,13 @@ "position" : [ 11, 76 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "MyWiFiNetwork" + } + } }, { "device" : null, @@ -6565,7 +6928,10 @@ "position" : [ 461, 710 - ] + ], + "system" : { + + } }, { "device" : { @@ -6590,7 +6956,10 @@ "position" : [ 473, 782 - ] + ], + "system" : { + + } }, { "device" : null, @@ -6748,7 +7117,13 @@ "position" : [ 367, 775 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "[ausgeschaltet]" + } + } } ] }, @@ -7018,7 +7393,13 @@ "position" : [ 164, 178 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : null, @@ -7176,7 +7557,13 @@ "position" : [ 612, 104 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : null, @@ -7208,7 +7595,10 @@ "position" : [ 395, 47 - ] + ], + "system" : { + + } }, { "device" : null, @@ -7249,7 +7639,23 @@ "position" : [ 602, 263 - ] + ], + "system" : { + "manualRoutes" : [ + [ + "141.99.1.0", + "255.255.255.0", + "141.99.2.1", + "141.99.2.2" + ], + [ + "141.99.5.0", + "255.255.255.0", + "141.99.4.2", + "141.99.4.1" + ] + ] + } }, { "device" : null, @@ -7281,7 +7687,10 @@ "position" : [ 497, 493 - ] + ], + "system" : { + + } }, { "device" : null, @@ -7439,7 +7848,13 @@ "position" : [ 641, 392 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -7464,7 +7879,10 @@ "position" : [ 694, 19 - ] + ], + "system" : { + + } }, { "device" : { @@ -7489,7 +7907,10 @@ "position" : [ 70, 235 - ] + ], + "system" : { + + } }, { "device" : { @@ -7514,7 +7935,10 @@ "position" : [ 759, 196 - ] + ], + "system" : { + + } }, { "device" : { @@ -7539,7 +7963,10 @@ "position" : [ 169, 366 - ] + ], + "system" : { + + } }, { "device" : { @@ -7564,7 +7991,10 @@ "position" : [ 735, 387 - ] + ], + "system" : { + + } }, { "device" : null, @@ -7722,7 +8152,13 @@ "position" : [ 302, 408 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -7747,7 +8183,10 @@ "position" : [ 198, 489 - ] + ], + "system" : { + + } } ] }, @@ -7896,7 +8335,10 @@ "position" : [ 53, 264 - ] + ], + "system" : { + + } }, { "device" : { @@ -7921,7 +8363,10 @@ "position" : [ 547, 169 - ] + ], + "system" : { + + } }, { "device" : { @@ -7946,7 +8391,10 @@ "position" : [ 116, 120 - ] + ], + "system" : { + + } }, { "device" : null, @@ -8104,7 +8552,13 @@ "position" : [ 269, 237 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "6e23d8" + } + } }, { "device" : { @@ -8129,7 +8583,10 @@ "position" : [ 408, 49 - ] + ], + "system" : { + + } }, { "device" : { @@ -8154,7 +8611,10 @@ "position" : [ 519, 382 - ] + ], + "system" : { + + } }, { "device" : { @@ -8179,7 +8639,10 @@ "position" : [ 278, 405 - ] + ], + "system" : { + + } } ] }, @@ -8241,7 +8704,10 @@ "position" : [ 586, 46 - ] + ], + "system" : { + + } }, { "device" : { @@ -8266,7 +8732,10 @@ "position" : [ 42, 182 - ] + ], + "system" : { + + } }, { "device" : null, @@ -8286,7 +8755,10 @@ "position" : [ 237, 212 - ] + ], + "system" : { + + } }, { "device" : null, @@ -8306,7 +8778,10 @@ "position" : [ 396, 79 - ] + ], + "system" : { + + } } ] }, @@ -8375,7 +8850,10 @@ "position" : [ 731, 164 - ] + ], + "system" : { + + } }, { "device" : { @@ -8400,7 +8878,13 @@ "position" : [ 213, 175 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "Filius Wi Fi" + } + } }, { "device" : { @@ -8425,7 +8909,13 @@ "position" : [ 192, 317 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "Filius Wi Fi" + } + } }, { "device" : null, @@ -8583,7 +9073,13 @@ "position" : [ 475, 318 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 2000, + "ssid" : "Filius Wi Fi" + } + } } ] }, @@ -9015,7 +9511,13 @@ "position" : [ 300, 131 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "9b063a" + } + } }, { "device" : null, @@ -9173,7 +9675,13 @@ "position" : [ 676, 161 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "a4e955" + } + } }, { "device" : null, @@ -9205,7 +9713,10 @@ "position" : [ 551, 90 - ] + ], + "system" : { + + } }, { "device" : null, @@ -9246,7 +9757,23 @@ "position" : [ 559, 263 - ] + ], + "system" : { + "manualRoutes" : [ + [ + "141.99.1.0", + "255.255.255.0", + "141.99.2.1", + "141.99.2.2" + ], + [ + "141.99.5.0", + "255.255.255.0", + "141.99.4.2", + "141.99.4.1" + ] + ] + } }, { "device" : null, @@ -9278,7 +9805,10 @@ "position" : [ 562, 426 - ] + ], + "system" : { + + } }, { "device" : { @@ -9303,7 +9833,10 @@ "position" : [ 837, 119 - ] + ], + "system" : { + + } }, { "device" : { @@ -9328,7 +9861,10 @@ "position" : [ 152, 54 - ] + ], + "system" : { + + } }, { "device" : { @@ -9353,7 +9889,10 @@ "position" : [ 157, 202 - ] + ], + "system" : { + + } }, { "device" : { @@ -9378,7 +9917,10 @@ "position" : [ 804, 307 - ] + ], + "system" : { + + } }, { "device" : { @@ -9403,7 +9945,10 @@ "position" : [ 99, 363 - ] + ], + "system" : { + + } }, { "device" : null, @@ -9561,7 +10106,13 @@ "position" : [ 313, 444 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "8b0798" + } + } }, { "device" : { @@ -9586,7 +10137,10 @@ "position" : [ 260, 538 - ] + ], + "system" : { + + } }, { "device" : null, @@ -9744,7 +10298,13 @@ "position" : [ 694, 342 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "f1dc45" + } + } } ] }, @@ -9805,7 +10365,10 @@ "position" : [ 169, 135 - ] + ], + "system" : { + + } }, { "device" : { @@ -9830,7 +10393,10 @@ "position" : [ 850, 148 - ] + ], + "system" : { + + } }, { "device" : null, @@ -9988,7 +10554,13 @@ "position" : [ 633, 190 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "082368" + } + } } ] }, @@ -10337,7 +10909,13 @@ "position" : [ 373, 185 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "52fd3f" + } + } }, { "device" : { @@ -10362,7 +10940,10 @@ "position" : [ 62, 100 - ] + ], + "system" : { + + } }, { "device" : { @@ -10387,7 +10968,10 @@ "position" : [ 73, 235 - ] + ], + "system" : { + + } }, { "device" : null, @@ -10545,7 +11129,13 @@ "position" : [ 726, 196 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "5e7d40" + } + } }, { "device" : { @@ -10570,7 +11160,10 @@ "position" : [ 978, 234 - ] + ], + "system" : { + + } } ] }, @@ -11007,7 +11600,13 @@ "position" : [ 358, 185 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "bfc600" + } + } }, { "device" : { @@ -11032,7 +11631,10 @@ "position" : [ 148, 100 - ] + ], + "system" : { + + } }, { "device" : { @@ -11057,7 +11659,10 @@ "position" : [ 78, 227 - ] + ], + "system" : { + + } }, { "device" : null, @@ -11215,7 +11820,13 @@ "position" : [ 651, 83 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "[ausgeschaltet]" + } + } }, { "device" : { @@ -11240,7 +11851,10 @@ "position" : [ 633, 178 - ] + ], + "system" : { + + } }, { "device" : null, @@ -11281,7 +11895,10 @@ "position" : [ 740, 84 - ] + ], + "system" : { + + } }, { "device" : { @@ -11306,7 +11923,10 @@ "position" : [ 1026, 255 - ] + ], + "system" : { + + } }, { "device" : { @@ -11331,7 +11951,10 @@ "position" : [ 1098, 72 - ] + ], + "system" : { + + } }, { "device" : null, @@ -11372,7 +11995,10 @@ "position" : [ 934, 181 - ] + ], + "system" : { + + } } ] }, @@ -11700,7 +12326,13 @@ "position" : [ 79, 124 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFi" + } + } }, { "device" : null, @@ -11741,7 +12373,10 @@ "position" : [ 521, 364 - ] + ], + "system" : { + + } }, { "device" : { @@ -11766,7 +12401,10 @@ "position" : [ 656, 419 - ] + ], + "system" : { + + } }, { "device" : { @@ -11791,7 +12429,10 @@ "position" : [ 383, 422 - ] + ], + "system" : { + + } }, { "device" : { @@ -11816,7 +12457,13 @@ "position" : [ 168, 29 - ] + ], + "system" : { + "hostWireless" : { + "isEnabled" : true, + "ssid" : "MyWiFi" + } + } }, { "device" : null, @@ -11974,7 +12621,13 @@ "position" : [ 255, 167 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "MyWiFi" + } + } } ] }, @@ -12038,7 +12691,10 @@ "position" : [ 501, 93 - ] + ], + "system" : { + + } }, { "device" : { @@ -12063,7 +12719,10 @@ "position" : [ 102, 151 - ] + ], + "system" : { + + } }, { "device" : null, @@ -12221,7 +12880,13 @@ "position" : [ 329, 146 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } } ] }, @@ -12435,7 +13100,13 @@ "position" : [ 164, 178 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : null, @@ -12593,7 +13264,13 @@ "position" : [ 612, 104 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -12618,7 +13295,10 @@ "position" : [ 694, 19 - ] + ], + "system" : { + + } }, { "device" : { @@ -12643,7 +13323,10 @@ "position" : [ 53, 100 - ] + ], + "system" : { + + } }, { "device" : { @@ -12668,7 +13351,10 @@ "position" : [ 70, 235 - ] + ], + "system" : { + + } }, { "device" : null, @@ -12700,7 +13386,10 @@ "position" : [ 373, 106 - ] + ], + "system" : { + + } } ] }, @@ -12816,7 +13505,10 @@ "position" : [ 32, 97 - ] + ], + "system" : { + + } }, { "device" : { @@ -12841,7 +13533,10 @@ "position" : [ 30, 226 - ] + ], + "system" : { + + } }, { "device" : { @@ -12866,7 +13561,10 @@ "position" : [ 31, 364 - ] + ], + "system" : { + + } }, { "device" : { @@ -12891,7 +13589,10 @@ "position" : [ 36, 503 - ] + ], + "system" : { + + } }, { "device" : { @@ -12916,7 +13617,10 @@ "position" : [ 564, 39 - ] + ], + "system" : { + + } }, { "device" : null, @@ -13074,7 +13778,13 @@ "position" : [ 449, 180 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : { @@ -13099,7 +13809,10 @@ "position" : [ 348, 512 - ] + ], + "system" : { + + } }, { "device" : { @@ -13124,7 +13837,10 @@ "position" : [ 498, 513 - ] + ], + "system" : { + + } }, { "device" : { @@ -13149,7 +13865,10 @@ "position" : [ 634, 510 - ] + ], + "system" : { + + } }, { "device" : { @@ -13174,7 +13893,10 @@ "position" : [ 768, 509 - ] + ], + "system" : { + + } }, { "device" : null, @@ -13332,7 +14054,13 @@ "position" : [ 601, 368 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } }, { "device" : null, @@ -13490,7 +14218,13 @@ "position" : [ 224, 300 - ] + ], + "system" : { + "switch" : { + "retentionTimeMilliseconds" : 300000, + "ssid" : "" + } + } } ] }, diff --git a/packages/core/src/runtime/config.ts b/packages/core/src/runtime/config.ts index 6b6972e..8f491af 100644 --- a/packages/core/src/runtime/config.ts +++ b/packages/core/src/runtime/config.ts @@ -84,6 +84,12 @@ export function defaultSwitchConfiguration(nodeId: Id): SwitchConfiguration { return switchConfiguration(compact.slice(0, 6)); } +/** A PC or notebook joining a switch's wireless network (Java Betriebssystem.ssid). */ +export interface HostWirelessConfiguration { + readonly isEnabled: boolean; + readonly ssid: string; +} + // ── Firewall (Java filius.software.firewall.Firewall, FirewallRule) ───────── export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const; diff --git a/packages/fls/src/importProject.ts b/packages/fls/src/importProject.ts index f3300ce..f36b3c6 100644 --- a/packages/fls/src/importProject.ts +++ b/packages/fls/src/importProject.ts @@ -15,11 +15,16 @@ // and put a default in place of an empty one); // - an address Java's setter rejected (null) becomes Swift's default, as // Swift does for an empty one; Swift would keep invalid text verbatim. -// Not here yet: switch and wireless settings, routes, firewall, DHCP, remote -// links and everything else under a node's system software. +// - a router's default gateway (its first interface's, as in Java) — Swift +// drops it, and routed examples lose their way back; +// - every router and home router has a firewall configuration, as every one +// has a firewall in Java (Swift keeps one only when the file sets a field; +// without one the core behaves as with an inactive one). +// Not here yet: DHCP, NAT, remote links, file systems, applications. import { canonicalMacAddress, + defaultSwitchConfiguration, deterministicDocumentationUuid, documentationColor, documentationItem, @@ -30,6 +35,13 @@ import { DOCUMENTATION_BLACK, DOCUMENTATION_PALE_YELLOW, type DeviceConfiguration, + type FirewallAction, + type FirewallConfiguration, + type FirewallProtocol, + type FirewallRule, + type HostWirelessConfiguration, + type ManualRoute, + type SwitchConfiguration, type DocumentationItem, type HostLabelPolicy, type Id, @@ -41,6 +53,7 @@ import { type TopologyLink, type TopologyNode, } from '@mdg-filius/core'; +import type { FlsFirewall } from './systemSoftware.js'; import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js'; export interface ImportedProject { @@ -48,6 +61,12 @@ export interface ImportedProject { graph: TopologyGraph; deviceConfigurations: Map; interfaceConfigurations: Map; + switchConfigurations: Map; + hostWirelessConfigurations: Map; + manualRoutesByNodeId: Map; + /** Only routers with RIP switched on. */ + ripEnabledByNodeId: Map; + firewallConfigurationsByNodeId: Map; /** In render order: rectangles below text, then by order. */ documentationItems: DocumentationItem[]; /** The topology's warnings, then this step's. */ @@ -73,9 +92,17 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP const deviceConfigurations = new Map(); const interfaceConfigurations = new Map(); + const system: SystemConfigurations = { + switchConfigurations: new Map(), + hostWirelessConfigurations: new Map(), + manualRoutesByNodeId: new Map(), + ripEnabledByNodeId: new Map(), + firewallConfigurationsByNodeId: new Map(), + }; const nodes = topology.nodes.map((source) => { const node = importNode(source, newId, warnings); registerConfigurations(source, node, deviceConfigurations, interfaceConfigurations); + registerSystemConfigurations(source, node, system, warnings); return node; }); @@ -122,6 +149,7 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP graph: { nodes, links }, deviceConfigurations, interfaceConfigurations, + ...system, documentationItems: inDeterministicRenderOrder(documentationItems), warnings, }; @@ -218,6 +246,21 @@ function registerConfigurations( subnetMask: orDefault(nic?.mask ?? null, wan ? '255.0.0.0' : '255.255.255.0'), }); }); + // Java routes a router's traffic without a better route to the gateway of + // its first interface (Weiterleitungstabelle.holeTabelle, getStandardGateway). + // Swift's model has no router gateway at all; the core reads it from the + // router's device configuration, which is set only when there is one. + const first = source.interfaces[0]; + const routerGateway = first?.gateway?.trim() ?? ''; + if (node.kind === 'router' && first !== undefined && routerGateway !== '') { + const firstConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id)); + deviceConfigurations.set(node.id, { + ipAddress: firstConfiguration?.ipAddress ?? '', + subnetMask: firstConfiguration?.subnetMask ?? '', + defaultGateway: routerGateway, + dnsServer: first.dns?.trim() ?? '', + }); + } const wan = source.interfaces[0]; if (node.kind === 'gateway' && wan !== undefined) { const wanConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id)); @@ -230,6 +273,92 @@ function registerConfigurations( } } +type SystemConfigurations = Pick< + ImportedProject, + | 'switchConfigurations' + | 'hostWirelessConfigurations' + | 'manualRoutesByNodeId' + | 'ripEnabledByNodeId' + | 'firewallConfigurationsByNodeId' +>; + +/** + * Swift's registerSwitchAndWirelessConfigurations, registerManualRoutes and + * the RIP and firewall parts of finalizeCurrentNodeCandidate. + */ +function registerSystemConfigurations( + source: FlsNode, + node: TopologyNode, + system: SystemConfigurations, + warnings: string[], +): void { + const where = `node '${node.displayName}'`; + const ssid = source.system.ssid?.trim() ?? ''; + if (node.kind === 'networkSwitch') { + // Without an SSID in the file, Java rolls one; Swift derives it from the node id. + system.switchConfigurations.set(node.id, { + ssid: ssid === '' ? defaultSwitchConfiguration(node.id).ssid : ssid, + retentionTimeMilliseconds: source.system.retentionTime ?? 300_000, + }); + } + if ((node.kind === 'pc' || node.kind === 'notebook') && ssid !== '') { + system.hostWirelessConfigurations.set(node.id, { isEnabled: true, ssid }); + } + const routes: ManualRoute[] = []; + for (const row of source.system.manualRoutes ?? []) { + const [destinationNetwork, subnetMask, gateway, interfaceIPAddress] = row; + if ( + row.length !== 4 || + destinationNetwork == null || + subnetMask == null || + gateway == null || + interfaceIPAddress == null + ) { + warnings.push(`${where}: a manual route without four entries, left out`); + continue; + } + routes.push({ destinationNetwork, subnetMask, gateway, interfaceIPAddress }); + } + if (routes.length > 0) system.manualRoutesByNodeId.set(node.id, routes); + if (node.kind === 'router' && source.system.ripEnabled === true) system.ripEnabledByNodeId.set(node.id, true); + if ((node.kind === 'router' || node.kind === 'gateway') && source.system.firewall !== undefined) { + system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(source.system.firewall, where, warnings)); + } +} + +const PROTOCOLS: Readonly> = { [-1]: 'all', 1: 'icmp', 6: 'tcp', 17: 'udp' }; +const ACTIONS: Readonly> = { 0: 'drop', 1: 'accept' }; + +/** Java's numbers as the core's names; an unknown number keeps Swift's default (TCP, drop). */ +function firewallConfigurationOf(firewall: FlsFirewall, where: string, warnings: string[]): FirewallConfiguration { + const action = (value: number, what: string): FirewallAction => { + const known = ACTIONS[value]; + if (known === undefined) warnings.push(`${where}: firewall ${what} ${value} unknown, used drop`); + return known ?? 'drop'; + }; + const rules: FirewallRule[] = firewall.rules.map((rule) => { + const protocol = PROTOCOLS[rule.protocol]; + if (protocol === undefined) warnings.push(`${where}: firewall protocol ${rule.protocol} unknown, used TCP`); + return { + sourceIPAddress: rule.srcIP ?? '', + sourceSubnetMask: rule.srcMask ?? '', + destinationIPAddress: rule.destIP ?? '', + destinationSubnetMask: rule.destMask ?? '', + port: rule.port, + protocol: protocol ?? 'tcp', + action: action(rule.action, 'action'), + }; + }); + return { + isActive: firewall.activated, + defaultPolicy: action(firewall.defaultPolicy, 'default policy'), + dropIcmp: firewall.dropICMP, + filterSynSegmentsOnly: firewall.filterSYNSegmentsOnly, + filterUdp: firewall.filterUdp, + rules, + }; +} + /** * Swift's finalizeCurrentDocumentationCandidate: RECT = 1, TEXT = 2; a frame * inside the editor's world; the id from the item's values and its order. diff --git a/packages/fls/test/importProject.test.ts b/packages/fls/test/importProject.test.ts index ff2b7f8..c775cc0 100644 --- a/packages/fls/test/importProject.test.ts +++ b/packages/fls/test/importProject.test.ts @@ -9,7 +9,11 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { defaultDisplayName, + defaultSwitchConfiguration, + firewallConfiguration, + FIREWALL_PROTOCOL, interfaceKey, + deepEqual, NetworkRuntimeEngine, portSnapshot, topologySnapshot, @@ -54,6 +58,50 @@ function importFile(file: string): ImportedProject { return importProject(readTopology(values), counter()); } +/** + * describeSystem's shape. A firewall as the core has it without one + * (inactive, drop, no rules) is left out: Swift keeps a configuration only + * when the file sets a field, and the core treats both alike. + */ +function systemOf(project: ImportedProject, nodeId: Id) { + const result: Record = {}; + const switchConfiguration = project.switchConfigurations.get(nodeId); + if (switchConfiguration !== undefined) { + result.switch = { + ssid: switchConfiguration.ssid === defaultSwitchConfiguration(nodeId).ssid ? '' : switchConfiguration.ssid, + retentionTimeMilliseconds: switchConfiguration.retentionTimeMilliseconds, + }; + } + const wireless = project.hostWirelessConfigurations.get(nodeId); + if (wireless !== undefined) result.hostWireless = wireless; + const routes = project.manualRoutesByNodeId.get(nodeId); + if (routes !== undefined) { + result.manualRoutes = routes.map((route) => [route.destinationNetwork, route.subnetMask, route.gateway, route.interfaceIPAddress]); + } + const rip = project.ripEnabledByNodeId.get(nodeId); + if (rip !== undefined) result.ripEnabled = rip; + const firewall = project.firewallConfigurationsByNodeId.get(nodeId); + if (firewall !== undefined && !deepEqual(firewall, firewallConfiguration())) { + result.firewall = { + isActive: firewall.isActive, + defaultPolicy: firewall.defaultPolicy === 'accept' ? 1 : 0, + dropICMP: firewall.dropIcmp, + filterSYNSegmentsOnly: firewall.filterSynSegmentsOnly, + filterUDP: firewall.filterUdp, + rules: firewall.rules.map((rule) => [ + rule.sourceIPAddress, + rule.sourceSubnetMask, + rule.destinationIPAddress, + rule.destinationSubnetMask, + rule.port, + FIREWALL_PROTOCOL[rule.protocol], + rule.action === 'accept' ? 1 : 0, + ]), + }; + } + return result; +} + /** The shape describeImportedState (tools/swift-oracle) writes: indices instead of random ids. */ function asSwiftState(project: ImportedProject) { const { nodes, links } = project.graph; @@ -68,6 +116,7 @@ function asSwiftState(project: ImportedProject) { ports: node.ports.map((port) => ({ label: port.label, mac: port.importedMacAddress ?? null, occupied: port.isOccupied })), device: project.deviceConfigurations.get(node.id) ?? null, interfaces: node.ports.map((port) => project.interfaceConfigurations.get(interfaceKey(node.id, port.id)) ?? null), + system: systemOf(project, node.id), })), links: links.map((link) => [ nodeIndex.get(link.sourceNodeId), @@ -175,31 +224,61 @@ describe('corpus: every file imports completely', () => { } }); +/** The core started on an imported project (the editor will derive this snapshot from its state). */ +function startedEngine(project: ImportedProject): NetworkRuntimeEngine { + const engine = new NetworkRuntimeEngine(14); + engine.handle({ + type: 'start', + snapshot: topologySnapshot({ + nodes: project.graph.nodes.map((node) => ({ + id: node.id, + kind: node.kind, + ports: node.ports.map((port) => portSnapshot(port.id, port.label, port.importedMacAddress)), + })), + links: project.graph.links, + deviceConfigurations: project.deviceConfigurations, + interfaceConfigurations: project.interfaceConfigurations, + manualRoutesByNodeId: project.manualRoutesByNodeId, + ripEnabledByNodeId: project.ripEnabledByNodeId, + firewallConfigurationsByNodeId: project.firewallConfigurationsByNodeId, + switchConfigurationsByNodeId: project.switchConfigurations, + }), + seed: 14, + initialTimeMilliseconds: 0, + }); + return engine; +} + describe('an imported file runs in the simulation core', () => { it('pings across the router from the notebook with a gateway, not from the one without', () => { const project = importFile(join(corpusRoot, 'java-examples/zwei_rechnernetze_komplett.fls')); const byName = (displayName: string) => project.graph.nodes.find((node) => node.displayName === displayName)!.id; - const engine = new NetworkRuntimeEngine(14); - engine.handle({ - type: 'start', - snapshot: topologySnapshot({ - nodes: project.graph.nodes.map((node) => ({ - id: node.id, - kind: node.kind, - ports: node.ports.map((port) => portSnapshot(port.id, port.label, port.importedMacAddress)), - })), - links: project.graph.links, - deviceConfigurations: project.deviceConfigurations, - interfaceConfigurations: project.interfaceConfigurations, - }), - seed: 14, - initialTimeMilliseconds: 0, - }); + const engine = startedEngine(project); expect(engine.sendIcmpEcho(byName('Notebook 1.10'), '141.99.2.10').type).toBe('delivered'); expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.2.10')).toMatchObject({ type: 'icmpError', kind: 'destinationNetworkUnreachable' }); expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.1.10').type).toBe('delivered'); }); + + it('pings across three routers on manual routes and the routers\' default gateways', () => { + // Router 1 (141.99.1/2) sends everything else to 2.2; router 2 has manual + // routes to 1.0 and 5.0; router 3 (141.99.4/5) sends everything else to + // 4.1. The way back needs router 3's default gateway — which Swift drops. + const project = importFile(join(corpusRoot, 'java-examples/internetwork_komplett.fls')); + const notebook = project.graph.nodes.find((node) => node.displayName === 'Notebook 1.10')!.id; + const routers = project.graph.nodes.filter((node) => node.kind === 'router').map((node) => node.id); + + expect(routers.map((router) => project.deviceConfigurations.get(router)?.defaultGateway)).toEqual([ + '141.99.2.2', + '141.99.4.2', + '141.99.4.1', + ]); + expect(startedEngine(project).sendIcmpEcho(notebook, '141.99.5.10').type).toBe('delivered'); + + const withoutRouterGateways = { ...project, deviceConfigurations: new Map(project.deviceConfigurations) }; + for (const router of routers) withoutRouterGateways.deviceConfigurations.delete(router); + expect(startedEngine(withoutRouterGateways).sendIcmpEcho(notebook, '141.99.5.10').type).not.toBe('delivered'); + }); }); // ── Rules the corpus does not exercise ───────────────────────────────────── @@ -295,6 +374,94 @@ describe('importProject rules', () => { expect(project.documentationItems[0]).toMatchObject({ isBold: true, fontSize: 18, color: { alpha: 0.2 } }); }); + it("maps a router's firewall, RIP and routes, and says what it cannot map", () => { + const project = importProject( + topology({ + nodes: [ + flsNode('Vermittlungsrechner', { + interfaces: [nic('10.0.0.1')], + system: { + ripEnabled: true, + manualRoutes: [ + ['10.1.0.0', '255.255.0.0', '10.0.0.2', '10.0.0.1'], + ['10.2.0.0', '255.255.0.0', null, '10.0.0.1'], + ], + firewall: { + activated: true, + defaultPolicy: 1, + dropICMP: true, + filterSYNSegmentsOnly: false, + filterUdp: false, + rules: [ + { srcIP: '10.0.0.0', srcMask: '255.0.0.0', destIP: null, destMask: '', port: 80, protocol: -1, action: 1 }, + { srcIP: '', srcMask: '', destIP: '', destMask: '', port: -1, protocol: 99, action: 0 }, + ], + }, + }, + }), + ], + }), + counter(), + ); + const router = project.graph.nodes[0]!.id; + + expect(project.ripEnabledByNodeId.get(router)).toBe(true); + expect(project.manualRoutesByNodeId.get(router)).toEqual([ + { destinationNetwork: '10.1.0.0', subnetMask: '255.255.0.0', gateway: '10.0.0.2', interfaceIPAddress: '10.0.0.1' }, + ]); + expect(project.firewallConfigurationsByNodeId.get(router)).toEqual({ + isActive: true, + defaultPolicy: 'accept', + dropIcmp: true, + filterSynSegmentsOnly: false, + filterUdp: false, + rules: [ + { + sourceIPAddress: '10.0.0.0', + sourceSubnetMask: '255.0.0.0', + destinationIPAddress: '', + destinationSubnetMask: '', + port: 80, + protocol: 'all', + action: 'accept', + }, + { + sourceIPAddress: '', + sourceSubnetMask: '', + destinationIPAddress: '', + destinationSubnetMask: '', + port: -1, + protocol: 'tcp', + action: 'drop', + }, + ], + }); + expect(project.warnings).toEqual([ + "node 'Vermittlungsrechner': a manual route without four entries, left out", + "node 'Vermittlungsrechner': firewall protocol 99 unknown, used TCP", + ]); + }); + + it("gives a switch without SSID one from its id, and joins a host to a wireless network only with an SSID", () => { + const project = importProject( + topology({ + nodes: [ + flsNode('Switch', { portCount: 24, system: { ssid: undefined, retentionTime: 2000 } }), + flsNode('Switch', { portCount: 24, system: { ssid: ' MyWiFi ', retentionTime: 300_000 } }), + flsNode('Notebook', { interfaces: [nic('10.0.0.1')], system: { ssid: 'MyWiFi', manualRoutes: [] } }), + flsNode('Rechner', { interfaces: [nic('10.0.0.2')], system: { ssid: ' ', manualRoutes: [] } }), + ], + }), + counter(), + ); + const [first, second, notebook, pc] = project.graph.nodes.map((node) => node.id); + + expect(project.switchConfigurations.get(first!)).toEqual({ ssid: defaultSwitchConfiguration(first!).ssid, retentionTimeMilliseconds: 2000 }); + expect(project.switchConfigurations.get(second!)).toEqual({ ssid: 'MyWiFi', retentionTimeMilliseconds: 300_000 }); + expect(project.hostWirelessConfigurations.get(notebook!)).toEqual({ isEnabled: true, ssid: 'MyWiFi' }); + expect(project.hostWirelessConfigurations.has(pc!)).toBe(false); + }); + it('leaves out a cable to itself and one to a port that already has a cable', () => { const project = importProject( topology({ diff --git a/tools/swift-oracle/Sources/Oracle/main.swift b/tools/swift-oracle/Sources/Oracle/main.swift index 31482a0..4160629 100644 --- a/tools/swift-oracle/Sources/Oracle/main.swift +++ b/tools/swift-oracle/Sources/Oracle/main.swift @@ -323,6 +323,7 @@ func describeImportedState(_ state: TopologyEditorState) -> JSON { }, "device": device, "interfaces": interfaces, + "system": describeSystem(state, node), ] } let links: [[Int]] = state.graph.links.map { link in @@ -347,6 +348,40 @@ func describeImportedState(_ state: TopologyEditorState) -> JSON { return ["nodes": nodes, "links": links, "documentation": documentation] } +/// The system software settings of a node in the imported state. A switch +/// SSID Swift derives from the (random) node id is written as a marker. +func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON { + var json: JSON = [:] + if let configuration = state.switchConfigurationsByNodeID[node.id] { + let derived = TopologySwitchConfiguration.defaultConfiguration(nodeID: node.id).ssid + json["switch"] = [ + "ssid": configuration.ssid == derived ? "" : configuration.ssid, + "retentionTimeMilliseconds": Int(configuration.retentionTimeMilliseconds), + ] as JSON + } + if let wireless = state.hostWirelessConfigurationsByNodeID[node.id] { + json["hostWireless"] = ["isEnabled": wireless.isEnabled, "ssid": wireless.ssid] as JSON + } + if let routes = state.runtimeManualRoutesByNodeID[node.id] { + json["manualRoutes"] = routes.map { [$0.destinationNetwork, $0.subnetMask, $0.gateway, $0.interfaceIPAddress] } + } + if let rip = state.runtimeRIPEnabledByNodeID[node.id] { json["ripEnabled"] = rip } + if let firewall = state.runtimeFirewallConfigurationsByNodeID[node.id] { + json["firewall"] = [ + "isActive": firewall.isActive, + "defaultPolicy": firewall.defaultPolicy.rawValue, + "dropICMP": firewall.dropICMP, + "filterSYNSegmentsOnly": firewall.filterSYNSegmentsOnly, + "filterUDP": firewall.filterUDP, + "rules": firewall.rules.map { rule -> [Any] in + [rule.sourceIPAddress, rule.sourceSubnetMask, rule.destinationIPAddress, rule.destinationSubnetMask, + rule.port, rule.protocolType.rawValue, rule.action.rawValue] + }, + ] as JSON + } + return json +} + // MARK: - Archive path folding struct PathCase: Decodable { let name: String; let first: String; let second: String }