diff --git a/CHANGES.md b/CHANGES.md index d18836b..56c9f69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,13 +13,16 @@ 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), aufgefrischt nur eingehend, seit 01.10.2026 genau in diesem Alter (Java findet sie erst bei seiner sekündlichen Prüfung, bis zu 1 s später; so auch der Switch: Javas `checkSAT` prüft jede Sekunde, der Kern verfällt genau zur Lernzeit); 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 genau nach ihren 75 s auf 16 gesetzt und sofort gemeldet (Java erst beim nächsten eigenen Beacon, bis zu gut 30 s später; Swift bei jedem Ereignis; seit 01.10.2026, vorher wie Java); 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. Seit der Abnahme F2 nach Javas `ARPThread`: ein Eintrag entsteht nur aus einer Anfrage nach der eigenen Adresse oder aus einer Antwort (Swift: aus jedem Paket; `swiftArp` behält das), ein Paket von der Netzadresse wird verworfen; ein Rechner leitet weiter, wenn seine IP-Weiterleitung an ist (`ipForwardingByNodeId`). Seit F2.8b nach Javas Tabellen gemessen: eine ARP-Anfrage trägt als Ziel-MAC die Broadcast-Adresse (Swift: Nullen; `swiftArp` behält sie), eine ICMP-Fehlermeldung Identifier und Sequenznummer 0 (RFC 792, Java; Swift kopiert die des Echos, ein Ping erkennt die Meldung am eingebetteten Echo), und „Host nicht erreichbar“ vom Router trägt das Paket, wie er es weitergeleitet hat (TTL gesenkt). Die Broadcast-Adresse schreibt sie immer groß (Java in der Antwort an 0.0.0.0 klein). Den Mitschnitt je Karte (`capture.ts`) schreibt sie seit F2.8a dort, wo Java ihn schreibt: beim Abgang auf das Kabel und bei der Ankunft an der Karte. 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/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), aufgefrischt nur eingehend, seit 01.10.2026 genau in diesem Alter (Java findet sie erst bei seiner sekündlichen Prüfung, bis zu 1 s später; so auch der Switch: Javas `checkSAT` prüft jede Sekunde, der Kern verfällt genau zur Lernzeit); 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 genau nach ihren 75 s auf 16 gesetzt und sofort gemeldet (Java erst beim nächsten eigenen Beacon, bis zu gut 30 s später; Swift bei jedem Ereignis; seit 01.10.2026, vorher wie Java); 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. Seit der Abnahme F2 nach Javas `ARPThread`: ein Eintrag entsteht nur aus einer Anfrage nach der eigenen Adresse oder aus einer Antwort (Swift: aus jedem Paket; `swiftArp` behält das), ein Paket von der Netzadresse wird verworfen; ein Rechner leitet weiter, wenn seine IP-Weiterleitung an ist (`ipForwardingByNodeId`). Seit F2.8b nach Javas Tabellen gemessen: eine ARP-Anfrage trägt als Ziel-MAC die Broadcast-Adresse (Swift: Nullen; `swiftArp` behält sie), eine ICMP-Fehlermeldung Identifier und Sequenznummer 0 (RFC 792, Java; Swift kopiert die des Echos, ein Ping erkennt die Meldung am eingebetteten Echo), und „Host nicht erreichbar“ vom Router trägt das Paket, wie er es weitergeleitet hat (TTL gesenkt). Die Broadcast-Adresse schreibt sie immer groß (Java in der Antwort an 0.0.0.0 klein). Den Mitschnitt je Karte (`capture.ts`) schreibt sie seit F2.8a dort, wo Java ihn schreibt: beim Abgang auf das Kabel und bei der Ankunft an der Karte. Seit F3.1 laufen Verbinden, Senden, Empfangen und Annehmen auch als Schritte eines Programm-Threads (`connectTcpSteps`, `sendTcpSteps`, `receiveTcpSteps`, `acceptTcpSteps`, `resolveHostSteps`); die blockierenden Aufrufe sind nur noch eine Hülle darum. Abweichend von Java: Ein Verbindungsaufbau ohne Route scheitert sofort (Javas Socket-Thread stirbt an der `RouteNotFoundException`, `verbinden` wartet ewig), eine Nachricht, die vor dem Schließen ankam, wird noch ausgegeben (Java verwirft sie), und Senden unterscheidet wie Java „nicht verbunden", „unterbrochen" und „Zeitüberschreitung". 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/threads.ts` | Javas Threads der Programme (`Anwendung extends Thread`, `ServerMitarbeiter`) | neu (F3.1): ein Programm-Thread als Generator, der liefert, worauf er wartet (Bedingung, Zeit oder beides); die Engine setzt nach jedem Ereignis fort, wessen Warten vorbei ist, in der Reihenfolge, in der die Threads begannen; nichts blockiert die Engine. Abweichend von Java: ein Thread wacht sofort, wenn seine Bedingung gilt (Java sieht oft nur alle 100 ms nach) | +| `src/runtime/apps/program.ts` | Java `filius.software.Anwendung` | neu (F3.1): die Zeilen im Fenster eines Programms (strukturiert, damit der Editor sie übersetzt) und der Befehls-Thread (`ausfuehren`) | +| `src/runtime/apps/echo.ts` | Java `filius.software.clientserver` (`ServerAnwendung`, `ServerMitarbeiter`, `ServerBaustein`, `ServerBausteinMitarbeiter`, `ClientAnwendung`, `ClientBaustein`) | neu nach Java statt nach Swift (F3.1): Echo-Server (ein Thread nimmt Verbindungen an, solange er eingeschaltet ist, je Verbindung einer sendet jede Nachricht zurück; ausgeschaltet schließt er Socket und Verbindungen; Port belegt → er schaltet sich aus) und einfacher Client (verbinden über Adresse oder Namen, senden, empfangen, trennen), mit Javas Fensterzeilen. An Java-Filius gemessen. Abweichend von Java: Der Server merkt das Ausschalten, der Client die Verbindung sofort (Java alle 100 ms); ohne Route scheitert der Client sofort (Java wartet ewig ohne Meldung); ausgeschaltet bestätigt der Server noch das FIN seiner Verbindungen (Java gibt den Port frei, das FIN bleibt unbeantwortet) | | `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 der Abnahme F2 mit der IP-Weiterleitung, wo sie vom Standard der Art abweicht, 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/snapshot.ts` | Engine, `TopologyNetworkRuntimeTopologySnapshot` | übertragen, bisher mit den Feldern der übertragenen Schichten, seit der Abnahme F2 mit der IP-Weiterleitung, wo sie vom Standard der Art abweicht, seit F2.3 mit den DHCP-Einstellungen von Clients und Servern, seit F2.4a mit den eingeschalteten DNS-Servern, seit F3.1 mit den installierten Echo-Servern (Port, ein oder aus) und einfachen Clients; 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/runtime/captureRows.ts` | Java `Lauscher.datenVorbereiten`, `classifyApplicationLayerProtocol`, die `toString` von `ArpPaket`, `IpPaket`, `IcmpPaket` | neu nach Java (F2.8b): die Tabelle des Mitschnitts, je Frame eine Zeile je Schicht (Ethernet; ARP, ICMP oder IP; TCP oder UDP; die Daten, wenn Javas `trim` etwas übrig lässt), in beiden Ansichten Javas (Ende als Adresse:Port samt Datenzeile, oder nur Ports und die Datenzeile ohne Enden); die Bemerkung bleibt strukturiert, `captureCommentText` sagt sie mit gegebenen Wörtern und Zahlenformat wie Java. An Javas eigenen Tabellen gemessen. Abweichend von Java: „ICMP Time Exceeded“ (Java: „Exeeded“) | @@ -35,6 +38,8 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `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/javaRun.ts` (`expectSameCaptures`) | — | seit F2.8a: der Mitschnitt jeder Karte gegen Javas Liste dieser Karte (dieselben Frames, dieselbe Folge, dieselbe Zeit), in den Vergleichen für ARP, WLAN, DNS, TCP, NAT, RIP und DHCP mit einem Client; seit F2.8b für jeden dieser Frames auch Javas Tabellenzeilen in beiden Ansichten (Wörter und Zahlen wie Javas deutsches Gebietsschema) | +| `test/echoJava.test.ts`, `test/scenarios/echo-java-*.java.json` | — | neu (F3.1): acht Abläufe mit Echo-Server und einfachem Client gegen Java-Filius (`EchoRun`): je Karte Frames, Zeiten und Tabellenzeilen, dazu die Fensterzeilen beider Programme auf Deutsch wie Java (Zeilen, die Java erst bei seinem 100-ms-Blick schreibt, dürfen dort bis zu 100 ms später kommen); drei benannte Java-Fehler (`JAVA_BUGS`: keine Route, nur ein SYN, das unbeantwortete FIN nach dem Ausschalten) | +| `test/apps.test.ts` | — | neu (F3.1): wie Threads aufwachen (nach dem Ereignis, nach einer Pause hinter den anderen, ein Weckruf nur für sein Warten, Reihenfolge des Beginns, kein Warten auf schon Geltendes), und was die Java-Abläufe nicht zeigen: belegter Port, Name über DNS und ohne DNS, keine Route, unbestätigtes Senden, Neustart, eine Nachricht vor dem Schließen | | `test/captureRows.test.ts` | — | neu (F2.8b): die Regeln der Tabelle, die kein Java-Lauf zeigt (GNT, POP3, SMTP, HTTP, Javas `trim`, ICMP-Fehler mit eingebettetem ICMP, Ansicht ohne Enden) | | `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 | @@ -62,7 +67,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/beanCapture.ts` | — | neu: eine decodierte Bean zurück in die Form des Schreibers, mit Javas Zahlentypen; der Verweis auf das Betriebssystem des Knotens als Marke; was das Format nicht sagen kann (Unaufgelöstes, `Field`, Fabrikmethode, ein anderswo per Getter erreichtes Objekt), wird abgelehnt, nicht geraten; Rundlauf-Tests in `test/beanCapture.test.ts` | | `src/javaBeansWriter.ts` | — | neu: Schreiber für das `XMLDecoder`-Format, Gegenstück zu `javabeans.ts`; ausdrücklich typisierte Eingabe, geteilte Objekte über `id`/`idref`, was XML nicht fassen kann (und CR) als ``; von Java selbst geprüft (`BeansDump`) | | `test/javaBeansWriter.test.ts`, `test/beans/` | — | neu: 11 Dateien des Schreibers, von Javas `XMLDecoder` gelesen und beschrieben; Erwartungen von Hand | -| `src/projectSnapshot.ts` | Swift `TopologyNetworkRuntimeTopologySnapshot(editorState:)` | neu (Abnahme F2): das importierte Projekt als Schnappschuss des Kerns; nur die Kabel verbinden (Swifts Verbindung über die SSID nicht übernommen), nur eingeschaltete DNS-Server laufen | +| `src/projectSnapshot.ts` | Swift `TopologyNetworkRuntimeTopologySnapshot(editorState:)` | neu (Abnahme F2): das importierte Projekt als Schnappschuss des Kerns; nur die Kabel verbinden (Swifts Verbindung über die SSID nicht übernommen), nur eingeschaltete DNS-Server laufen; seit F3.1 jeder installierte Echo-Server (ein oder aus, er lässt sich zur Laufzeit schalten) und einfache Client | | `test/examplesJava.test.ts`, `test/examples/*.java.json` | — | neu (Abnahme F2): fünf Beispiele von Filius (`internetwork_komplett`, `einfaches_rechnernetz_komplett`, `dns_server`, `nat_with_mapped_port`, `Internet_Komplett_…_DE`) mit dem eigenen Leser eingelesen und im Kern gespielt, gegen Java-Filius (`ExampleRun`): je Karte Frames, Zeiten und Tabellenzeilen, die Aufrufe, die Adressen am Ende; gleichzeitig startende DHCP-Clients je Client; zwei benannte Java-Fehler (`JAVA_BUGS`: Ping ohne Gateway wirft eine `NullPointerException`; NAT-Eintrag nur für die erste Gegenstelle) | | `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` und der Systemsoftware | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Systemsoftware über ihre Setter: SSID, Lernzeit, RIP, manuelle Routen, DHCP-Client und -Server, statische NAT, Modem, Router-Firewall (über das `konfig`-Plug-in des Webservers, wie XMLEncoder sie erreicht); eigene Anwendungen eines Projekts, wie die Datei sie hatte; Dateisysteme (an die Wurzel des Konstruktors angehängt wie bei XMLEncoder, beim Router hinter dessen Vorlagen); installierte Programme nach den Dateien (Webserver, Tauschbörse und E-Mail-Server legen ihre Ordner nur an, wo sie fehlen; der DNS-Server liest `/dns/hosts` beim Installieren), je unter ihrem Schlüssel mit der Systemsoftware wie `installApp`, samt Einstellungen und persönlicher Firewall; E-Mail: Domain und Bean-Konten des Servers samt Postfächern, Ordner und Adressbuch des Clients (sein Konto steht in `/konten.txt`, einer Datei); Javas Fehler nicht übernommen: ein Empfänger, den eine ältere Datei als Zeichenkette hält, wird als der `AddressEntry` geschrieben, den Javas `AddressEntry(String)` daraus macht (Java legt die Zeichenkette ungeprüft in die typisierte Liste, sein E-Mail-Fenster scheitert daran), ein `null`-Empfänger fällt weg; Kopf „Filius version: 2.14.0 (mdg-filius)“; seit der Abnahme F2 `ipForwardingEnabled`, wo es vom Standard der Klasse abweicht | | `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie, Systemsoftware, Dateisysteme und Programme samt E-Mail wie beim Original (bis auf die benannte Abweichung bei Empfängern als Zeichenkette, nur in `man-in-the-middle.fls`) (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat (u. a. eingeschaltete Router-Firewall mit Regeln jedes Protokolls, alle Firewall-Schalter abseits der Vorgabe, Modem mit eigenem Port und Ziel, verschachtelter Dateibaum mit umbenannter Wurzel, namenlosem Ordner, Datei ohne Typ und Inhalt mit CR/CRLF und Zeichen jenseits der BMP, Heimrouter mit eigener Wurzel, jedes Filius-Programm mit Einstellungen abseits der Vorgabe samt persönlicher Firewall, von Java angelegte Ordner `/peer2peer` und `/mailserver`, E-Mail-Server und -Client mit Konto, Postfach, drei Ordnern, Adressbuch und Empfängern aller Formen) | diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8d37772..5e72c2f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,6 +17,9 @@ export * from './runtime/speed.js'; export * from './runtime/trace.js'; export * from './runtime/capture.js'; export * from './runtime/captureRows.js'; +export * from './runtime/threads.js'; +export * from './runtime/apps/program.js'; +export * from './runtime/apps/echo.js'; export * from './util/equal.js'; export * from './util/javaIp.js'; export * from './util/swiftString.js'; diff --git a/packages/core/src/runtime/apps/echo.ts b/packages/core/src/runtime/apps/echo.ts new file mode 100644 index 0000000..9039364 --- /dev/null +++ b/packages/core/src/runtime/apps/echo.ts @@ -0,0 +1,268 @@ +// FILIUS' echo server and generic client (filius.software.clientserver: +// ServerAnwendung, ServerMitarbeiter, ServerBaustein, ServerBausteinMitarbeiter; +// ClientAnwendung, ClientBaustein). The server takes connections on its port +// while it is switched on and sends each message back; the client connects +// to a server, sends what it is given and shows what comes back. +// +// Better than Java: the server notices at once that it was switched off, and +// the client at once that it is connected (Java looks every 100 ms); a +// connection to an address without a route fails (Java's client waits for +// ever, without a word). + +import type { Id } from '../../ids.js'; +import type { NetworkRuntimeEngine, TcpSendResult } from '../engine.js'; +import { pause, waitUntil, type Steps } from '../threads.js'; +import { CommandThread, ProgramLog } from './program.js'; + +/** Why a send failed (TCPSocket.senden's exceptions). */ +export type SendFailure = Exclude; + +export type EchoServerLogEntry = + /** sw_serveranwendung_msg2: switched on. */ + | { readonly kind: 'acceptingStarted' } + /** sw_serveranwendung_msg1: switched off. */ + | { readonly kind: 'acceptingStopped' } + /** sw_serveranwendung_msg3: the listening socket closed after the server was switched off. */ + | { readonly kind: 'serverSocketClosed' } + /** sw_serveranwendung_msg6: the port is taken; the server switches itself off. */ + | { readonly kind: 'serverSocketFailed' } + /** sw_serveranwendung_msg4, msg5: connected to the client at its address and port. */ + | { readonly kind: 'connectionAccepted'; readonly address: string; readonly port: number } + /** ">>" and the message. */ + | { readonly kind: 'received'; readonly text: string } + /** "<<" and the message, once it was acknowledged. */ + | { readonly kind: 'sent'; readonly text: string } + | { readonly kind: 'sendFailed'; readonly failure: SendFailure } + /** sw_servermitarbeiter_msg1, msg2: the connection to the client at its address and port closed. */ + | { readonly kind: 'connectionClosed'; readonly address: string; readonly port: number }; + +/** Why a connection could not be made (shown after sw_clientbaustein_msg1). */ +export type ConnectFailure = + /** sw_socket_msg1: the name has no address. */ + | 'unresolved' + /** sw_tcpsocket_msg2: the server did not answer. */ + | 'timedOut' + /** sw_tcpsocket_msg3: no route, or no free port. */ + | 'failed'; + +export type SimpleClientLogEntry = + /** sw_clientbaustein_msg2. */ + | { readonly kind: 'connected' } + /** sw_clientbaustein_msg1 and the reason. */ + | { readonly kind: 'connectFailed'; readonly failure: ConnectFailure } + /** sw_clientbaustein_msg3: the user disconnected. */ + | { readonly kind: 'disconnected' } + /** sw_clientbaustein_msg4: a send without a connection. */ + | { readonly kind: 'notConnected' } + | { readonly kind: 'sent'; readonly text: string } + | { readonly kind: 'received'; readonly text: string } + | { readonly kind: 'sendFailed'; readonly failure: SendFailure } + /** sw_clientbaustein_msg5, msg6: the connection to the server at its address and port ended. */ + | { readonly kind: 'connectionClosed'; readonly address: string; readonly port: number } + /** sw_tcpsocket_msg9: the receive found no connection. */ + | { readonly kind: 'receiveFailed' }; + +const encode = (text: string) => new TextEncoder().encode(text); +const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); + +export class EchoServer { + readonly log: ProgramLog; + private active: boolean; + private portValue: number; + private listenerId: Id | undefined; + private readonly connections = new Set(); + + constructor( + private readonly engine: NetworkRuntimeEngine, + private readonly nodeId: Id, + configuration: { readonly port: number; readonly isActive: boolean }, + ) { + this.log = new ProgramLog(engine); + this.portValue = configuration.port; + this.active = configuration.isActive; + engine.startThread(this.acceptConnections()); + } + + get isActive(): boolean { + return this.active; + } + + get port(): number { + return this.portValue; + } + + /** Takes effect when the server next opens its socket, as in Java. */ + setPort(port: number): void { + this.portValue = port; + } + + /** ServerAnwendung.setAktiv: switched off, it closes its socket and every connection. */ + setActive(active: boolean): void { + this.active = active; + if (!active) { + if (this.listenerId !== undefined) this.engine.closeTcpSocket(this.listenerId); + for (const socketId of this.connections) this.engine.closeTcpSocket(socketId); + this.connections.clear(); + this.log.write({ kind: 'acceptingStopped' }); + } else { + this.log.write({ kind: 'acceptingStarted' }); + } + this.engine.resumeThreads(); + } + + /** ServerAnwendung.annehmenVerbindungen. */ + private *acceptConnections(): Steps { + for (;;) { + if (this.active) { + if (this.listenerId === undefined) { + this.listenerId = this.engine.openTcpServerSocket(this.nodeId, this.portValue); + if (this.listenerId === undefined) { + this.log.write({ kind: 'serverSocketFailed' }); + this.setActive(false); + continue; + } + } + const accepted = yield* this.engine.acceptTcpSteps(this.listenerId); + const socket = accepted === undefined ? undefined : this.engine.state.socketsById.get(accepted); + if (accepted !== undefined && socket?.remoteIPAddress !== undefined && socket.remotePort !== undefined) { + this.connections.add(accepted); + this.engine.startThread(this.serve(accepted, socket.remoteIPAddress, socket.remotePort)); + this.log.write({ kind: 'connectionAccepted', address: socket.remoteIPAddress, port: socket.remotePort }); + } + } else { + if (this.listenerId !== undefined) { + // After the connections' threads have seen theirs close (Java's loop looks later than they do). + yield pause(this.engine.state.currentTimeMilliseconds); + this.engine.closeTcpSocket(this.listenerId); + this.log.write({ kind: 'serverSocketClosed' }); + } + this.listenerId = undefined; + yield* waitUntil(() => this.active); + } + } + } + + /** ServerMitarbeiter.run with ServerBausteinMitarbeiter: each message back to where it came from. */ + private *serve(socketId: Id, address: string, port: number): Steps { + for (;;) { + const received = yield* this.engine.receiveTcpSteps(socketId); + if (received.type !== 'message') break; + const text = decode(received.payload); + this.log.write({ kind: 'received', text }); + const result = yield* this.engine.sendTcpSteps(socketId, encode(text)); + this.log.write(result === 'sent' ? { kind: 'sent', text } : { kind: 'sendFailed', failure: result }); + } + this.engine.closeTcpSocket(socketId); + this.connections.delete(socketId); + this.log.write({ kind: 'connectionClosed', address, port }); + } +} + +export class SimpleClient { + readonly log: ProgramLog; + private targetAddressValue: string; + private targetPortValue: number; + private socketId: Id | undefined; + /** The program's thread (Anwendung): connects, then receives until the connection ends. */ + private readonly commands: CommandThread; + /** The window's: its sends, one after the other (Java's senden blocks the window). */ + private readonly sends: CommandThread; + + constructor( + private readonly engine: NetworkRuntimeEngine, + private readonly nodeId: Id, + configuration: { readonly targetAddress: string; readonly targetPort: number }, + ) { + this.log = new ProgramLog(engine); + this.targetAddressValue = configuration.targetAddress; + this.targetPortValue = configuration.targetPort; + this.commands = new CommandThread(engine); + this.sends = new CommandThread(engine); + } + + get targetAddress(): string { + return this.targetAddressValue; + } + + get targetPort(): number { + return this.targetPortValue; + } + + get isConnected(): boolean { + return this.socketId !== undefined && this.engine.state.socketsById.get(this.socketId)?.tcpState === 'ESTABLISHED'; + } + + /** ClientBaustein.verbinden: to the server at the address (or name) and port. */ + connect(address: string, port: number): void { + this.targetAddressValue = address; + this.targetPortValue = port; + this.commands.hand(() => this.openConnection(address, port)); + this.commands.hand(() => this.receiveMessages()); + this.engine.resumeThreads(); + } + + /** ClientBaustein.trennen. */ + disconnect(): void { + if (this.socketId !== undefined) { + this.engine.closeTcpSocket(this.socketId); + this.log.write({ kind: 'disconnected' }); + } + this.engine.resumeThreads(); + } + + /** ClientBaustein.senden. */ + send(text: string): void { + this.sends.hand(() => this.sendMessage(text)); + this.engine.resumeThreads(); + } + + /** + * ClientBaustein.initialisiereSocket. Java does nothing while connected; + * it never is here: the command runs after the last connection's receive + * loop, which ends only with the connection. + */ + private *openConnection(address: string, port: number): Steps { + const ipAddress = yield* this.engine.resolveHostSteps(this.nodeId, address); + if (ipAddress === undefined) { + this.socketId = undefined; + this.log.write({ kind: 'connectFailed', failure: 'unresolved' }); + return; + } + const socketId = this.engine.openTcpClientSocket(this.nodeId, ipAddress, port); + const result = socketId === undefined ? 'invalidSocket' : yield* this.engine.connectTcpSteps(socketId); + if (result === 'connected') { + this.socketId = socketId; + this.log.write({ kind: 'connected' }); + return; + } + this.socketId = undefined; + this.log.write({ kind: 'connectFailed', failure: result === 'timedOut' ? 'timedOut' : 'failed' }); + } + + /** ClientBaustein.empfangeNachricht: until the connection ends. */ + private *receiveMessages(): Steps { + while (this.isConnected) { + const socketId = this.socketId!; + const received = yield* this.engine.receiveTcpSteps(socketId); + if (received.type === 'message') { + this.log.write({ kind: 'received', text: decode(received.payload) }); + } else if (received.type === 'closed') { + this.engine.closeTcpSocket(socketId); + const socket = this.engine.state.socketsById.get(socketId); + this.log.write({ kind: 'connectionClosed', address: socket?.remoteIPAddress ?? '', port: socket?.remotePort ?? 0 }); + } else { + this.log.write({ kind: 'receiveFailed' }); + this.socketId = undefined; + } + } + } + + private *sendMessage(text: string): Steps { + if (!this.isConnected) { + this.log.write({ kind: 'notConnected' }); + return; + } + const result = yield* this.engine.sendTcpSteps(this.socketId!, encode(text)); + this.log.write(result === 'sent' ? { kind: 'sent', text } : { kind: 'sendFailed', failure: result }); + } +} diff --git a/packages/core/src/runtime/apps/program.ts b/packages/core/src/runtime/apps/program.ts new file mode 100644 index 0000000..11b2500 --- /dev/null +++ b/packages/core/src/runtime/apps/program.ts @@ -0,0 +1,43 @@ +// What FILIUS' programs share (filius.software.Anwendung): the lines a +// program writes into its window, and the program's own thread that runs +// the commands handed to it one after the other (Anwendung.ausfuehren). + +import type { NetworkRuntimeEngine } from '../engine.js'; +import { waitUntil, type Steps } from '../threads.js'; + +/** A line of a program's window: what happened, structured, so that the editor says it in the viewer's language. */ +export type LogLine = Entry & { readonly timeMilliseconds: number }; + +export class ProgramLog { + private readonly lines: LogLine[] = []; + + constructor(private readonly engine: NetworkRuntimeEngine) {} + + write(entry: Entry): void { + this.lines.push({ ...entry, timeMilliseconds: this.engine.state.currentTimeMilliseconds }); + } + + get all(): readonly LogLine[] { + return this.lines; + } +} + +/** A thread that runs the commands handed to it, each to its end, in the order they came. */ +export class CommandThread { + private readonly commands: (() => Steps)[] = []; + + constructor(engine: NetworkRuntimeEngine) { + engine.startThread(this.run()); + } + + hand(command: () => Steps): void { + this.commands.push(command); + } + + private *run(): Steps { + for (;;) { + yield* waitUntil(() => this.commands.length > 0); + yield* this.commands.shift()!(); + } + } +} diff --git a/packages/core/src/runtime/engine.ts b/packages/core/src/runtime/engine.ts index a058775..fa14c63 100644 --- a/packages/core/src/runtime/engine.ts +++ b/packages/core/src/runtime/engine.ts @@ -111,6 +111,8 @@ import { JavaCompatibleRandom } from './random.js'; import { routeTableRows, type RouteRow } from './routeTable.js'; import { EMPTY_TOPOLOGY_SNAPSHOT, withSnapshot, type NodeSnapshot, type TopologySnapshot } from './snapshot.js'; import { RuntimeSpeed } from './speed.js'; +import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.js'; +import { EchoServer, SimpleClient } from './apps/echo.js'; import { PRODUCTION_TRACE_RETENTION, type HeaderField, @@ -152,6 +154,8 @@ export type ScheduledEventKind = /** The lookups waiting for an address wake up, after what else happens at that time. */ | { readonly kind: 'arpAnswered'; readonly nodeId: Id; readonly ipAddress: string; readonly macAddress: string } | { readonly kind: 'tcpTimeout'; readonly sessionId: Id } + /** A program's thread waited until this time (threads.ts). */ + | { readonly kind: 'applicationWake'; readonly wakeId: number } /** A DNS query waited five round trips for its answer (DNSQueryAgent). */ | { readonly kind: 'dnsQueryTimeout'; readonly socketId: Id } | { @@ -333,10 +337,17 @@ export interface TcpSession { closingWaitsLeft: number | undefined; receiveAssembly: Uint8Array; receivedMessages: Uint8Array[]; + /** Set when data went unacknowledged after the last attempt and the socket closed for it. */ + dataTimedOut?: boolean; } export type TcpConnectionResult = 'connected' | 'invalidSocket' | 'unreachable' | 'timedOut'; +export type TcpSendResult = 'sent' | 'notConnected' | 'connectionLost' | 'timedOut'; + +/** What a program's receive gets (receiveTcpSteps). */ +export type TcpReceived = { readonly type: 'message'; readonly payload: Uint8Array } | { readonly type: 'closed' } | { readonly type: 'notConnected' }; + /** Java's DNSQueryAgent waits five round trips for an answer. */ export const DNS_QUERY_TIMEOUT_ROUND_TRIPS = 5; export type DnsResolution = { readonly type: 'answered'; readonly records: readonly DnsMessageRecord[] } | { readonly type: 'timedOut' }; @@ -850,6 +861,31 @@ export class NetworkRuntimeEngine { private readonly swiftArp: boolean; private readonly swiftSends: boolean; private readonly packetCapture: PacketCapture; + /** The programs' threads (threads.ts), and the wake-ups they wait for, by id. */ + private readonly applicationThreads = new ApplicationThreads({ + nowMilliseconds: () => this.stateValue.currentTimeMilliseconds, + scheduleWake: (deadlineMilliseconds, wake) => { + const wakeId = this.nextApplicationWakeId++; + this.applicationWakes.set(wakeId, wake); + this.schedule(deadlineMilliseconds, { kind: 'applicationWake', wakeId }); + }, + asEvent: (step) => { + const was = this.isProcessingScheduledEvents; + this.isProcessingScheduledEvents = true; + try { + step(); + } finally { + this.isProcessingScheduledEvents = was; + } + }, + }); + private readonly applicationWakes = new Map void>(); + /** The programs, by node (initializePrograms). */ + private readonly echoServersByNodeId = new Map(); + private readonly simpleClientsByNodeId = new Map(); + /** Accepted connections a program has taken (acceptTcpSteps). */ + private readonly handedOutTcpSocketIds = new Set(); + private nextApplicationWakeId = 1; /** * `swiftCables`: a cable carries all frames at once, as in the Swift @@ -938,11 +974,15 @@ export class NetworkRuntimeEngine { this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n); this.dnsIdRandom = new JavaCompatibleRandom(seed ^ 0x444e_5349n); this.isProcessingScheduledEvents = false; + this.applicationThreads.clear(); + this.applicationWakes.clear(); + this.handedOutTcpSocketIds.clear(); this.cableBusyUntilByPort.clear(); this.initializeRipState(seed); this.initializeDhcpState(); this.initializeDnsState(); this.initializeNatState(); + this.initializePrograms(); return [this.output({ type: 'started' })]; } @@ -952,6 +992,11 @@ export class NetworkRuntimeEngine { state.phase = 'stopped'; state.globalPacketLossEnabled = false; this.isProcessingScheduledEvents = false; + this.applicationThreads.clear(); + this.applicationWakes.clear(); + this.handedOutTcpSocketIds.clear(); + this.echoServersByNodeId.clear(); + this.simpleClientsByNodeId.clear(); this.arpLookups = []; this.protocolThreads.clear(); this.dnsQueriesBySocketId.clear(); @@ -996,6 +1041,61 @@ export class NetworkRuntimeEngine { } } + /** The node's echo server while the simulation runs, if it has one installed. */ + echoServer(nodeId: Id): EchoServer | undefined { + return this.echoServersByNodeId.get(nodeId); + } + + /** The node's generic client while the simulation runs, if it has one installed. */ + simpleClient(nodeId: Id): SimpleClient | undefined { + return this.simpleClientsByNodeId.get(nodeId); + } + + /** + * Each node's programs start with the simulation (Betriebssystem.starten), + * from the snapshot; what they show starts empty. + */ + private initializePrograms(): void { + const snapshot = this.stateValue.topologySnapshot; + this.echoServersByNodeId.clear(); + this.simpleClientsByNodeId.clear(); + for (const node of snapshot.nodes) { + const echoServer = snapshot.echoServersByNodeId.get(node.id); + if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer)); + const simpleClient = snapshot.simpleClientsByNodeId.get(node.id); + if (simpleClient !== undefined) this.simpleClientsByNodeId.set(node.id, new SimpleClient(this, node.id, simpleClient)); + } + } + + /** Begins a program's thread (threads.ts): it runs at once up to its first wait. */ + startThread(steps: Steps): void { + if (this.stateValue.phase !== 'running') return; + this.applicationThreads.start(steps); + } + + /** Resumes the threads whose wait is over: after a call from outside that changed what they wait for. */ + resumeThreads(): void { + this.applicationThreads.resume(); + } + + /** Runs steps to their end, waiting on the virtual clock as a caller of Java blocks. */ + private runBlocking(steps: Steps): T { + for (let next = steps.next(); ; next = steps.next()) { + if (next.done === true) return next.value; + const wait: Wait = next.value; + const until = wait.until ?? (() => false); + if (wait.deadlineMilliseconds !== undefined) { + this.advanceUntil(until, wait.deadlineMilliseconds); + continue; + } + while (this.stateValue.phase === 'running' && !until()) { + const deadline = this.earliestDeadline(() => true); + if (deadline === undefined) throw new Error('blocking call waits for nothing that can happen'); + this.advance(deadline); + } + } + } + allocateFrameIdentity(): number { return this.stateValue.nextFrameIdentity++; } @@ -1071,6 +1171,7 @@ export class NetworkRuntimeEngine { this.stateValue.currentTimeMilliseconds = event.deadlineMilliseconds; this.ageAllSwitchSatEntries(); this.processScheduledRuntimeEvent(event.kind); + this.applicationThreads.resume(); outputs.push(this.output({ type: 'fired', event })); } @@ -1160,6 +1261,12 @@ export class NetworkRuntimeEngine { case 'tcpTimeout': this.handleTcpTimeout(kind.sessionId); return; + case 'applicationWake': { + const wake = this.applicationWakes.get(kind.wakeId); + this.applicationWakes.delete(kind.wakeId); + wake?.(); + return; + } case 'remoteLinkDelivery': case 'lanRemoteLinkTransmission': throw new NotPortedError('remote links'); @@ -1667,6 +1774,32 @@ export class NetworkRuntimeEngine { return { type: 'resolved', address: findDnsRecords(domainName, 'A', resolution.records)[0]?.data }; } + /** Resolver.holeIPAdresse in a program's thread. */ + *resolveDnsAddressSteps(nodeId: Id, name: string): Steps { + const domainName = name.endsWith('.') ? name : `${name}.`; + let lookup: DnsLookup | undefined; + const server = this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer; + this.beginDnsResolution(nodeId, domainName, 'A', server, (result) => { + lookup = result; + }); + yield* waitUntil(() => lookup !== undefined || this.stateValue.phase !== 'running'); + const result = lookup as DnsLookup | undefined; + if (result === undefined || result.type === 'timedOut') return { type: 'timedOut' }; + return { type: 'resolved', address: findDnsRecords(domainName, 'A', [...result.answers, ...result.authority])[0]?.data }; + } + + /** + * The address a socket of Java's goes to (Socket's constructor): the + * address itself when it is one (IP.ipCheck), else the name's A record; + * undefined when there is none or the server did not answer. + */ + *resolveHostSteps(nodeId: Id, address: string): Steps { + const ipAddress = javaIpCheck(address); + if (ipAddress !== null) return ipAddress; + const resolution = yield* this.resolveDnsAddressSteps(nodeId, address); + return resolution.type === 'resolved' ? resolution.address : undefined; + } + /** Resolver.holeIPAdresseMailServer: the address of the domain's mail exchange. */ resolveDnsMailServer(nodeId: Id, domain: string): DnsAddressResolution { const domainName = domain.endsWith('.') ? domain : `${domain}.`; @@ -1818,6 +1951,11 @@ export class NetworkRuntimeEngine { /** The three-way handshake from a closed client socket, waiting on the virtual clock as Java blocks. */ connectTcpWithResult(socketId: Id): TcpConnectionResult { + return this.runBlocking(this.connectTcpSteps(socketId)); + } + + /** The three-way handshake from a closed client socket, in a program's thread (TCPSocket.verbinden). */ + *connectTcpSteps(socketId: Id): Steps { const state = this.stateValue; const socket = state.socketsById.get(socketId); if ( @@ -1839,10 +1977,13 @@ export class NetworkRuntimeEngine { ) { return 'invalidSocket'; } + // No route to the server: Java's socket thread dies of the + // RouteNotFoundException and verbinden waits for ever; the core fails at once. + if (this.routeForPacket(socket.nodeId, socket.remoteIPAddress).type === 'unavailable') return 'unreachable'; this.updateTcpState(socketId, 'SYN_SENT'); this.resetTcpSendAttempts(socketId); const initialDelivery = this.transmitTcpSegment(socketId, TCP_FLAG.synchronize, new Uint8Array(), 0, 'TCP SYN'); - const waitedForTimeout = this.advanceTcpUntilSettled(socketId, { waitingStates: ['SYN_SENT'] }); + const waitedForTimeout = yield* this.tcpSettledSteps(socketId, { waitingStates: ['SYN_SENT'] }); if (state.socketsById.get(socketId)?.tcpState === 'ESTABLISHED') return 'connected'; if (initialDelivery === undefined || initialDelivery.type !== 'delivered') return 'unreachable'; return waitedForTimeout ? 'timedOut' : 'unreachable'; @@ -1850,8 +1991,19 @@ export class NetworkRuntimeEngine { /** Sends in segments of at most TCP_MAXIMUM_SEGMENT_SIZE bytes, each acknowledged before the next. */ sendTcp(socketId: Id, payload: Uint8Array): boolean { + return this.runBlocking(this.sendTcpSteps(socketId, payload)) === 'sent'; + } + + /** + * sendTcp in a program's thread (TCPSocket.senden): `sent` when every + * segment was acknowledged; `notConnected` when the socket was not + * connected at the call, `connectionLost` when it stopped being so + * meanwhile, `timedOut` when a segment's last attempt went unanswered + * (Java's messages sw_tcpsocket_msg6, msg7, msg8). + */ + *sendTcpSteps(socketId: Id, payload: Uint8Array): Steps { const state = this.stateValue; - if (state.socketsById.get(socketId)?.tcpState !== 'ESTABLISHED') return false; + if (state.socketsById.get(socketId)?.tcpState !== 'ESTABLISHED') return 'notConnected'; const chunks: Uint8Array[] = []; if (payload.length === 0) { chunks.push(new Uint8Array()); @@ -1861,15 +2013,17 @@ export class NetworkRuntimeEngine { } } for (const [index, chunk] of chunks.entries()) { - if (state.socketsById.get(socketId)?.tcpState !== 'ESTABLISHED') return false; + if (state.socketsById.get(socketId)?.tcpState !== 'ESTABLISHED') return 'connectionLost'; this.resetTcpSendAttempts(socketId); const flags = index === chunks.length - 1 ? TCP_FLAG.push : 0; this.transmitTcpSegment(socketId, flags, chunk, 0, `TCP data segment ${index + 1}/${chunks.length}`); - this.advanceTcpUntilSettled(socketId, { waitingForAcknowledgement: true }); + yield* this.tcpSettledSteps(socketId, { waitingForAcknowledgement: true }); const session = state.tcpSessionsById.get(socketId); - if (session === undefined || session.state !== 'ESTABLISHED' || session.lastSentSegment !== undefined) return false; + if (session === undefined || session.state !== 'ESTABLISHED' || session.lastSentSegment !== undefined) { + return session?.dataTimedOut === true ? 'timedOut' : 'connectionLost'; + } } - return true; + return 'sent'; } /** The oldest whole message (up to a PSH) received on the socket. */ @@ -1879,6 +2033,41 @@ export class NetworkRuntimeEngine { return session.receivedMessages.shift(); } + /** + * TCPSocket.empfangen in a program's thread: the next whole message, as + * soon as there is one; `closed` once the connection is no longer + * established (Java: null), `notConnected` when it was not at the call + * and nothing is left (Java: VerbindungsException). Better than Java: a + * message that came before the connection closed is still handed out + * (Java drops what its thread had not taken yet). + */ + *receiveTcpSteps(socketId: Id): Steps { + const messages = () => this.stateValue.tcpSessionsById.get(socketId)?.receivedMessages ?? []; + const established = () => this.stateValue.socketsById.get(socketId)?.tcpState === 'ESTABLISHED'; + if (messages().length === 0 && !established()) return { type: 'notConnected' }; + yield* waitUntil(() => messages().length > 0 || !established() || this.stateValue.phase !== 'running'); + const payload = messages().shift(); + return payload === undefined ? { type: 'closed' } : { type: 'message', payload }; + } + + /** + * ServerSocket.oeffnen in a program's thread: the listener's next + * connection, once established; undefined when the listener closes. + * Java looks every 100 ms whether its socket is connected; the core at once. + */ + *acceptTcpSteps(listenerId: Id): Steps { + const state = this.stateValue; + const next = () => + (state.tcpAcceptedSocketIdsByListenerId.get(listenerId) ?? []).find( + (id) => !this.handedOutTcpSocketIds.has(id) && state.socketsById.get(id)?.tcpState === 'ESTABLISHED', + ); + const listening = () => state.socketsById.get(listenerId)?.tcpState === 'LISTEN'; + yield* waitUntil(() => next() !== undefined || !listening() || state.phase !== 'running'); + const accepted = next(); + if (accepted !== undefined) this.handedOutTcpSocketIds.add(accepted); + return accepted; + } + closeTcpSocket(socketId: Id): void { const socket = this.stateValue.socketsById.get(socketId); if (socket === undefined || socket.protocol !== 'tcp' || socket.tcpState === undefined) return; @@ -2302,6 +2491,7 @@ export class NetworkRuntimeEngine { // ends senden with schliessen, an orderly close with FIN. A connection // attempt that fails closes at once. if (socket.tcpState === 'ESTABLISHED') { + session.dataTimedOut = true; this.closeTcpSocket(socketId); } else { this.updateTcpState(socketId, 'CLOSED'); @@ -2345,10 +2535,15 @@ export class NetworkRuntimeEngine { * Advances the clock to the socket's timeouts while it waits in one of * the states or for an acknowledgement; whether it advanced at all. */ - private advanceTcpUntilSettled( + /** + * Waits while the socket is in one of `waitingStates`, or while its data + * wait for their acknowledgement, as long as an attempt's time runs; true + * when it waited for one. + */ + private *tcpSettledSteps( socketId: Id, options: { waitingStates?: readonly TcpSocketState[]; waitingForAcknowledgement?: boolean }, - ): boolean { + ): Steps { const waiting = () => { const socket = this.stateValue.socketsById.get(socketId); if (socket === undefined || socket.tcpState === undefined) return false; @@ -2361,17 +2556,11 @@ export class NetworkRuntimeEngine { this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined; return waitingOnState || waitingOnAck; }; - let advancedToTimeout = false; - while (this.stateValue.phase === 'running' && waiting()) { - const deadlines = this.stateValue.pendingEvents - .filter((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId) - .map((event) => event.deadlineMilliseconds); - if (deadlines.length === 0) return advancedToTimeout; - advancedToTimeout = true; - // Until the answer comes, or the attempt's time is up. - this.advanceUntil(() => !waiting(), Math.min(...deadlines)); - } - return advancedToTimeout; + const attemptRuns = () => this.stateValue.pendingEvents.some((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId); + if (this.stateValue.phase !== 'running' || !waiting() || !attemptRuns()) return false; + // Until the answer comes, or the last attempt's time is up. + yield* waitUntil(() => this.stateValue.phase !== 'running' || !waiting() || !attemptRuns()); + return true; } private updateTcpState(socketId: Id, newState: TcpSocketState): void { diff --git a/packages/core/src/runtime/snapshot.ts b/packages/core/src/runtime/snapshot.ts index 699e40b..c52568a 100644 --- a/packages/core/src/runtime/snapshot.ts +++ b/packages/core/src/runtime/snapshot.ts @@ -12,14 +12,21 @@ import type { DhcpClientConfiguration, DhcpServerConfiguration, DnsServerConfiguration, + EchoServerConfiguration, FirewallConfiguration, GatewayPortForwardingRow, InterfaceConfiguration, InterfaceKey, ManualRoute, + SimpleClientConfiguration, SwitchConfiguration, } from './config.js'; +/** An installed echo server: its port, and whether it is switched on (Java: ServerBaustein.aktiv). */ +export interface EchoServerSnapshot extends EchoServerConfiguration { + readonly isActive: boolean; +} + export interface PortSnapshot { readonly id: Id; readonly label: string; @@ -65,6 +72,10 @@ export interface TopologySnapshot { readonly switchConfigurationsByNodeId: ReadonlyMap; /** Nodes whose IP forwarding differs from their kind's (forwardsIPv4ByDefault): a host that forwards. */ readonly ipForwardingByNodeId: ReadonlyMap; + /** The installed echo servers (ServerBaustein), by node. */ + readonly echoServersByNodeId: ReadonlyMap; + /** The installed generic clients (ClientBaustein) with the server they last talked to, by node. */ + readonly simpleClientsByNodeId: ReadonlyMap; } export interface TopologySnapshotInput { @@ -81,6 +92,8 @@ export interface TopologySnapshotInput { portForwardingRowsByNodeId?: ReadonlyMap; switchConfigurationsByNodeId?: ReadonlyMap; ipForwardingByNodeId?: ReadonlyMap; + echoServersByNodeId?: ReadonlyMap; + simpleClientsByNodeId?: ReadonlyMap; } export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot { @@ -98,6 +111,8 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot portForwardingRowsByNodeId: input.portForwardingRowsByNodeId ?? new Map(), switchConfigurationsByNodeId: input.switchConfigurationsByNodeId ?? new Map(), ipForwardingByNodeId: input.ipForwardingByNodeId ?? new Map(), + echoServersByNodeId: input.echoServersByNodeId ?? new Map(), + simpleClientsByNodeId: input.simpleClientsByNodeId ?? new Map(), }; } diff --git a/packages/core/src/runtime/threads.ts b/packages/core/src/runtime/threads.ts new file mode 100644 index 0000000..ba5d6e7 --- /dev/null +++ b/packages/core/src/runtime/threads.ts @@ -0,0 +1,103 @@ +// A program's threads (Java: each Anwendung is a Thread, a server one more +// per connection). The core writes a thread as a generator that yields what +// it waits for, where Java's thread blocks: a condition, a time, or both. +// After every event the engine resumes each thread whose wait is over, in +// the order the threads began; a thread never blocks the engine. +// +// Java's threads often look only now and then (TCPSocket.verbinden every +// 100 ms); a thread here wakes at once, as soon as its condition holds. + +/** What a thread waits for: until the condition holds, or until the time, whichever comes first. */ +export interface Wait { + readonly until?: () => boolean; + readonly deadlineMilliseconds?: number; +} + +export type Steps = Generator; + +/** A pause: the thread goes on at the time, after what else happens then. */ +export function pause(untilMilliseconds: number): Wait { + return { deadlineMilliseconds: untilMilliseconds }; +} + +interface Thread { + readonly steps: Steps; + wait: Wait | undefined; + /** Counts the thread's waits; a wake-up belongs to the wait it was set for. */ + waitNumber: number; + woken: boolean; +} + +export interface ThreadHost { + readonly nowMilliseconds: () => number; + /** A wake-up at the time, as an event after those already due then. */ + readonly scheduleWake: (deadlineMilliseconds: number, wake: () => void) => void; + /** Runs a step of a thread as within an event: sends queue, nothing blocks. */ + readonly asEvent: (step: () => void) => void; +} + +export class ApplicationThreads { + private threads: Thread[] = []; + private resuming = false; + + constructor(private readonly host: ThreadHost) {} + + /** Begins a thread: it runs at once up to its first wait. */ + start(steps: Steps): void { + const thread: Thread = { steps, wait: undefined, waitNumber: 0, woken: false }; + this.threads.push(thread); + this.step(thread); + this.resume(); + } + + /** Resumes each thread whose wait is over, until none is. */ + resume(): void { + if (this.resuming) return; + this.resuming = true; + try { + for (;;) { + const ready = this.threads.find((thread) => thread.woken || thread.wait?.until?.() === true); + if (ready === undefined) return; + this.step(ready); + } + } finally { + this.resuming = false; + } + } + + /** Ends every thread (the simulation stops). */ + clear(): void { + this.threads = []; + } + + get count(): number { + return this.threads.length; + } + + private step(thread: Thread): void { + this.host.asEvent(() => { + const next = thread.steps.next(); + if (next.done === true) { + this.threads = this.threads.filter((each) => each !== thread); + return; + } + const wait = next.value; + thread.wait = wait; + thread.woken = false; + thread.waitNumber += 1; + const waitNumber = thread.waitNumber; + if (wait.deadlineMilliseconds !== undefined) { + this.host.scheduleWake(Math.max(wait.deadlineMilliseconds, this.host.nowMilliseconds()), () => { + if (thread.waitNumber === waitNumber && this.threads.includes(thread)) thread.woken = true; + }); + } + }); + } +} + +/** Waits until the condition holds (not at all when it does), or until the time; whether it holds. */ +export function* waitUntil(condition: () => boolean, deadlineMilliseconds?: number): Steps { + if (condition()) return true; + yield deadlineMilliseconds === undefined ? { until: condition } : { until: condition, deadlineMilliseconds }; + return condition(); +} diff --git a/packages/core/test/apps.test.ts b/packages/core/test/apps.test.ts new file mode 100644 index 0000000..005eb0f --- /dev/null +++ b/packages/core/test/apps.test.ts @@ -0,0 +1,228 @@ +// Programs' threads (threads.ts) and the echo server and generic client +// (apps/echo.ts) where echoJava.test.ts's stories do not go: how threads +// wake, a taken port, a name, no route, lost data, a new start. + +import { describe, expect, it } from 'vitest'; +import { deviceConfiguration, pause, waitUntil, type NetworkRuntimeEngine, type Steps, type TopologySnapshotInput } from '../src/index.js'; +import { link, pc, startedEngine, uuid } from './fixtures.js'; + +const SERVER = uuid(0x2001); +const CLIENT = uuid(0x2002); + +/** 10.0.0.1 (echo server, DNS server with echo.filius.de) and 10.0.0.2 (client), on one cable. */ +function network(changes: Partial = {}): TopologySnapshotInput { + return { + nodes: [pc(SERVER, uuid(0x2011)), pc(CLIENT, uuid(0x2012))], + links: [link(SERVER, uuid(0x2011), CLIENT, uuid(0x2012))], + deviceConfigurations: new Map([ + [SERVER, deviceConfiguration('10.0.0.1', '255.255.255.0', '', '')], + [CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '10.0.0.1')], + ]), + echoServersByNodeId: new Map([[SERVER, { port: 55555, isActive: true }]]), + simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 55555 }]]), + ...changes, + }; +} + +const advance = (engine: NetworkRuntimeEngine, toMilliseconds: number) => engine.handle({ type: 'advance', toMilliseconds }); +const kinds = (lines: readonly { kind: string }[]) => lines.map((line) => line.kind); + +describe("programs' threads", () => { + it('wake when their condition holds, right after the event that made it so', () => { + const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); + let flag = false; + const woken: number[] = []; + engine.startThread( + (function* (): Steps { + yield* waitUntil(() => flag); + woken.push(engine.state.currentTimeMilliseconds); + })(), + ); + engine.handle({ type: 'schedule', deadlineMilliseconds: 300, kind: { kind: 'parityMarker', label: 'a' } }); + advance(engine, 200); + flag = true; + expect(woken).toEqual([]); + advance(engine, 1_000); + expect(woken).toEqual([300]); + }); + + it('go on after a pause, after the threads that are ready at that time', () => { + const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); + const order: string[] = []; + let ready = false; + engine.startThread( + (function* (): Steps { + yield pause(engine.state.currentTimeMilliseconds); + order.push(`paused ${engine.state.currentTimeMilliseconds}`); + })(), + ); + engine.startThread( + (function* (): Steps { + yield* waitUntil(() => ready); + order.push(`waited ${engine.state.currentTimeMilliseconds}`); + })(), + ); + ready = true; + engine.resumeThreads(); + advance(engine, 0); + expect(order).toEqual(['waited 0', 'paused 0']); + }); + + it('wake from a wait by its time only once, not again in a later wait', () => { + const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); + let flag = false; + const woken: string[] = []; + engine.startThread( + (function* (): Steps { + const first = yield* waitUntil(() => flag, 500); + woken.push(`first ${first} ${engine.state.currentTimeMilliseconds}`); + const second = yield* waitUntil(() => false, 2_000); + woken.push(`second ${second} ${engine.state.currentTimeMilliseconds}`); + })(), + ); + engine.handle({ type: 'schedule', deadlineMilliseconds: 100, kind: { kind: 'parityMarker', label: 'a' } }); + flag = true; + advance(engine, 3_000); + // The first wait ended at 100 by its condition; its wake-up at 500 does not end the second. + expect(woken).toEqual(['first true 100', 'second false 2000']); + }); + + it('resume in the order they began when the same event wakes them', () => { + const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); + let flag = false; + const order: string[] = []; + for (const name of ['first', 'second', 'third']) { + engine.startThread( + (function* (): Steps { + yield* waitUntil(() => flag); + order.push(name); + })(), + ); + } + flag = true; + engine.resumeThreads(); + expect(order).toEqual(['first', 'second', 'third']); + }); + + it('do not wait for what already holds: no other thread runs in between', () => { + const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); + let flag = false; + const order: string[] = []; + engine.startThread( + (function* (): Steps { + yield* waitUntil(() => flag); + order.push('woken'); + })(), + ); + engine.startThread( + (function* (): Steps { + flag = true; + yield* waitUntil(() => true); + order.push('went on'); + })(), + ); + expect(order).toEqual(['went on', 'woken']); + }); + + it('end with the simulation', () => { + const engine = startedEngine(network()); + expect(engine.echoServer(SERVER)).toBeDefined(); + engine.handle({ type: 'stop' }); + expect(engine.echoServer(SERVER)).toBeUndefined(); + expect(engine.simpleClient(CLIENT)).toBeUndefined(); + }); +}); + +describe('the echo server and the generic client', () => { + it('switches the server off when its port is taken (sw_serveranwendung_msg6)', () => { + const engine = startedEngine(network({ echoServersByNodeId: new Map([[SERVER, { port: 7, isActive: false }]]) })); + expect(engine.openTcpServerSocket(SERVER, 7)).toBeDefined(); + const server = engine.echoServer(SERVER)!; + server.setActive(true); + advance(engine, 100); + expect(kinds(server.log.all)).toEqual(['acceptingStarted', 'serverSocketFailed', 'acceptingStopped']); + expect(server.isActive).toBe(false); + }); + + it('connects to a name through the DNS server', () => { + const engine = startedEngine( + network({ + dnsServerConfigurationsByNodeId: new Map([[SERVER, { recursiveResolutionEnabled: false, records: [{ name: 'echo.filius.de', type: 'A', ttlSeconds: 3600, target: '10.0.0.1' }] }]]), + }), + ); + const client = engine.simpleClient(CLIENT)!; + client.connect('echo.filius.de', 55555); + advance(engine, 3_000); + client.send('Hallo'); + advance(engine, 6_000); + expect(kinds(client.log.all)).toEqual(['connected', 'sent', 'received']); + expect(engine.echoServer(SERVER)!.log.all[0]).toMatchObject({ kind: 'connectionAccepted', address: '10.0.0.2' }); + }); + + it('fails to connect to a name no server knows, without a frame (sw_socket_msg1)', () => { + const engine = startedEngine(network({ deviceConfigurations: new Map([[CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '')]]) })); + const client = engine.simpleClient(CLIENT)!; + client.connect('echo.filius.de', 55555); + advance(engine, 5_000); + expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'unresolved', timeMilliseconds: 0 }]); + expect(engine.capture.frames(uuid(0x2012))).toEqual([]); + }); + + it('fails at once to connect to an address without a route, without a frame (Java waits for ever)', () => { + const engine = startedEngine(network()); + const client = engine.simpleClient(CLIENT)!; + advance(engine, 100); + client.connect('10.9.9.9', 55555); + expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'failed', timeMilliseconds: 100 }]); + advance(engine, 5_000); + expect(engine.capture.frames(uuid(0x2012))).toEqual([]); + }); + + it('reports a send that no one acknowledges and the connection it ends (sw_tcpsocket_msg8)', () => { + const engine = startedEngine(network()); + const client = engine.simpleClient(CLIENT)!; + client.connect('10.0.0.1', 55555); + advance(engine, 3_000); + engine.setGlobalPacketLossEnabled(true); + client.send('verloren'); + advance(engine, 40_000); + expect(client.log.all.filter((line) => line.kind === 'sendFailed')).toMatchObject([{ kind: 'sendFailed', failure: 'timedOut' }]); + expect(kinds(client.log.all)).toContain('connectionClosed'); + expect(client.isConnected).toBe(false); + }); + + it('starts anew with a new simulation: from the snapshot, with empty windows', () => { + const engine = startedEngine(network()); + engine.echoServer(SERVER)!.setActive(false); + engine.simpleClient(CLIENT)!.send('Hallo'); + engine.handle({ type: 'stop' }); + engine.handle({ type: 'start', snapshot: engine.state.topologySnapshot, seed: 14, initialTimeMilliseconds: 0 }); + expect(engine.echoServer(SERVER)!.isActive).toBe(true); + expect(engine.echoServer(SERVER)!.log.all).toEqual([]); + expect(engine.simpleClient(CLIENT)!.log.all).toEqual([]); + }); + + it('hands out a message that came before the connection closed (Java drops it)', () => { + const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() })); + const listener = engine.openTcpServerSocket(SERVER, 80)!; + const client = engine.openTcpClientSocket(CLIENT, '10.0.0.1', 80)!; + expect(engine.connectTcpWithResult(client)).toBe('connected'); + const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listener)![0]!; + advance(engine, engine.state.currentTimeMilliseconds + 100); + expect(engine.sendTcp(accepted, new TextEncoder().encode('zuletzt'))).toBe(true); + engine.closeTcpSocket(accepted); + advance(engine, engine.state.currentTimeMilliseconds + 1_000); + expect(engine.state.socketsById.get(client)!.tcpState).not.toBe('ESTABLISHED'); + const received: string[] = []; + engine.startThread( + (function* (): Steps { + for (;;) { + const next = yield* engine.receiveTcpSteps(client); + received.push(next.type === 'message' ? new TextDecoder().decode(next.payload) : next.type); + if (next.type !== 'message') return; + } + })(), + ); + expect(received).toEqual(['zuletzt', 'notConnected']); + }); +}); diff --git a/packages/core/test/echoJava.test.ts b/packages/core/test/echoJava.test.ts new file mode 100644 index 0000000..030cda2 --- /dev/null +++ b/packages/core/test/echoJava.test.ts @@ -0,0 +1,337 @@ +// The echo server and the generic client as Java FILIUS runs them. +// tools/java-oracle's EchoRun played stories of the two programs on tcp-lan's +// network in Java FILIUS and wrote every frame each card saw, Java's tables +// of them, and every line each program wrote into its window +// (echo-java-.java.json). The engine plays the same stories with its +// programs (apps/echo.ts): each card's frames must come in the same order, +// with the same content and rows, at about the same time; each program's +// lines must read as Java's (its German message bundle), in the same order, +// at about the same time. +// +// Java's client looks every 100 ms whether it is connected +// (TCPSocket.verbinden), its server likewise whether a connection came +// (ServerSocket.oeffnen) or it was switched off; the engine at once. Those +// lines come in Java up to POLL_MILLISECONDS later. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { withSnapshot, type EchoServerLogEntry, type LogLine, type NetworkRuntimeEngine, type SendFailure, type SimpleClientLogEntry } from '../src/index.js'; +import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables } from './javaRun.js'; +import { snapshotOf, type Scenario } from './scenario.js'; + +const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); + +const SERVER = '00000000-0000-0000-0000-000000000101'; +const CLIENT = '00000000-0000-0000-0000-000000000102'; +const OTHER_CLIENT = '00000000-0000-0000-0000-000000000103'; +const POLL_MILLISECONDS = 100; + +type Program = 'server' | 'client' | 'other'; + +interface JavaRun extends JavaTables { + frames: Record; + /** Time, program, and a line of its window or one of the story's calls ("call …", "returned …"). */ + events: [number, Program, string][]; +} + +/** sw_tcpsocket_msg6, msg7, msg8. */ +const SEND_FAILURES: Record = { + notConnected: 'Verbindung nicht aufgebaut!', + connectionLost: 'Verbindung wurde unterbrochen', + timedOut: 'Zeitüberschreitung während des Versands einer Nachricht', +}; + +/** A server's line as Java's window shows it (German message bundle). */ +function serverText(entry: EchoServerLogEntry): string { + switch (entry.kind) { + case 'acceptingStarted': + return 'Annahme von Verbindungsanfragen gestartet'; + case 'acceptingStopped': + return 'Annahme von Verbindungsanfragen beendet'; + case 'serverSocketClosed': + return 'Server-Socket geschlossen'; + case 'serverSocketFailed': + return 'Server-Socket konnte nicht geöffnet werden.'; + case 'connectionAccepted': + return `Verbindung zu ${entry.address}:${entry.port} hergestellt`; + case 'received': + return `>>${entry.text}`; + case 'sent': + return `<<${entry.text}`; + case 'sendFailed': + return SEND_FAILURES[entry.failure]; + case 'connectionClosed': + return `Socket zu ${entry.address}:${entry.port} geschlossen`; + } +} + +const CONNECT_FAILURES = { + unresolved: 'Ziel konnte nicht bestimmt werden!', + timedOut: 'Zeitüberschreitung beim Verbindungsaufbau', + failed: 'Aktives Öffnen des Sockets fehlgeschlagen', +}; + +/** A client's line as Java's window shows it. */ +function clientText(entry: SimpleClientLogEntry): string { + switch (entry.kind) { + case 'connected': + return 'Verbindung hergestellt'; + case 'connectFailed': + return `Fehler beim Verbindungsaufbau\n${CONNECT_FAILURES[entry.failure]}`; + case 'disconnected': + return 'Verbindung getrennt'; + case 'notConnected': + return 'Fehler beim Verbindungsaufbau'; + case 'sent': + return `<<${entry.text}`; + case 'received': + return `>>${entry.text}`; + case 'sendFailed': + return SEND_FAILURES[entry.failure]; + case 'connectionClosed': + return `Socket zu ${entry.address}:${entry.port} getrennt`; + case 'receiveFailed': + return 'Keine Verbindung hergestellt!'; + } +} + +/** Lines Java writes after one of its 100 ms looks. */ +const POLLED = new Set(['Verbindung hergestellt', 'Server-Socket geschlossen']); +const isPolled = (text: string) => POLLED.has(text) || /^Verbindung zu .* hergestellt$/.test(text); + +type Act = (engine: NetworkRuntimeEngine) => void; +const server = (engine: NetworkRuntimeEngine) => engine.echoServer(SERVER)!; +const client = (engine: NetworkRuntimeEngine) => engine.simpleClient(CLIENT)!; +const other = (engine: NetworkRuntimeEngine) => engine.simpleClient(OTHER_CLIENT)!; + +/** Each story's calls at their times, as EchoRun makes them. */ +const STORIES: Record = { + exchange: [ + [500, (engine) => client(engine).connect('192.168.0.10', 55555)], + [2000, (engine) => client(engine).send('Hallo')], + [4000, (engine) => client(engine).send('Welt')], + [6000, (engine) => client(engine).disconnect()], + ], + closedPort: [[500, (engine) => client(engine).connect('192.168.0.30', 55555)]], + noRoute: [[500, (engine) => client(engine).connect('10.0.0.1', 55555)]], + notConnected: [[500, (engine) => client(engine).send('Hallo')]], + serverOff: [ + [500, (engine) => client(engine).connect('192.168.0.10', 55555)], + [2000, (engine) => server(engine).setActive(false)], + [4000, (engine) => client(engine).send('Hallo')], + [6000, (engine) => server(engine).setActive(true)], + [7000, (engine) => client(engine).connect('192.168.0.10', 55555)], + [9000, (engine) => client(engine).send('Welt')], + ], + reconnect: [ + [500, (engine) => client(engine).connect('192.168.0.10', 55555)], + [2000, (engine) => client(engine).disconnect()], + [4000, (engine) => client(engine).connect('192.168.0.10', 55555)], + [6000, (engine) => client(engine).send('Hallo')], + ], + twoClients: [ + [500, (engine) => client(engine).connect('192.168.0.10', 55555)], + [1000, (engine) => other(engine).connect('192.168.0.10', 55555)], + [2000, (engine) => client(engine).send('eins')], + [3000, (engine) => other(engine).send('zwei')], + [4000, (engine) => client(engine).disconnect()], + [5000, (engine) => other(engine).disconnect()], + ], + otherPort: [ + [500, (engine) => server(engine).setActive(false)], + [ + 1500, + (engine) => { + server(engine).setPort(7); + server(engine).setActive(true); + }, + ], + [2000, (engine) => client(engine).connect('192.168.0.10', 7)], + [4000, (engine) => client(engine).send('Hallo')], + [6000, (engine) => other(engine).connect('192.168.0.10', 55555)], + ], +}; + +/** The windows' lines, time and text, per program. */ +type Lines = Record; + +function javaLines(java: JavaRun): Lines { + const lines: Lines = { server: [], client: [], other: [] }; + for (const [time, program, text] of java.events) { + // Anwendung.starten tells its observers nothing (null), which the window skips. + if (text === 'null' || text.startsWith('call ') || text.startsWith('returned ')) continue; + lines[program].push([time, withClientPortText(text)]); + } + return lines; +} + +function engineLines(engine: NetworkRuntimeEngine): Lines { + const lines = (log: readonly LogLine[], text: (entry: E) => string): [number, string][] => + log.map((line) => [line.timeMilliseconds, withClientPortText(text(line))]); + return { + server: lines(server(engine).log.all, serverText), + client: lines(client(engine).log.all, clientText), + other: lines(other(engine).log.all, clientText), + }; +} + +/** Java's timeout of a TCP attempt (3 round trips at 90 %), and the round trip of a frame between two cards on the switch. */ +const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750; +const CARD_TO_CARD_MILLISECONDS = 111; + +/** NumberFormat for Locale.GERMANY. */ +const javaNumber = (value: number) => String(value).replace(/\B(?=(\d{3})+(?!\d))/g, '.'); + +/** Java's rows of a TCP segment without data (Lauscher.datenVorbereiten), with addresses or ports only. */ +function tcpRows(number: number, frame: JavaFrame, withAddresses: boolean): (string | null)[][] { + const end = (address: string, port: number) => (withAddresses ? `${address}:${port}` : String(port)); + return [ + [String(number), frame.source, frame.destination, '', 'Netzzugang', '0x800'], + [String(number), frame.sourceIp!, frame.destinationIp!, 'IP', 'Vermittlung', 'Protokoll: 6, TTL: 64'], + [ + String(number), + end(frame.sourceIp!, frame.sourcePort!), + end(frame.destinationIp!, frame.destinationPort!), + 'TCP', + 'Transport', + `SEQ: ${javaNumber(frame.sequence!)}, ACK: ${javaNumber(frame.acknowledgement!)}`, + ], + ]; +} + +/** Java's run with a frame more on a card, in time order, its rows numbered in. */ +function withJavaFrame(java: JavaRun, card: string, frame: JavaFrame): JavaRun { + const frames = java.frames[card]!; + const index = frames.filter((each) => each.time <= frame.time).length; + const renumbered = (rows: (string | null)[][], withAddresses: boolean) => [ + ...rows.map((row) => (Number(row[0]) > index ? [String(Number(row[0]) + 1), ...row.slice(1)] : row)), + ...tcpRows(index + 1, frame, withAddresses), + ]; + return { + ...java, + frames: { ...java.frames, [card]: [...frames.slice(0, index), frame, ...frames.slice(index)] }, + rows: { ...java.rows, [card]: renumbered(java.rows![card]!, true) }, + layeredRows: { ...java.layeredRows, [card]: renumbered(java.layeredRows![card]!, false) }, + }; +} + +/** When the program last called verbinden: its SYN goes down then, and the attempt's time runs from there. */ +const lastConnect = (java: JavaRun, program: Program) => java.events.filter(([, who, text]) => who === program && text.startsWith('call verbinden')).at(-1)![0]; + +/** Java's connection attempt with one SYN: its failure two attempts later, when the third has timed out. */ +function failingAfterThreeAttempts(java: JavaRun, program: Program): JavaRun { + return { + ...java, + events: java.events.map(([time, who, text]) => + who === program && text.startsWith('Fehler beim Verbindungsaufbau\n') ? [time + 2 * JAVA_TIMEOUT_MILLISECONDS, who, text] : [time, who, text], + ), + }; +} + +/** + * Where Java errs, per story: its run corrected, as Java means it. + * + * noRoute: the client's socket finds no route for its SYN; the exception + * ends the socket's thread, and verbinden waits for ever for a state that + * never comes — the client hangs without a word (and is no longer usable). + * The engine's connection fails at once (sw_tcpsocket_msg3). + * + * closedPort, otherPort: Java's client sends one SYN, not three + * (tcpJava.test.ts, closedPort); its connection fails two attempts later in + * the engine. + * + * serverOff: switched off, Java's server frees its port (ServerSocket + * .schliessen) while its connections still close; the client's FIN finds + * no socket and goes unanswered, the client waits in LAST_ACK in vain. The + * engine's server answers it with its ACK. + */ +const JAVA_BUGS: Record JavaRun> = { + noRoute: (java) => ({ + ...java, + events: [...java.events, [java.events.find(([, , text]) => text.startsWith('call verbinden'))![0], 'client', `Fehler beim Verbindungsaufbau\n${CONNECT_FAILURES.failed}`]], + }), + closedPort: (java) => failingAfterThreeAttempts(java, 'client'), + otherPort: (java) => failingAfterThreeAttempts(java, 'other'), + serverOff: (java) => { + const fin = java.frames[SERVER]!.filter((frame) => frame.tcpFlags === 'F').at(-1)!; + const ack: JavaFrame = { + ...fin, + source: fin.destination, + destination: fin.source, + sourceIp: fin.destinationIp!, + destinationIp: fin.sourceIp!, + sourcePort: fin.destinationPort!, + destinationPort: fin.sourcePort!, + tcpFlags: 'A', + sequence: java.frames[SERVER]!.find((frame) => frame.tcpFlags === 'F')!.sequence! + 1, + acknowledgement: fin.sequence! + 1, + }; + return withJavaFrame(withJavaFrame(java, SERVER, ack), CLIENT, { ...ack, time: fin.time + CARD_TO_CARD_MILLISECONDS }); + }, +}; + +/** + * Where each card's capture is compared only up to a time (Java's). + * closedPort, otherPort: the engine's second and third SYN come when the + * switch has forgotten the server (tcp-lan: 5 s) and floods them to every + * card; compared up to the second. + */ +const CAPTURE_UNTIL: Record number> = { + closedPort: (java) => lastConnect(java, 'client') + JAVA_TIMEOUT_MILLISECONDS - GAP_TOLERANCE_MILLISECONDS, + otherPort: (java) => lastConnect(java, 'other') + JAVA_TIMEOUT_MILLISECONDS - GAP_TOLERANCE_MILLISECONDS, +}; + +describe('the echo server and the generic client as Java FILIUS runs them', () => { + it('cuts only where the engine goes on beyond Java', () => { + for (const [name, until] of Object.entries(CAPTURE_UNTIL)) { + const java = JSON.parse(readFileSync(join(scenarioDirectory, `echo-java-${name}.java.json`), 'utf8')) as JavaRun; + const cut = until(java); + expect(Object.values(java.frames).flat().every((frame) => frame.time < cut), name).toBe(true); + } + }); + + it('corrects Java only where it errs', () => { + for (const [name, correct] of Object.entries(JAVA_BUGS)) { + const java = JSON.parse(readFileSync(join(scenarioDirectory, `echo-java-${name}.java.json`), 'utf8')) as JavaRun; + expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java)); + } + }); + + const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'tcp-lan.scenario.json'), 'utf8')) as Scenario; + const snapshot = withSnapshot(snapshotOf(scenario), { + echoServersByNodeId: new Map([[SERVER, { port: 55555, isActive: true }]]), + simpleClientsByNodeId: new Map([ + [CLIENT, { targetAddress: '', targetPort: 55555 }], + [OTHER_CLIENT, { targetAddress: '', targetPort: 55555 }], + ]), + }); + + for (const [name, acts] of Object.entries(STORIES)) { + it(name, () => { + const measured = JSON.parse(readFileSync(join(scenarioDirectory, `echo-java-${name}.java.json`), 'utf8')) as JavaRun; + const java = JAVA_BUGS[name]?.(measured) ?? measured; + const { engine } = watchedEngine(scenario, snapshot); + for (const [time, act] of acts) { + engine.handle({ type: 'advance', toMilliseconds: time }); + act(engine); + } + engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000 }); + + expectSameCaptures(engine, scenario, java.frames, { text: withClientPortText, until: CAPTURE_UNTIL[name]?.(measured), tables: java }); + + const expected = javaLines(java); + const actual = engineLines(engine); + for (const program of ['server', 'client', 'other'] as const) { + expect(actual[program].map(([, text]) => text), `${program}'s lines`).toEqual(expected[program].map(([, text]) => text)); + for (const [index, [javaTime, text]] of expected[program].entries()) { + const late = javaTime - actual[program][index]![0]; + const [least, most] = isPolled(text) ? [-GAP_TOLERANCE_MILLISECONDS, POLL_MILLISECONDS + GAP_TOLERANCE_MILLISECONDS] : [-GAP_TOLERANCE_MILLISECONDS, GAP_TOLERANCE_MILLISECONDS]; + expect(late, `${program}'s line ${JSON.stringify(text)}: Java ${late} ms later`).toBeGreaterThanOrEqual(least); + expect(late, `${program}'s line ${JSON.stringify(text)}: Java ${late} ms later`).toBeLessThanOrEqual(most); + } + } + }); + } +}); diff --git a/packages/core/test/javaRun.ts b/packages/core/test/javaRun.ts index 6124c12..e0c9248 100644 --- a/packages/core/test/javaRun.ts +++ b/packages/core/test/javaRun.ts @@ -30,6 +30,7 @@ import { type CapturedFrame, type CaptureLayer, type EthernetFrame, + type TopologySnapshot, } from '../src/index.js'; import { snapshotOf, type Scenario } from './scenario.js'; @@ -117,8 +118,11 @@ export function engineText(frame: EthernetFrame): string { return `${macs} UDP ${packet.senderIPAddress}:${datagram.sourcePort} > ${packet.receiverIPAddress}:${datagram.destinationPort} ${new TextDecoder().decode(datagram.payload)}`; } -/** A started engine on the scenario's network whose frames can be read afterwards as FILIUS logs them. */ -export function watchedEngine(scenario: Scenario) { +/** + * A started engine on the scenario's network (or on `snapshot`, made from + * it) whose frames can be read afterwards as FILIUS logs them. + */ +export function watchedEngine(scenario: Scenario, snapshot: TopologySnapshot = snapshotOf(scenario)) { const engine = new NetworkRuntimeEngine(scenario.seed); const framesByIdentity = new Map(); const send = engine.sendEthernetFrame.bind(engine); @@ -126,7 +130,7 @@ export function watchedEngine(scenario: Scenario) { framesByIdentity.set(frame.identity, frame); send(fromNodeId, outgoingPortId, frame); }; - engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 }); + engine.handle({ type: 'start', snapshot, seed: scenario.seed, initialTimeMilliseconds: 0 }); /** Every frame when it first shows on a host's card (sent, or received). */ const frames = (): SeenFrame[] => { diff --git a/packages/core/test/scenarios/echo-java-closedPort.java.json b/packages/core/test/scenarios/echo-java-closedPort.java.json new file mode 100644 index 0000000..18e4e50 --- /dev/null +++ b/packages/core/test/scenarios/echo-java-closedPort.java.json @@ -0,0 +1,377 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 641, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.30" + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 519, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.30" + }, + { + "time": 755, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.30", + "targetIp": "192.168.0.20" + }, + { + "time": 758, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.30", + "protocol": 6, + "sourcePort": 58845, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "S", + "sequence": 2000000, + "acknowledgement": 0 + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 641, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.30" + }, + { + "time": 643, + "source": "02:00:00:00:01:13", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.30", + "targetIp": "192.168.0.20" + }, + { + "time": 871, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:13", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.30", + "protocol": 6, + "sourcePort": 58845, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "S", + "sequence": 2000000, + "acknowledgement": 0 + } + ] + }, + "rows": { + "00000000-0000-0000-0000-000000000101": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.30", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]" + ] + ], + "00000000-0000-0000-0000-000000000102": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.30", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]" + ], + [ + "2", + "02:00:00:00:01:13", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.30", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.30", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "192.168.0.20:58845", + "192.168.0.30:55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ] + ], + "00000000-0000-0000-0000-000000000103": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.30", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]" + ], + [ + "2", + "02:00:00:00:01:13", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.30", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.30", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "192.168.0.20:58845", + "192.168.0.30:55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ] + ] + }, + "layeredRows": { + "00000000-0000-0000-0000-000000000101": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.30", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]" + ] + ], + "00000000-0000-0000-0000-000000000102": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.30", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]" + ], + [ + "2", + "02:00:00:00:01:13", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.30", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.30", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "58845", + "55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ] + ], + "00000000-0000-0000-0000-000000000103": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.30", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.30 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.30]" + ], + [ + "2", + "02:00:00:00:01:13", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.30", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:13 [op=REPLY, sender=02:00:00:00:01:13|192.168.0.30, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.30", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "58845", + "55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ] + ] + }, + "events": [ + [ + 211, + "server", + "null" + ], + [ + 212, + "client", + "null" + ], + [ + 213, + "other", + "null" + ], + [ + 500, + "client", + "call verbinden 192.168.0.30:55555" + ], + [ + 8810, + "client", + "Fehler beim Verbindungsaufbau\nZeitüberschreitung beim Verbindungsaufbau" + ] + ] +} diff --git a/packages/core/test/scenarios/echo-java-exchange.java.json b/packages/core/test/scenarios/echo-java-exchange.java.json new file mode 100644 index 0000000..7cdaa7e --- /dev/null +++ b/packages/core/test/scenarios/echo-java-exchange.java.json @@ -0,0 +1,2326 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000101": [ + { + "time": 638, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 639, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 866, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "S", + "sequence": 2000000, + "acknowledgement": 0 + }, + { + "time": 868, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "SA", + "sequence": 1000000, + "acknowledgement": 2000001 + }, + { + "time": 1091, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000001, + "acknowledgement": 1000001 + }, + { + "time": 2112, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": "Hallo", + "tcpFlags": "P", + "sequence": 2000001, + "acknowledgement": 0 + }, + { + "time": 2112, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "A", + "sequence": 1000001, + "acknowledgement": 2000006 + }, + { + "time": 2168, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": "Hallo", + "tcpFlags": "P", + "sequence": 1000001, + "acknowledgement": 0 + }, + { + "time": 2391, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1000006 + }, + { + "time": 4111, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": "Welt", + "tcpFlags": "P", + "sequence": 2000006, + "acknowledgement": 0 + }, + { + "time": 4112, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "A", + "sequence": 1000006, + "acknowledgement": 2000010 + }, + { + "time": 4167, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": "Welt", + "tcpFlags": "P", + "sequence": 1000006, + "acknowledgement": 0 + }, + { + "time": 4389, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000010, + "acknowledgement": 1000010 + }, + { + "time": 6112, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "F", + "sequence": 2000010, + "acknowledgement": 0 + }, + { + "time": 6112, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "A", + "sequence": 1000010, + "acknowledgement": 2000011 + }, + { + "time": 6168, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "F", + "sequence": 1000010, + "acknowledgement": 0 + }, + { + "time": 6390, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000011, + "acknowledgement": 1000011 + } + ], + "00000000-0000-0000-0000-000000000102": [ + { + "time": 517, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + }, + { + "time": 751, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "arp": 2, + "senderIp": "192.168.0.10", + "targetIp": "192.168.0.20" + }, + { + "time": 753, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "S", + "sequence": 2000000, + "acknowledgement": 0 + }, + { + "time": 979, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "SA", + "sequence": 1000000, + "acknowledgement": 2000001 + }, + { + "time": 980, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000001, + "acknowledgement": 1000001 + }, + { + "time": 2000, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": "Hallo", + "tcpFlags": "P", + "sequence": 2000001, + "acknowledgement": 0 + }, + { + "time": 2224, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "A", + "sequence": 1000001, + "acknowledgement": 2000006 + }, + { + "time": 2279, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": "Hallo", + "tcpFlags": "P", + "sequence": 1000001, + "acknowledgement": 0 + }, + { + "time": 2280, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000006, + "acknowledgement": 1000006 + }, + { + "time": 4000, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": "Welt", + "tcpFlags": "P", + "sequence": 2000006, + "acknowledgement": 0 + }, + { + "time": 4223, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "A", + "sequence": 1000006, + "acknowledgement": 2000010 + }, + { + "time": 4278, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": "Welt", + "tcpFlags": "P", + "sequence": 1000006, + "acknowledgement": 0 + }, + { + "time": 4279, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000010, + "acknowledgement": 1000010 + }, + { + "time": 6001, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "F", + "sequence": 2000010, + "acknowledgement": 0 + }, + { + "time": 6223, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "A", + "sequence": 1000010, + "acknowledgement": 2000011 + }, + { + "time": 6279, + "source": "02:00:00:00:01:11", + "destination": "02:00:00:00:01:12", + "sourceIp": "192.168.0.10", + "destinationIp": "192.168.0.20", + "protocol": 6, + "sourcePort": 55555, + "destinationPort": 51463, + "payload": null, + "tcpFlags": "F", + "sequence": 1000010, + "acknowledgement": 0 + }, + { + "time": 6279, + "source": "02:00:00:00:01:12", + "destination": "02:00:00:00:01:11", + "sourceIp": "192.168.0.20", + "destinationIp": "192.168.0.10", + "protocol": 6, + "sourcePort": 51463, + "destinationPort": 55555, + "payload": null, + "tcpFlags": "A", + "sequence": 2000011, + "acknowledgement": 1000011 + } + ], + "00000000-0000-0000-0000-000000000103": [ + { + "time": 638, + "source": "02:00:00:00:01:12", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "192.168.0.20", + "targetIp": "192.168.0.10" + } + ] + }, + "rows": { + "00000000-0000-0000-0000-000000000101": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.10", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]" + ], + [ + "2", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.10", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:11 [op=REPLY, sender=02:00:00:00:01:11|192.168.0.10, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ], + [ + "4", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "4", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SYN, SEQ: 1.000.000, ACK: 2.000.001" + ], + [ + "5", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "5", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.001, ACK: 1.000.001" + ], + [ + "6", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "6", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.001" + ], + [ + "6", + "192.168.0.20:51463", + "192.168.0.10:55555", + "", + "Anwendung", + "Hallo" + ], + [ + "7", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "7", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "7", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.001, ACK: 2.000.006" + ], + [ + "8", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "8", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "8", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.001" + ], + [ + "8", + "192.168.0.10:55555", + "192.168.0.20:51463", + "", + "Anwendung", + "Hallo" + ], + [ + "9", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "9", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "9", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.006, ACK: 1.000.006" + ], + [ + "10", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "10", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "10", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.006" + ], + [ + "10", + "192.168.0.20:51463", + "192.168.0.10:55555", + "", + "Anwendung", + "Welt" + ], + [ + "11", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "11", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "11", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.006, ACK: 2.000.010" + ], + [ + "12", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "12", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "12", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.006" + ], + [ + "12", + "192.168.0.10:55555", + "192.168.0.20:51463", + "", + "Anwendung", + "Welt" + ], + [ + "13", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "13", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "13", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.010, ACK: 1.000.010" + ], + [ + "14", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "14", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "14", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "FIN, SEQ: 2.000.010" + ], + [ + "15", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "15", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "15", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.010, ACK: 2.000.011" + ], + [ + "16", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "16", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "16", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "FIN, SEQ: 1.000.010" + ], + [ + "17", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "17", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "17", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.011, ACK: 1.000.011" + ] + ], + "00000000-0000-0000-0000-000000000102": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.10", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]" + ], + [ + "2", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.10", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:11 [op=REPLY, sender=02:00:00:00:01:11|192.168.0.10, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ], + [ + "4", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "4", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SYN, SEQ: 1.000.000, ACK: 2.000.001" + ], + [ + "5", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "5", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.001, ACK: 1.000.001" + ], + [ + "6", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "6", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.001" + ], + [ + "6", + "192.168.0.20:51463", + "192.168.0.10:55555", + "", + "Anwendung", + "Hallo" + ], + [ + "7", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "7", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "7", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.001, ACK: 2.000.006" + ], + [ + "8", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "8", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "8", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.001" + ], + [ + "8", + "192.168.0.10:55555", + "192.168.0.20:51463", + "", + "Anwendung", + "Hallo" + ], + [ + "9", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "9", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "9", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.006, ACK: 1.000.006" + ], + [ + "10", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "10", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "10", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.006" + ], + [ + "10", + "192.168.0.20:51463", + "192.168.0.10:55555", + "", + "Anwendung", + "Welt" + ], + [ + "11", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "11", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "11", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.006, ACK: 2.000.010" + ], + [ + "12", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "12", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "12", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.006" + ], + [ + "12", + "192.168.0.10:55555", + "192.168.0.20:51463", + "", + "Anwendung", + "Welt" + ], + [ + "13", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "13", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "13", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.010, ACK: 1.000.010" + ], + [ + "14", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "14", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "14", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "FIN, SEQ: 2.000.010" + ], + [ + "15", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "15", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "15", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "SEQ: 1.000.010, ACK: 2.000.011" + ], + [ + "16", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "16", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "16", + "192.168.0.10:55555", + "192.168.0.20:51463", + "TCP", + "Transport", + "FIN, SEQ: 1.000.010" + ], + [ + "17", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "17", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "17", + "192.168.0.20:51463", + "192.168.0.10:55555", + "TCP", + "Transport", + "SEQ: 2.000.011, ACK: 1.000.011" + ] + ], + "00000000-0000-0000-0000-000000000103": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.10", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]" + ] + ] + }, + "layeredRows": { + "00000000-0000-0000-0000-000000000101": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.10", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]" + ], + [ + "2", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.10", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:11 [op=REPLY, sender=02:00:00:00:01:11|192.168.0.10, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "51463", + "55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ], + [ + "4", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "4", + "55555", + "51463", + "TCP", + "Transport", + "SYN, SEQ: 1.000.000, ACK: 2.000.001" + ], + [ + "5", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "5", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.001, ACK: 1.000.001" + ], + [ + "6", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "6", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.001" + ], + [ + "6", + null, + null, + "", + "Anwendung", + "Hallo" + ], + [ + "7", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "7", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "7", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.001, ACK: 2.000.006" + ], + [ + "8", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "8", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "8", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.001" + ], + [ + "8", + null, + null, + "", + "Anwendung", + "Hallo" + ], + [ + "9", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "9", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "9", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.006, ACK: 1.000.006" + ], + [ + "10", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "10", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "10", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.006" + ], + [ + "10", + null, + null, + "", + "Anwendung", + "Welt" + ], + [ + "11", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "11", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "11", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.006, ACK: 2.000.010" + ], + [ + "12", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "12", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "12", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.006" + ], + [ + "12", + null, + null, + "", + "Anwendung", + "Welt" + ], + [ + "13", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "13", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "13", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.010, ACK: 1.000.010" + ], + [ + "14", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "14", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "14", + "51463", + "55555", + "TCP", + "Transport", + "FIN, SEQ: 2.000.010" + ], + [ + "15", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "15", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "15", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.010, ACK: 2.000.011" + ], + [ + "16", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "16", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "16", + "55555", + "51463", + "TCP", + "Transport", + "FIN, SEQ: 1.000.010" + ], + [ + "17", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "17", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "17", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.011, ACK: 1.000.011" + ] + ], + "00000000-0000-0000-0000-000000000102": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.10", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]" + ], + [ + "2", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "192.168.0.10", + "192.168.0.20", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:01:11 [op=REPLY, sender=02:00:00:00:01:11|192.168.0.10, target=02:00:00:00:01:12|192.168.0.20]" + ], + [ + "3", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "3", + "51463", + "55555", + "TCP", + "Transport", + "SYN, SEQ: 2.000.000" + ], + [ + "4", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "4", + "55555", + "51463", + "TCP", + "Transport", + "SYN, SEQ: 1.000.000, ACK: 2.000.001" + ], + [ + "5", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "5", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.001, ACK: 1.000.001" + ], + [ + "6", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "6", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.001" + ], + [ + "6", + null, + null, + "", + "Anwendung", + "Hallo" + ], + [ + "7", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "7", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "7", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.001, ACK: 2.000.006" + ], + [ + "8", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "8", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "8", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.001" + ], + [ + "8", + null, + null, + "", + "Anwendung", + "Hallo" + ], + [ + "9", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "9", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "9", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.006, ACK: 1.000.006" + ], + [ + "10", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "10", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "10", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.006" + ], + [ + "10", + null, + null, + "", + "Anwendung", + "Welt" + ], + [ + "11", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "11", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "11", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.006, ACK: 2.000.010" + ], + [ + "12", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "12", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "12", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.006" + ], + [ + "12", + null, + null, + "", + "Anwendung", + "Welt" + ], + [ + "13", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "13", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "13", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.010, ACK: 1.000.010" + ], + [ + "14", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "14", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "14", + "51463", + "55555", + "TCP", + "Transport", + "FIN, SEQ: 2.000.010" + ], + [ + "15", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "15", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "15", + "55555", + "51463", + "TCP", + "Transport", + "SEQ: 1.000.010, ACK: 2.000.011" + ], + [ + "16", + "02:00:00:00:01:11", + "02:00:00:00:01:12", + "", + "Netzzugang", + "0x800" + ], + [ + "16", + "192.168.0.10", + "192.168.0.20", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "16", + "55555", + "51463", + "TCP", + "Transport", + "FIN, SEQ: 1.000.010" + ], + [ + "17", + "02:00:00:00:01:12", + "02:00:00:00:01:11", + "", + "Netzzugang", + "0x800" + ], + [ + "17", + "192.168.0.20", + "192.168.0.10", + "IP", + "Vermittlung", + "Protokoll: 6, TTL: 64" + ], + [ + "17", + "51463", + "55555", + "TCP", + "Transport", + "SEQ: 2.000.011, ACK: 1.000.011" + ] + ], + "00000000-0000-0000-0000-000000000103": [ + [ + "1", + "02:00:00:00:01:12", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "192.168.0.20", + "192.168.0.10", + "ARP", + "Vermittlung", + "Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:01:12|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]" + ] + ] + }, + "events": [ + [ + 208, + "server", + "null" + ], + [ + 209, + "client", + "null" + ], + [ + 210, + "other", + "null" + ], + [ + 500, + "client", + "call verbinden 192.168.0.10:55555" + ], + [ + 1004, + "client", + "Verbindung hergestellt" + ], + [ + 1115, + "server", + "Verbindung zu 192.168.0.20:51463 hergestellt" + ], + [ + 2000, + "client", + "call senden Hallo" + ], + [ + 2112, + "server", + ">>Hallo" + ], + [ + 2224, + "client", + "<>Hallo" + ], + [ + 2391, + "server", + "<>Welt" + ], + [ + 4223, + "client", + "<>Welt" + ], + [ + 4390, + "server", + "<>Hallo" + ], + [ + 4223, + "client", + "<>Hallo" + ], + [ + 4390, + "server", + "<>Hallo" + ], + [ + 6224, + "client", + "<>Hallo" + ], + [ + 6391, + "server", + "<>Welt" + ], + [ + 9224, + "client", + "<>Welt" + ], + [ + 9391, + "server", + "<>eins" + ], + [ + 2223, + "client", + "<>eins" + ], + [ + 2390, + "server", + "<>zwei" + ], + [ + 3224, + "other", + "<>zwei" + ], + [ + 3391, + "server", + "<(); + for (const [nodeId, configuration] of project.echoServerConfigurations) { + echoServers.set(nodeId, { ...configuration, isActive: project.activePrograms.get(nodeId)?.has('echoServer') === true }); + } return topologySnapshot({ nodes: project.graph.nodes.map((node) => ({ id: node.id, @@ -32,5 +38,7 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot { portForwardingRowsByNodeId: project.portForwardingRowsByNodeId, switchConfigurationsByNodeId: project.switchConfigurations, ipForwardingByNodeId: project.ipForwardingByNodeId, + echoServersByNodeId: echoServers, + simpleClientsByNodeId: project.simpleClientConfigurations, }); } diff --git a/packages/fls/test/importProject.test.ts b/packages/fls/test/importProject.test.ts index 9fd5e60..596ef85 100644 --- a/packages/fls/test/importProject.test.ts +++ b/packages/fls/test/importProject.test.ts @@ -726,6 +726,29 @@ describe('importProject rules', () => { expect([...projectSnapshot(project).dnsServerConfigurationsByNodeId.keys()]).toEqual([on]); }); + it('runs every echo server, switched on or off, and every generic client (projectSnapshot)', () => { + const source = flsNode('Rechner').source; + const echoServer = (isActive: boolean, port: number): FlsApplication => ({ key: 'echo', javaClass: 'filius.software.clientserver.ServerBaustein', source, isActive, port }); + const client: FlsApplication = { key: 'client', javaClass: 'filius.software.clientserver.ClientBaustein', source, targetAddress: '10.0.0.1', targetPort: 7 }; + const project = importProject( + topology({ + nodes: [ + flsNode('Rechner', { interfaces: [nic('10.0.0.1')], system: { applications: [echoServer(true, 55555)] } }), + flsNode('Rechner', { interfaces: [nic('10.0.0.2')], system: { applications: [echoServer(false, 7), client] } }), + ], + }), + counter(), + ); + const [on, off] = project.graph.nodes.map((node) => node.id); + const snapshot = projectSnapshot(project); + + expect([...snapshot.echoServersByNodeId]).toEqual([ + [on, { port: 55555, isActive: true }], + [off, { port: 7, isActive: false }], + ]); + expect([...snapshot.simpleClientsByNodeId]).toEqual([[off, { targetAddress: '10.0.0.1', targetPort: 7 }]]); + }); + it("keeps a node's IP forwarding where it differs from its kind's: a host that forwards, a router that does not", () => { const system = (ipForwardingEnabled: boolean) => ({ manualRoutes: [], ipForwardingEnabled }); const project = importProject( diff --git a/scripts/code_language_allow.txt b/scripts/code_language_allow.txt index 292dc92..b61d104 100644 --- a/scripts/code_language_allow.txt +++ b/scripts/code_language_allow.txt @@ -65,3 +65,13 @@ packages/fls/::datei packages/fls/::hausnr packages/fls/::typ packages/fls/::inhalt +# Keys of Java FILIUS's message bundle for the window lines of its programs +# (sw_serveranwendung_msg1, sw_servermitarbeiter_msg1, sw_clientbaustein_msg1, +# sw_tcpsocket_msg8 …) and the method ClientBaustein.senden, cited where the +# core's structured lines and tests say which of Java's lines they stand for. +packages/core/src/runtime/apps/::serveranwendung +packages/core/src/runtime/apps/::servermitarbeiter +packages/core/src/runtime/apps/::clientbaustein +packages/core/src/runtime/apps/::tcpsocket +packages/core/src/runtime/apps/::senden +packages/core/test/echoJava.test.ts::tcpsocket diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 446524e..39f60c6 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -546,3 +546,37 @@ Gemessen (01.10.2026): in der Javas Threads senden; mit acht Clients stauen sich die Broadcasts, ein Frame erreicht die Karten bis zu rund 450 ms versetzt, und Javas Mitschnitt verliert eigene Frames eines Clients. + +## Echo-Server und einfacher Client (F3.1) + +`EchoRun` installiert auf dem Netz von `tcp-lan` Filius' Echo-Server +(`ServerBaustein`) auf .10, eingeschaltet auf Port 55555, bevor die +Simulation startet (wie eine Datei ihn lädt), und den einfachen Client +(`ClientBaustein`) auf .20 und .30. Es macht die Aufrufe wie die Fenster: +`verbinden` und `trennen` gehen an den Thread des Programms, `senden` läuft +im Thread des Aufrufers, bis es zurückkehrt. Es schreibt nach +`echo-java-.java.json` die Frames jeder Karte, Javas Tabellen und +jede Zeile, die ein Programm in sein Fenster schreibt (seine Beobachter), +dazu die Aufrufe. Abläufe: `exchange`, `closedPort`, `noRoute`, +`notConnected`, `serverOff`, `reconnect`, `twoClients`, `otherPort`. + +Gemessen (01.10.2026): + +* `Anwendung.starten` meldet seinen Beobachtern `null`; das Fenster + überspringt es. +* Der Client merkt die Verbindung erst beim nächsten Blick + (`TCPSocket.verbinden` schläft 100 ms), der Server ebenso eine neue + Verbindung (`ServerSocket.oeffnen`) und dass er ausgeschaltet wurde + („Server-Socket geschlossen“ rund 20 ms nach dem Ausschalten). +* Ohne Route zum Ziel stirbt der Socket-Thread des Clients an der + `RouteNotFoundException`; `verbinden` wartet ewig, das Fenster bleibt + stumm, kein Frame geht hinaus. +* Ausgeschaltet schließt der Server seine Verbindungen mit FIN, gibt aber + zugleich den Port frei (`ServerSocket.schliessen`); das FIN des Clients + findet keinen Socket mehr und bleibt ohne ACK. +* Aus- und sofort wieder eingeschaltet (ohne Pause) merkt Javas Schleife das + Ausschalten nicht und lauscht weiter auf dem alten Port, auch wenn er + geändert wurde. Wer im Fenster den Port wechselt, lässt Sekunden + dazwischen; so misst es `otherPort`. +* Der Client sendet nur ein SYN (wie bei `TcpRun`, `closedPort`). + diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index 850c0c5..c82492b 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" "$here/src/WlanRun.java" "$here/src/ExampleRun.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" "$here/src/ExampleRun.java" "$here/src/EchoRun.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. @@ -194,6 +194,18 @@ for story in pingWired pingWireless pingWithoutCable pingFromWithoutCable; do test -s "$output" done +# The echo server and the generic client as Java FILIUS runs them, on +# tcp-lan's network (EchoRun); frames and each program's window lines. +for run in exchange:12000 closedPort:20000 noRoute:8000 notConnected:3000 serverOff:14000 reconnect:12000 \ + twoClients:11000 otherPort:22000; do + story="${run%%:*}" + output="$root/packages/core/test/scenarios/echo-java-$story.java.json" + rm -f "$output" + HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" EchoRun \ + "$root/packages/core/test/scenarios/tcp-lan.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true + test -s "$output" +done + # FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS # does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts). for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \ diff --git a/tools/java-oracle/src/EchoRun.java b/tools/java-oracle/src/EchoRun.java new file mode 100644 index 0000000..e58e7c3 --- /dev/null +++ b/tools/java-oracle/src/EchoRun.java @@ -0,0 +1,188 @@ +// Runs a story of FILIUS' echo server (ServerBaustein) and generic client +// (ClientBaustein) on a core scenario's network in Java FILIUS +// (ScenarioNetwork) and records what really happens: every frame each card +// saw and every line each program wrote into its window (its observers). +// +// EchoRun +// +// The stories use the network of tcp-lan (server 192.168.0.10, clients +// 192.168.0.20 and .30). The server program runs on .10, switched on +// (aktiv) and on its port 55555 before the simulation starts, as a file +// loads it; each client program on .20 and .30. The calls come as the +// windows make them: verbinden and trennen hand over to the program's +// thread, senden runs in the caller's thread (the window's) until it returns. +// exchange .20 connects, sends "Hallo" and "Welt", disconnects +// closedPort .20 connects to .30, where no server runs +// noRoute .20 connects to 10.0.0.1, outside its network, no gateway +// notConnected .20 sends without a connection +// serverOff connected, the server is switched off, .20 sends; the +// server is switched on again, .20 connects and sends +// reconnect .20 connects, disconnects, connects again and sends +// twoClients .20 and .30 connected at once, each sends, each disconnects +// otherPort the server switched off, a second later over to port 7 and +// on (as its window does it); .20 connects to 7 and sends, .30 +// connects to 55555 (no longer served) + +import filius.software.clientserver.ClientBaustein; +import filius.software.clientserver.ServerBaustein; + +import java.util.ArrayList; +import java.util.List; + +public class EchoRun { + static final String SERVER = "00000000-0000-0000-0000-000000000101"; + static final String CLIENT = "00000000-0000-0000-0000-000000000102"; + static final String OTHER_CLIENT = "00000000-0000-0000-0000-000000000103"; + static final String SERVER_CLASS = "filius.software.clientserver.ServerBaustein"; + static final String CLIENT_CLASS = "filius.software.clientserver.ClientBaustein"; + + static ScenarioNetwork network; + static final List events = new ArrayList<>(); + + static synchronized void event(String who, String what) { + events.add(new Object[] { System.currentTimeMillis() - network.start, who, what }); + } + + static void sleepUntil(long milliseconds) throws InterruptedException { + long rest = network.start + milliseconds - System.currentTimeMillis(); + if (rest > 0) Thread.sleep(rest); + } + + static ClientBaustein client(String nodeId) { + return (ClientBaustein) network.system(nodeId).holeSoftware(CLIENT_CLASS); + } + + static void connect(String who, ClientBaustein client, String address, int port) { + event(who, "call verbinden " + address + ":" + port); + client.setZielIPAdresse(address); + client.setZielPort(port); + client.verbinden(); + } + + static void send(String who, ClientBaustein client, String text) { + event(who, "call senden " + text); + client.senden(text); + event(who, "returned senden"); + } + + static void disconnect(String who, ClientBaustein client) { + event(who, "call trennen"); + client.trennen(); + } + + public static void main(String[] args) throws Exception { + network = ScenarioNetwork.read(args[0]); + String story = args[1]; + long end = Long.parseLong(args[2]); + network.system(SERVER).installApp(SERVER_CLASS, false); + network.system(CLIENT).installApp(CLIENT_CLASS, false); + network.system(OTHER_CLIENT).installApp(CLIENT_CLASS, false); + ServerBaustein server = (ServerBaustein) network.system(SERVER).holeSoftware(SERVER_CLASS); + server.setPort(55555); + server.setAktiv(true); + // The windows: opened after loading, so they see what follows. + server.hinzuBeobachter((observable, data) -> event("server", String.valueOf(data))); + client(CLIENT).hinzuBeobachter((observable, data) -> event("client", String.valueOf(data))); + client(OTHER_CLIENT).hinzuBeobachter((observable, data) -> event("other", String.valueOf(data))); + network.start(); + ClientBaustein client = client(CLIENT); + ClientBaustein other = client(OTHER_CLIENT); + + switch (story) { + case "exchange": + sleepUntil(500); + connect("client", client, "192.168.0.10", 55555); + sleepUntil(2000); + send("client", client, "Hallo"); + sleepUntil(4000); + send("client", client, "Welt"); + sleepUntil(6000); + disconnect("client", client); + break; + case "closedPort": + sleepUntil(500); + connect("client", client, "192.168.0.30", 55555); + break; + case "noRoute": + sleepUntil(500); + connect("client", client, "10.0.0.1", 55555); + break; + case "notConnected": + sleepUntil(500); + send("client", client, "Hallo"); + break; + case "serverOff": + sleepUntil(500); + connect("client", client, "192.168.0.10", 55555); + sleepUntil(2000); + event("server", "call setAktiv false"); + server.setAktiv(false); + sleepUntil(4000); + send("client", client, "Hallo"); + sleepUntil(6000); + event("server", "call setAktiv true"); + server.setAktiv(true); + sleepUntil(7000); + connect("client", client, "192.168.0.10", 55555); + sleepUntil(9000); + send("client", client, "Welt"); + break; + case "reconnect": + sleepUntil(500); + connect("client", client, "192.168.0.10", 55555); + sleepUntil(2000); + disconnect("client", client); + sleepUntil(4000); + connect("client", client, "192.168.0.10", 55555); + sleepUntil(6000); + send("client", client, "Hallo"); + break; + case "twoClients": + sleepUntil(500); + connect("client", client, "192.168.0.10", 55555); + sleepUntil(1000); + connect("other", other, "192.168.0.10", 55555); + sleepUntil(2000); + send("client", client, "eins"); + sleepUntil(3000); + send("other", other, "zwei"); + sleepUntil(4000); + disconnect("client", client); + sleepUntil(5000); + disconnect("other", other); + break; + case "otherPort": + sleepUntil(500); + event("server", "call setAktiv false"); + server.setAktiv(false); + sleepUntil(1500); + event("server", "call setPort 7"); + server.setPort(7); + event("server", "call setAktiv true"); + server.setAktiv(true); + sleepUntil(2000); + connect("client", client, "192.168.0.10", 7); + sleepUntil(4000); + send("client", client, "Hallo"); + sleepUntil(6000); + connect("other", other, "192.168.0.10", 55555); + break; + default: + throw new IllegalArgumentException("unknown story " + story); + } + sleepUntil(end); + + ScenarioNetwork.Json out = new ScenarioNetwork.Json(); + out.open('{'); + network.writeFrames(out); + synchronized (EchoRun.class) { + out.key("events").open('['); + for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).close(']'); + out.close(']'); + } + out.close('}'); + network.stop(); + ScenarioNetwork.write(args[3], out); + System.exit(0); + } +}