From 65ccd9ddd7ce3e443b26fa3e866aad3c328623c5 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Thu, 1 Oct 2026 08:04:18 +0000 Subject: [PATCH] =?UTF-8?q?test(core):=20WLAN=20measured=20with=20Java=20F?= =?UTF-8?q?ILIUS=20=E2=80=94=20a=20wireless=20cable=20is=20a=20cable,=20an?= =?UTF-8?q?=20SSID=20connects=20nothing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WlanRun pings on the new network wlan-lan: over wireless cables, over a plain cable, and to and from a notebook on the switch's SSID without a cable. The engine matches Java without a change: it runs the links only. Swift's wirelessAssociations, which join a host to its SSID's switch at run time, are not taken. ScenarioNetwork sets wireless cables, wireless cards and SSIDs; a corpus test pings over the wireless cables of multicast_server.fls. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 7 +- packages/core/src/runtime/engine.ts | 6 +- packages/core/test/scenario.ts | 5 +- .../wlan-java-pingFromWithoutCable.java.json | 35 +++++ .../scenarios/wlan-java-pingWired.java.json | 101 ++++++++++++++ .../wlan-java-pingWireless.java.json | 101 ++++++++++++++ .../wlan-java-pingWithoutCable.java.json | 69 ++++++++++ .../test/scenarios/wlan-lan.scenario.json | 127 ++++++++++++++++++ packages/core/test/wlanJava.test.ts | 81 +++++++++++ packages/fls/test/importProject.test.ts | 10 ++ tools/java-oracle/README.md | 29 ++++ tools/java-oracle/run.sh | 12 +- tools/java-oracle/src/ScenarioNetwork.java | 16 ++- tools/java-oracle/src/WlanRun.java | 85 ++++++++++++ 14 files changed, 677 insertions(+), 7 deletions(-) create mode 100644 packages/core/test/scenarios/wlan-java-pingFromWithoutCable.java.json create mode 100644 packages/core/test/scenarios/wlan-java-pingWired.java.json create mode 100644 packages/core/test/scenarios/wlan-java-pingWireless.java.json create mode 100644 packages/core/test/scenarios/wlan-java-pingWithoutCable.java.json create mode 100644 packages/core/test/scenarios/wlan-lan.scenario.json create mode 100644 packages/core/test/wlanJava.test.ts create mode 100644 tools/java-oracle/src/WlanRun.java diff --git a/CHANGES.md b/CHANGES.md index 81cb9a9..cb310d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,13 +13,13 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `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, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | -| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; seit F2.4c auf den Ereignissen statt blockierend und mit rekursivem Server nach `DNSServer.answerWithRemoteData` (Nameserver der Zone, dann der eigene DNS-Server, sonst Verweis), abweichend von Java fragt er sich nie selbst (Java endlos). Gateway-NAT seit F2.6 (Z. ~3585–3900) nach Javas `filius.software.nat`, an Java-Filius gemessen: die Übersetzung vor dem IP-Thread (wie Javas NAT-Threads zwischen Karte und Ethernet), ausgehend WAN-Adresse und der Port des Eintrags (ICMP behält die Kennung), eingehend nach Gegenstelle, Port und Protokoll, sonst nach der Portweiterleitung (legt einen dynamischen Eintrag an), ein Ping an die WAN-Adresse beantwortet der Heimrouter, Pakete von außen an das LAN verschwinden, beide Seiten durch die Firewall des Heimrouters, ausgehend TTL 1 bekommt „Zeit überschritten“; dynamische Einträge verfallen nach 300 s mal Javas Verzögerungsfaktor (Swift fest 300 s) bei Javas sekündlicher Prüfung, aufgefrischt nur eingehend; der Heimrouter leitet weiter wie ein Router. Abweichend von Java: ein LAN-Endpunkt bekommt für jede neue Gegenstelle einen Eintrag auf demselben WAN-Port (Java behält nur den für die erste, die Antworten weiterer bleiben am Heimrouter hängen). RIP seit F2.5a (Z. ~4700–5090) nach Javas `filius.software.rip`, seit F2.5b an Java-Filius gemessen (die Pause in Javas `float`-Rechnung): eine Route mit 0 Hops je eigenem Netz, die nie verfällt; der erste Beacon nach einer Sekunde, dann alle 25,2–35,2 s aus einem Zufall je Router (Java: ein ungesetzter `Random` je Router; der Kern setzt ihn aus Szenario und Router-ID, `ripSeed`; Swift: ein Zufall für alle); eine Nachricht je Schnittstelle ohne die Routen über sie (Split Horizon); eine neue Route oder mehr Hops vom selben Router senden sofort; eine gelernte Route wird erst beim nächsten eigenen Beacon auf 16 gesetzt, wenn sie verfallen ist (Swift: bei jedem Ereignis); Nachrichten von jedem Quellport (Swift: nur 521); ein RIP-Router leitet nur nach seiner Tabelle weiter, wenige Hops vor langer Maske. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Remote Link, WLAN | +| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; seit F2.4c auf den Ereignissen statt blockierend und mit rekursivem Server nach `DNSServer.answerWithRemoteData` (Nameserver der Zone, dann der eigene DNS-Server, sonst Verweis), abweichend von Java fragt er sich nie selbst (Java endlos). Gateway-NAT seit F2.6 (Z. ~3585–3900) nach Javas `filius.software.nat`, an Java-Filius gemessen: die Übersetzung vor dem IP-Thread (wie Javas NAT-Threads zwischen Karte und Ethernet), ausgehend WAN-Adresse und der Port des Eintrags (ICMP behält die Kennung), eingehend nach Gegenstelle, Port und Protokoll, sonst nach der Portweiterleitung (legt einen dynamischen Eintrag an), ein Ping an die WAN-Adresse beantwortet der Heimrouter, Pakete von außen an das LAN verschwinden, beide Seiten durch die Firewall des Heimrouters, ausgehend TTL 1 bekommt „Zeit überschritten“; dynamische Einträge verfallen nach 300 s mal Javas Verzögerungsfaktor (Swift fest 300 s) bei Javas sekündlicher Prüfung, aufgefrischt nur eingehend; der Heimrouter leitet weiter wie ein Router. Abweichend von Java: ein LAN-Endpunkt bekommt für jede neue Gegenstelle einen Eintrag auf demselben WAN-Port (Java behält nur den für die erste, die Antworten weiterer bleiben am Heimrouter hängen). RIP seit F2.5a (Z. ~4700–5090) nach Javas `filius.software.rip`, seit F2.5b an Java-Filius gemessen (die Pause in Javas `float`-Rechnung): eine Route mit 0 Hops je eigenem Netz, die nie verfällt; der erste Beacon nach einer Sekunde, dann alle 25,2–35,2 s aus einem Zufall je Router (Java: ein ungesetzter `Random` je Router; der Kern setzt ihn aus Szenario und Router-ID, `ripSeed`; Swift: ein Zufall für alle); eine Nachricht je Schnittstelle ohne die Routen über sie (Split Horizon); eine neue Route oder mehr Hops vom selben Router senden sofort; eine gelernte Route wird erst beim nächsten eigenen Beacon auf 16 gesetzt, wenn sie verfallen ist (Swift: bei jedem Ereignis); Nachrichten von jedem Quellport (Swift: nur 521); ein RIP-Router leitet nur nach seiner Tabelle weiter, wenige Hops vor langer Maske. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. WLAN seit F2.7 an Java-Filius gemessen: zur Laufzeit ist ein Funkkabel ein Kabel wie jedes andere, und eine SSID verbindet nichts (nach ihr legt nur Javas Editor das Funkkabel, `JHostKonfiguration.updateWifiConnection`); Swifts Verbindung über die SSID zur Laufzeit, auch ohne Kabel (`wirelessAssociations`), nicht übernommen. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Remote Link | | `src/runtime/dns.ts` | Java `filius.software.dns.DNSNachricht`, `Query`, `ResourceRecord` | neu nach Java statt nach Swift (Swift ersetzt Javas Textnachrichten durch `FILIUS-DNS-TYPED/1`): Kopfzeile `ID=… QR=… RCODE=… QDCOUNT=… …`, Frage- und Eintragszeilen, Suche nach Namen ohne Rücksicht auf Groß- und Kleinschreibung, NS der längsten Zone, sonst der Wurzel | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | | `src/runtime/random.ts` | Engine, `TopologyJavaCompatibleRandom` | übertragen; BigInt für die 48-Bit-Arithmetik; dazu `nextDouble` (Javas `Math.random`, an OpenJDK 21 gemessen) | | `src/runtime/routeTable.ts` | `State/TopologyJavaRouteTable.swift` | übertragen ohne Tabellengeometrie (gehört in den Editor) | -| `src/runtime/snapshot.ts` | Engine, `TopologyNetworkRuntimeTopologySnapshot` | übertragen, bisher mit den Feldern der übertragenen Schichten, seit F2.3 mit den DHCP-Einstellungen von Clients und Servern, seit F2.4a mit den eingeschalteten DNS-Servern | +| `src/runtime/snapshot.ts` | Engine, `TopologyNetworkRuntimeTopologySnapshot` | übertragen, bisher mit den Feldern der übertragenen Schichten, seit F2.3 mit den DHCP-Einstellungen von Clients und Servern, seit F2.4a mit den eingeschalteten DNS-Servern; ohne Swifts WLAN-Einstellungen der Rechner (`hostWirelessConfigurationsByNodeID`), die zur Laufzeit nichts bewirken (F2.7) | | `src/runtime/speed.ts` | Engine, `TopologyNetworkRuntimeSpeed` | übertragen; dazu Javas RTT (`Verbindung.holeRTT`: 50 Kabel), die Swift nicht kennt | | `src/runtime/trace.ts` | Engine, Paketspur-Modell | übertragen | | `src/util/equal.ts` | — | neu (Ersatz für Swifts `Equatable`) | @@ -32,6 +32,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `test/dhcpJava.test.ts`, `test/scenarios/*.java.json`, `test/scenario.ts`, `test/javaRun.ts` | — | neu: die DHCP-Szenarien gegen Java-Filius (`tools/java-oracle`, `DhcpRun`): je Client dieselben Frames in derselben Reihenfolge, Abstände auf 200 ms, dieselben Adressen am Ende | | `test/arpJava.test.ts`, `test/scenarios/arp-routed.scenario.json`, `test/scenarios/arp-java-*.java.json` | — | neu: vier ARP-Abläufe gegen Java-Filius (`ArpRun`): Frames, Abstände und wann jeder Aufruf zurückkehrt; die zwei Pings stimmen seit F2.4b-1, die zwei UDP-Abläufe seit F2.4b-2 (Javas Transport-Thread), drei Abläufe durch den Router seit F2.4b-3 (IP- und ICMP-Thread) | | `test/natJava.test.ts`, `test/scenarios/nat-home.scenario.json`, `test/scenarios/nat-java-*.java.json` | — | neu: sechs NAT-Abläufe gegen Java-Filius (`NatRun`): je Absenderkarte dieselben Frames in derselben Folge zur selben Zeit, dieselben Aufrufe, dieselbe NAT-Tabelle; freie Ports und TCP-Startnummern als Platzhalter; ein benannter Java-Fehler (`JAVA_BUGS`: die Antwort der zweiten Gegenstelle) | +| `test/wlanJava.test.ts`, `test/scenarios/wlan-lan.scenario.json`, `test/scenarios/wlan-java-*.java.json` | — | neu: vier Pings gegen Java-Filius (`WlanRun`): über Funkkabel, über ein Kabel und an ein Notebook mit der SSID des Switches, aber ohne Kabel (ARP scheitert in Java wie im Kern); Frames, Abstände, Ergebnis und Dauer | | `test/ripJava.test.ts`, `test/scenarios/rip-chain.scenario.json`, `test/scenarios/rip-java-*.java.json` | — | neu: zwei RIP-Abläufe gegen Java-Filius (`RipRun`, der Zufall jedes Routers auf `ripSeed` gesetzt): je Absenderkarte dieselben Nachrichten zur selben Zeit, jede Tabelle durch dieselben Zustände, derselbe Ping; die ersten 5 s (Javas gestaffelter Start) nur über den erreichten Zustand | | `test/dnsJava.test.ts`, `test/scenarios/dns-lan.scenario.json`, `test/scenarios/dns-java-*.java.json` | — | neu: fünf DNS-Abfragen gegen Java-Filius (`DnsRun`): Frames, Ergebnis und Dauer; seit F2.4b-1 alle wie Java; seit F2.4c dazu fünf mit rekursivem Server (`dns-recursive.scenario.json`, `dns-recursive-java-*.java.json`), einer nur bis zur Anfrage des Clients verglichen (`JAVA_UNTIL`: Javas Server fragt sich selbst ohne Ende) | | `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: sechs TCP-Abläufe gegen Java-Filius (`TcpRun`, drei davon mit genau einem verlorenen Frame, `loseFirst`): Frames mit Flags und Nummern, Socket-Zustände; benannte Java-Fehler (`JAVA_BUGS`: nur ein SYN, der Zustand bleibt nach dem Schließen stehen), `lostSynAck` nur bis zum zweiten SYN verglichen (`JAVA_UNTIL`) | @@ -45,7 +46,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `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; merkt sich den Java-Typ jedes Primitivs an Settern, Konstruktor- und Aufrufargumenten und welche Eigenschaften ein eigener Getter angelegt hat (für `beanCapture.ts`) | | `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, 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, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet, WLAN eines Rechners eingeschaltet nach Javas Funk-Merkmal der Karte (auch ohne SSID; Swift: jede SSID); der Webserver behält Javas Schalter für virtuelle Hosts (Swift: aus); gegen Swifts importiertes Modell auf 13 Dateien geprüft | -| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, begründete Abweichungen; im Dateisystem ausgenommen nur Swifts versteckte Zustandsdateien), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) | +| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, begründete Abweichungen; im Dateisystem ausgenommen nur Swifts versteckte Zustandsdateien), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways; seit F2.7 über Funkkabel) | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem (bei Router und Heimrouter das, was die Datei zu den Vorlagen des Konstruktors hinzufügt, oder eine eigene Wurzel, die sie ersetzt), installierte Programme von Rechner und Notebook (eigene Anwendungen eines Projekts als erfasste Bean, `beanCapture.ts`); `Character.isWhitespace` am JDK gemessen; gegen Java geprüft | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen; seit F2.5a in `packages/core/src/util/javaIp.ts` (der Kern liest damit RIP-Nachrichten) | | `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen | diff --git a/packages/core/src/runtime/engine.ts b/packages/core/src/runtime/engine.ts index 9863ed6..3c28d22 100644 --- a/packages/core/src/runtime/engine.ts +++ b/packages/core/src/runtime/engine.ts @@ -3,7 +3,11 @@ // "Runtime snapshots and transient state" (lines ~313–938) // "Deterministic runtime engine" (lines ~984–1284) // "Ethernet, ARP, IPv4, and ICMP packet runtime" (lines ~1284–3585, -// without remote link, wireless and gateway NAT; with the firewall) +// without remote link and gateway NAT; with the firewall. Wireless +// needs nothing here: in Java a wireless cable is a cable like any +// other at run time and an SSID connects nothing — only the editor +// lays the cable by it; Swift's wirelessAssociations, which join by +// SSID at run time, are not taken. Measured with Java FILIUS) // "UDP sockets and Router RIP" (lines ~4472–5187; RIP // after Java's filius.software.rip: expiry checked at the beacon, a // random of its own per router, any source port) diff --git a/packages/core/test/scenario.ts b/packages/core/test/scenario.ts index 9e63ff5..746f47a 100644 --- a/packages/core/test/scenario.ts +++ b/packages/core/test/scenario.ts @@ -22,11 +22,14 @@ export interface Scenario { seed: number; speedPercent?: number; nodes: { id: string; kind: NodeKind; ports: { id: string; label: string; mac?: string }[] }[]; - links: { id: string; sourceNode: string; sourcePort: string; targetNode: string; targetPort: string }[]; + /** `wireless`: a wireless cable (Java only: Kabel.wireless; a cable like any other at run time). */ + links: { id: string; sourceNode: string; sourcePort: string; targetNode: string; targetPort: string; wireless?: boolean }[]; devices?: Record; interfaces?: { node: string; port: string; ip: string; mask: string }[]; routes?: Record; switches?: Record; + /** Hosts with a wireless card and the SSID they join (Java only: the editor lays the cable, the simulation does not ask). */ + wireless?: Record; firewalls?: Record< string, { diff --git a/packages/core/test/scenarios/wlan-java-pingFromWithoutCable.java.json b/packages/core/test/scenarios/wlan-java-pingFromWithoutCable.java.json new file mode 100644 index 0000000..cc904ce --- /dev/null +++ b/packages/core/test/scenarios/wlan-java-pingFromWithoutCable.java.json @@ -0,0 +1,35 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000601": [], + "00000000-0000-0000-0000-000000000602": [], + "00000000-0000-0000-0000-000000000603": [], + "00000000-0000-0000-0000-000000000605": [ + { + "time": 717, + "source": "02:00:00:00:06:15", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.14", + "targetIp": "192.168.1.11" + }, + { + "time": 3468, + "source": "02:00:00:00:06:15", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.14", + "targetIp": "192.168.1.11" + } + ] + }, + "events": [ + [ + 713, + "sendProbe 192.168.1.11 called" + ], + [ + 6225, + "sendProbe 192.168.1.11 returned ICMP 3/1 from 127.0.0.1" + ] + ] +} diff --git a/packages/core/test/scenarios/wlan-java-pingWired.java.json b/packages/core/test/scenarios/wlan-java-pingWired.java.json new file mode 100644 index 0000000..de55df2 --- /dev/null +++ b/packages/core/test/scenarios/wlan-java-pingWired.java.json @@ -0,0 +1,101 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000601": [ + { + "time": 724, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.13" + }, + { + "time": 960, + "source": "02:00:00:00:06:13", + "destination": "02:00:00:00:06:11", + "arp": 2, + "senderIp": "192.168.1.13", + "targetIp": "192.168.1.11" + }, + { + "time": 964, + "source": "02:00:00:00:06:11", + "destination": "02:00:00:00:06:13", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "192.168.1.11", + "destinationIp": "192.168.1.13", + "sequence": 1 + }, + { + "time": 1189, + "source": "02:00:00:00:06:13", + "destination": "02:00:00:00:06:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "192.168.1.13", + "destinationIp": "192.168.1.11", + "sequence": 1 + } + ], + "00000000-0000-0000-0000-000000000602": [ + { + "time": 846, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.13" + } + ], + "00000000-0000-0000-0000-000000000603": [ + { + "time": 846, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.13" + }, + { + "time": 848, + "source": "02:00:00:00:06:13", + "destination": "02:00:00:00:06:11", + "arp": 2, + "senderIp": "192.168.1.13", + "targetIp": "192.168.1.11" + }, + { + "time": 1077, + "source": "02:00:00:00:06:11", + "destination": "02:00:00:00:06:13", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "192.168.1.11", + "destinationIp": "192.168.1.13", + "sequence": 1 + }, + { + "time": 1077, + "source": "02:00:00:00:06:13", + "destination": "02:00:00:00:06:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "192.168.1.13", + "destinationIp": "192.168.1.11", + "sequence": 1 + } + ], + "00000000-0000-0000-0000-000000000605": [] + }, + "events": [ + [ + 719, + "sendProbe 192.168.1.13 called" + ], + [ + 1190, + "sendProbe 192.168.1.13 returned ICMP 0/0 from 192.168.1.13" + ] + ] +} diff --git a/packages/core/test/scenarios/wlan-java-pingWireless.java.json b/packages/core/test/scenarios/wlan-java-pingWireless.java.json new file mode 100644 index 0000000..950af3c --- /dev/null +++ b/packages/core/test/scenarios/wlan-java-pingWireless.java.json @@ -0,0 +1,101 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000601": [ + { + "time": 720, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.12" + }, + { + "time": 955, + "source": "02:00:00:00:06:12", + "destination": "02:00:00:00:06:11", + "arp": 2, + "senderIp": "192.168.1.12", + "targetIp": "192.168.1.11" + }, + { + "time": 959, + "source": "02:00:00:00:06:11", + "destination": "02:00:00:00:06:12", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "192.168.1.11", + "destinationIp": "192.168.1.12", + "sequence": 1 + }, + { + "time": 1184, + "source": "02:00:00:00:06:12", + "destination": "02:00:00:00:06:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "192.168.1.12", + "destinationIp": "192.168.1.11", + "sequence": 1 + } + ], + "00000000-0000-0000-0000-000000000602": [ + { + "time": 842, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.12" + }, + { + "time": 843, + "source": "02:00:00:00:06:12", + "destination": "02:00:00:00:06:11", + "arp": 2, + "senderIp": "192.168.1.12", + "targetIp": "192.168.1.11" + }, + { + "time": 1072, + "source": "02:00:00:00:06:11", + "destination": "02:00:00:00:06:12", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "192.168.1.11", + "destinationIp": "192.168.1.12", + "sequence": 1 + }, + { + "time": 1072, + "source": "02:00:00:00:06:12", + "destination": "02:00:00:00:06:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "192.168.1.12", + "destinationIp": "192.168.1.11", + "sequence": 1 + } + ], + "00000000-0000-0000-0000-000000000603": [ + { + "time": 842, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.12" + } + ], + "00000000-0000-0000-0000-000000000605": [] + }, + "events": [ + [ + 715, + "sendProbe 192.168.1.12 called" + ], + [ + 1185, + "sendProbe 192.168.1.12 returned ICMP 0/0 from 192.168.1.12" + ] + ] +} diff --git a/packages/core/test/scenarios/wlan-java-pingWithoutCable.java.json b/packages/core/test/scenarios/wlan-java-pingWithoutCable.java.json new file mode 100644 index 0000000..d8663a0 --- /dev/null +++ b/packages/core/test/scenarios/wlan-java-pingWithoutCable.java.json @@ -0,0 +1,69 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000601": [ + { + "time": 716, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.14" + }, + { + "time": 3466, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.14" + } + ], + "00000000-0000-0000-0000-000000000602": [ + { + "time": 837, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.14" + }, + { + "time": 3578, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.14" + } + ], + "00000000-0000-0000-0000-000000000603": [ + { + "time": 837, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.14" + }, + { + "time": 3578, + "source": "02:00:00:00:06:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.1.11", + "targetIp": "192.168.1.14" + } + ], + "00000000-0000-0000-0000-000000000605": [] + }, + "events": [ + [ + 711, + "sendProbe 192.168.1.14 called" + ], + [ + 6222, + "sendProbe 192.168.1.14 returned ICMP 3/1 from 127.0.0.1" + ] + ] +} diff --git a/packages/core/test/scenarios/wlan-lan.scenario.json b/packages/core/test/scenarios/wlan-lan.scenario.json new file mode 100644 index 0000000..230abf2 --- /dev/null +++ b/packages/core/test/scenarios/wlan-lan.scenario.json @@ -0,0 +1,127 @@ +{ + "seed": 71, + "nodes": [ + { + "id": "00000000-0000-0000-0000-000000000601", + "kind": "notebook", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000611", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000602", + "kind": "notebook", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000612", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000603", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000613", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000604", + "kind": "networkSwitch", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000641", + "label": "sw1" + }, + { + "id": "00000000-0000-0000-0000-000000000642", + "label": "sw2" + }, + { + "id": "00000000-0000-0000-0000-000000000643", + "label": "sw3" + }, + { + "id": "00000000-0000-0000-0000-000000000644", + "label": "sw4" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000605", + "kind": "notebook", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000615", + "label": "eth0" + } + ] + } + ], + "links": [ + { + "id": "00000000-0000-0000-0000-00000000F601", + "sourceNode": "00000000-0000-0000-0000-000000000601", + "sourcePort": "00000000-0000-0000-0000-000000000611", + "targetNode": "00000000-0000-0000-0000-000000000604", + "targetPort": "00000000-0000-0000-0000-000000000641", + "wireless": true + }, + { + "id": "00000000-0000-0000-0000-00000000F602", + "sourceNode": "00000000-0000-0000-0000-000000000602", + "sourcePort": "00000000-0000-0000-0000-000000000612", + "targetNode": "00000000-0000-0000-0000-000000000604", + "targetPort": "00000000-0000-0000-0000-000000000642", + "wireless": true + }, + { + "id": "00000000-0000-0000-0000-00000000F603", + "sourceNode": "00000000-0000-0000-0000-000000000603", + "sourcePort": "00000000-0000-0000-0000-000000000613", + "targetNode": "00000000-0000-0000-0000-000000000604", + "targetPort": "00000000-0000-0000-0000-000000000643" + } + ], + "devices": { + "00000000-0000-0000-0000-000000000601": { + "ip": "192.168.1.11", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000602": { + "ip": "192.168.1.12", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000603": { + "ip": "192.168.1.13", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000605": { + "ip": "192.168.1.14", + "mask": "255.255.255.0" + } + }, + "switches": { + "00000000-0000-0000-0000-000000000604": { + "ssid": "schule" + } + }, + "wireless": { + "00000000-0000-0000-0000-000000000601": { + "ssid": "schule" + }, + "00000000-0000-0000-0000-000000000602": { + "ssid": "schule" + }, + "00000000-0000-0000-0000-000000000605": { + "ssid": "schule" + } + }, + "steps": [] +} diff --git a/packages/core/test/wlanJava.test.ts b/packages/core/test/wlanJava.test.ts new file mode 100644 index 0000000..ca8d4e9 --- /dev/null +++ b/packages/core/test/wlanJava.test.ts @@ -0,0 +1,81 @@ +// WLAN as Java FILIUS runs it. tools/java-oracle's WlanRun pinged on +// wlan-lan's network in Java FILIUS — notebooks with wireless cards joined to +// the switch's SSID by wireless cables, a PC on a plain cable, a notebook on +// the same SSID without a cable — and wrote every frame each card saw and +// when each call started and ended (wlan-java-.java.json). In Java a +// wireless cable is a cable like any other at run time, and an SSID connects +// nothing: only the editor reads it, to lay or remove the wireless cable +// (JHostKonfiguration.updateWifiConnection). The engine runs the scenario's +// links and knows no SSID; it makes the same calls: the frames must come in +// the same order, with the same content, at about the same distance from +// each other (javaRun.ts); the calls must end at about the same times and +// return the same. Swift instead joins a host to the switch with its SSID at +// run time, also without a cable (wirelessAssociations); not taken. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import type { Id, NetworkRuntimeEngine } from '../src/index.js'; +import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame } from './javaRun.js'; +import type { Scenario } from './scenario.js'; + +const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); +const NOTEBOOK = '00000000-0000-0000-0000-000000000601'; +const UNCABLED = '00000000-0000-0000-0000-000000000605'; + +interface JavaRun { + frames: Record; + events: [number, string][]; +} + +/** A ping in Java's words: the ICMP answer's type and code, or null when none came in time. */ +function ping(engine: NetworkRuntimeEngine, from: Id, target: string): string { + const result = engine.sendIcmpEcho(from, target); + if (result.type === 'icmpError' && result.kind === 'destinationHostUnreachable') return 'ICMP 3/1'; + const answered = (engine.state.icmpObservationsByNodeId.get(from) ?? []).some((observation) => observation.message.kind === 'echoReply'); + return answered ? 'ICMP 0/0' : 'null'; +} + +const STORIES: Record = { + pingWired: [NOTEBOOK, '192.168.1.13'], + pingWireless: [NOTEBOOK, '192.168.1.12'], + pingWithoutCable: [NOTEBOOK, '192.168.1.14'], + pingFromWithoutCable: [UNCABLED, '192.168.1.11'], +}; + +describe('WLAN as Java FILIUS runs it', () => { + const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'wlan-lan.scenario.json'), 'utf8')) as Scenario; + + it('has wireless cables and an SSID without a cable to compare', () => { + expect(scenario.links.filter((link) => link.wireless === true)).toHaveLength(2); + const cabled = new Set(scenario.links.flatMap((link) => [link.sourceNode, link.targetNode])); + expect(cabled.has(UNCABLED)).toBe(false); + expect(scenario.wireless?.[UNCABLED]?.ssid).toBe(Object.values(scenario.switches ?? {})[0]?.ssid); + }); + + for (const [name, [from, target]] of Object.entries(STORIES)) { + it(name, () => { + const java = JSON.parse(readFileSync(join(scenarioDirectory, `wlan-java-${name}.java.json`), 'utf8')) as JavaRun; + const { engine, frames } = watchedEngine(scenario); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + const events: [number, string][] = []; + const label = `sendProbe ${target}`; + events.push([engine.state.currentTimeMilliseconds, `${label} called`]); + const result = ping(engine, from, target); + events.push([engine.state.currentTimeMilliseconds, `${label} returned ${result}`]); + engine.handle({ type: 'advance', toMilliseconds: 12_000 }); + + expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => event.replace(/ from .*$/, ''))); + expect(Math.abs(events[1]![0] - events[0]![0] - (java.events[1]![0] - java.events[0]![0])), 'duration of the ping').toBeLessThanOrEqual( + GAP_TOLERANCE_MILLISECONDS, + ); + const expected = gaps(javaFrames(java.frames)); + const actual = gaps(frames()); + for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) { + expect(actual[index]?.text, `frame #${index + 1}`).toBe(expected[index]?.text); + expect(Math.abs(actual[index]!.gap - expected[index]!.gap), `gap before frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); + } + }); + } +}); diff --git a/packages/fls/test/importProject.test.ts b/packages/fls/test/importProject.test.ts index a36271e..56bfeef 100644 --- a/packages/fls/test/importProject.test.ts +++ b/packages/fls/test/importProject.test.ts @@ -490,6 +490,16 @@ describe('an imported file runs in the simulation core', () => { expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.1.10').type).toBe('delivered'); }); + it('pings over wireless cables, a cable like any other at run time (Java Kabel.wireless)', () => { + const project = importFile(join(corpusRoot, 'java-examples/multicast_server.fls')); + const byIp = (ip: string) => project.graph.nodes.find((node) => project.deviceConfigurations.get(node.id)?.ipAddress === ip)!.id; + const notebook = byIp('192.168.0.10'); + + expect(project.hostWirelessConfigurations.get(notebook)?.isEnabled).toBe(true); + expect(project.graph.links).toHaveLength(3); + expect(startedEngine(project).sendIcmpEcho(notebook, '192.168.0.20').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 diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 3a928c4..0a24683 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -417,3 +417,32 @@ Threads zwischen Karte und Ethernet-Schicht sitzen): (bei 90 % 55 Minuten), geprüft jede Sekunde, aufgefrischt nur von eingehenden Paketen (aus dem Code, nicht gemessen). + +## WLAN zur Laufzeit + +`WlanRun` startet das Netz von `wlan-lan` (Notebooks 192.168.1.11 und .12 +mit Funkkarte, je über ein Funkkabel am Switch mit der SSID „schule"; +PC 192.168.1.13 an einem Kabel; Notebook 192.168.1.14 mit Funkkarte und +SSID „schule", aber ohne Kabel) und schreibt nach `wlan-java-.java.json` +die Frames und die Aufrufe. `ScenarioNetwork` setzt dazu `Kabel.setWireless` +nach dem Feld `wireless` eines Kabels, die SSID des Switches (`switches`) +und Funkkarte und SSID der Rechner (`wireless`). Abläufe: `pingWired` (.11 +pingt .13), `pingWireless` (.11 pingt .12), `pingWithoutCable` (.11 pingt +.14), `pingFromWithoutCable` (.14 pingt .11). + +Gemessen (01.10.2026), wie im Code (`Kabel.wireless`, `Betriebssystem.ssid` +liest nur die Oberfläche): + +* Ein Funkkabel ist zur Laufzeit ein Kabel wie jedes andere: dieselben + Frames zu denselben Zeiten über Funk wie über Kabel (Ping 470 ms). +* Eine SSID verbindet nichts. Das Notebook ohne Kabel bleibt unerreichbar + (zweimal ARP, nach 5,5 s „Host unreachable" beim Absender), und es selbst + erreicht niemanden. Das Funkkabel legt nur Javas Editor + (`JHostKonfiguration.updateWifiConnection`: das alte entfernen, an den + Switch mit der SSID anschließen); ändert sich die SSID eines Switches, + entfernt er dessen Funkkabel, ohne neu zu verbinden. So entsteht in einer + Datei ein Rechner mit SSID ohne Kabel. + +Swift verbindet zur Laufzeit jeden Rechner mit eingeschaltetem WLAN mit +einem freien Port des Switches seiner SSID, auch ohne Kabel +(`wirelessAssociations`). Der Kern folgt Java: Er kennt nur die Kabel. diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index 0bc9dee..f14024d 100755 --- a/tools/java-oracle/run.sh +++ b/tools/java-oracle/run.sh @@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then fi classpath="$upstream/target/filius.jar:$upstream/target/lib/*" mkdir -p "$here/.build" -javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" +javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" # FILIUS writes its settings below $HOME; keep them out of the real one. mkdir -p "$here/.build/home" # Removed first: a crashed run must not leave the previous result standing. @@ -183,3 +183,13 @@ for story in pingLan pingRouted udpTwice udpBehind routedUdpBehind routedPingBes "$root/packages/core/test/scenarios/arp-routed.scenario.json" "$story" 16000 "$output" 2>&1 | grep -v 'logback\|^$' || true test -s "$output" done + +# WLAN as Java FILIUS runs it: pings over wireless cables, a plain cable and +# to a host on the switch's SSID without a cable (WlanRun); frames and calls. +for story in pingWired pingWireless pingWithoutCable pingFromWithoutCable; do + output="$root/packages/core/test/scenarios/wlan-java-$story.java.json" + rm -f "$output" + HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" WlanRun \ + "$root/packages/core/test/scenarios/wlan-lan.scenario.json" "$story" 12000 "$output" 2>&1 | grep -v 'logback\|^$' || true + test -s "$output" +done diff --git a/tools/java-oracle/src/ScenarioNetwork.java b/tools/java-oracle/src/ScenarioNetwork.java index b4c6956..b7884b6 100644 --- a/tools/java-oracle/src/ScenarioNetwork.java +++ b/tools/java-oracle/src/ScenarioNetwork.java @@ -1,5 +1,6 @@ // A core scenario's network (packages/core/test/scenarios/*.scenario.json: -// nodes, links, devices, dhcpServers, dhcpClients) built in Java FILIUS the +// nodes, links (wireless or not), devices, switches' and hosts' SSIDs, +// dhcpServers, dhcpClients, …) built in Java FILIUS the // way the core reads it: a port's MAC is its `mac`, else the core's // stableGeneratedMacAddress. Started as the action mode starts it (every // system software's starten()); afterwards every frame each network card saw @@ -22,8 +23,10 @@ import filius.rahmenprogramm.nachrichten.Lauscher; import filius.software.dhcp.DHCPServer; import filius.software.dns.DNSServer; import filius.software.netzzugangsschicht.EthernetFrame; +import filius.software.system.Betriebssystem; import filius.software.system.GatewayFirmware; import filius.software.system.InternetKnotenBetriebssystem; +import filius.software.system.SwitchFirmware; import filius.software.system.VermittlungsrechnerBetriebssystem; import filius.software.transportschicht.Segment; import filius.software.transportschicht.TcpSegment; @@ -139,6 +142,17 @@ public class ScenarioNetwork { Map link = map(each); Kabel cable = new Kabel(); cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) }); + cable.setWireless(Boolean.TRUE.equals(link.get("wireless"))); + } + // A switch's SSID, a host's wireless card and the SSID it joins: what the + // editor reads to lay a wireless cable; the simulation does not ask. + for (Map.Entry entry : map(scenario.getOrDefault("switches", new HashMap<>())).entrySet()) { + ((SwitchFirmware) nodes.get(entry.getKey()).getSystemSoftware()).setSSID((String) map(entry.getValue()).get("ssid")); + } + for (Map.Entry entry : map(scenario.getOrDefault("wireless", new HashMap<>())).entrySet()) { + Betriebssystem os = (Betriebssystem) system(nodes.get(entry.getKey())); + os.nicWireless(true); + os.setSsid((String) map(entry.getValue()).get("ssid")); } for (Map.Entry entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) { Map config = map(entry.getValue()); diff --git a/tools/java-oracle/src/WlanRun.java b/tools/java-oracle/src/WlanRun.java new file mode 100644 index 0000000..f5c970a --- /dev/null +++ b/tools/java-oracle/src/WlanRun.java @@ -0,0 +1,85 @@ +// Runs wlan-lan's network in Java FILIUS (ScenarioNetwork) and records what a +// wireless connection does at run time: every frame each card saw and when +// each call started and ended. +// +// WlanRun +// +// Notebooks 192.168.1.11 and .12 with wireless cards on the switch's SSID +// "schule", each joined by a wireless cable; PC 192.168.1.13 on a plain +// cable; notebook 192.168.1.14 with a wireless card on "schule" but no cable +// (as after the switch's SSID changed: Java's editor removes the cable and +// does not lay it again). The stories: +// pingWired .11 pings .13 +// pingWireless .11 pings .12 +// pingWithoutCable .11 pings .14 +// pingFromWithoutCable .14 pings .11 + +import filius.software.vermittlungsschicht.IcmpPaket; + +import java.util.ArrayList; +import java.util.List; + +public class WlanRun { + static final String NOTEBOOK = "00000000-0000-0000-0000-000000000601"; + static final String UNCABLED = "00000000-0000-0000-0000-000000000605"; + + static ScenarioNetwork network; + static final List events = new ArrayList<>(); + + static synchronized void event(String what) { + events.add(new Object[] { System.currentTimeMillis() - network.start, what }); + } + + static String icmp(IcmpPaket packet) { + return packet == null ? "null" : "ICMP " + packet.getIcmpType() + "/" + packet.getIcmpCode() + " from " + packet.getSender(); + } + + static void ping(String from, String target) { + String label = "sendProbe " + target; + event(label + " called"); + try { + event(label + " returned " + icmp(network.system(from).holeICMP().sendProbe(target, 64, 1, 1))); + } catch (Exception e) { + event(label + " failed " + e.getClass().getSimpleName()); + } + } + + public static void main(String[] args) throws Exception { + network = ScenarioNetwork.read(args[0]); + String story = args[1]; + long end = Long.parseLong(args[2]); + network.start(); + Thread.sleep(500); + switch (story) { + case "pingWired": + ping(NOTEBOOK, "192.168.1.13"); + break; + case "pingWireless": + ping(NOTEBOOK, "192.168.1.12"); + break; + case "pingWithoutCable": + ping(NOTEBOOK, "192.168.1.14"); + break; + case "pingFromWithoutCable": + ping(UNCABLED, "192.168.1.11"); + break; + default: + throw new IllegalArgumentException("unknown story " + story); + } + long rest = network.start + end - System.currentTimeMillis(); + if (rest > 0) Thread.sleep(rest); + + ScenarioNetwork.Json out = new ScenarioNetwork.Json(); + out.open('{'); + network.writeFrames(out); + synchronized (WlanRun.class) { + out.key("events").open('['); + for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']'); + out.close(']'); + } + out.close('}'); + network.stop(); + ScenarioNetwork.write(args[3], out); + System.exit(0); + } +}