diff --git a/CHANGES.md b/CHANGES.md index b7eaaaf..30b2350 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,7 +14,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | | `src/model/documentation.ts` (Java-Regeln) | Java `JDocuElement` (Konstruktor, `fontColor`, `panelColor`, `showPopupMenu`, `mouseDragged`, `setColor`), `GUIDocumentationPanel.updateViewport` (gemessen mit `DocuRun`) | geändert (F4.8a): `isItalic` (Javas Stil 2, mit Fett 3; ging vorher beim Speichern verloren); ein Element behält Größe und Schriftgröße seiner Datei (Swift vergrößert auf 32 × 24, Schrift nur 8–72); fehlende Schrift Dialog 12; neue Elemente 100 × 50, Fläche grau (204), Text schwarz; Javas 15 Text- und 12 Flächenfarben, Schriftgrößen 12/18/24, Stile Normal/Kursiv/Fett; `resizedFrame` (Javas Ziehen am Rand, kleinste Größe Fläche 20 × 20, Text 88 × 20); `inJavaDrawingOrder` (Texte über Flächen, je Art das frühere oben) | | `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. Seit F3.4a läuft die Befehlszeile mit (`terminal`, auf den Dateien des Knotens). Seit F3.4b nach Javas `ICMP`/`ICMPThread`, an Java-Filius gemessen: ein Echo im Programm-Thread (`icmpEchoSteps`: Warteschlange leeren, senden mit ARP-Wartezeit, eine RTT auf die erste ICMP-Nachricht warten, gleich welche), ein ICMP-Paket an den Broadcast eines eigenen Netzes geht mit TTL 1 an alle und wird von dort beantwortet, einer für ein fremdes Netz verworfen; Javas Routenwahl (`routeRow`), die ARP-Tabelle mit Javas Broadcast-Zeile (`arpTable`, `removeArpEntry`, `resetArpTable`), DNS im Programm-Thread mit Antwort- und Autoritätsteil (`resolveDnsSteps`), ein UDP-Socket weiß, dass er ohne Gegenstelle lauscht (`listening`); ein Rechner nimmt Pakete an 127.0.0.1 an. Eine ARP-Wartezeit endet wie Javas `wait` nach allem, was zur selben Zeit sonst geschieht (eine Antwort zur selben Millisekunde zählt noch). Seit F3.4c die Uhrzeit eines Frames (`wallTime`) für `tcpdump`. Seit F3.6a läuft das Gnutella-Programm mit (`gnutella`, Schnappschuss `gnutellaByNodeId`), mit eigener Zufallsquelle für die GUIDs (`drawGnutellaGuid`). Seit F3.5a schreibt der Mailserver seine Kontendatei `/mailserver/konten.txt` beim Start und bei jeder Änderung seiner Konten in die Dateien des Knotens (Java schreibt die Benachrichtigung an einen lokalen Absender erst beim nächsten Speichern hinein). Abweichend von Java: eine Echo-Antwort kommt von der gefragten Adresse (RFC 792; Java antwortet von 127.0.0.1, wenn die eigene Adresse gefragt war, und von der nahen Karte, wenn die ferne eines Routers gefragt war), und „Netz nicht erreichbar“ vom Router trägt wie „Host nicht erreichbar“ das Paket, wie er es weitergeleitet hätte, mit Absender (Java bettet ein halb gebautes ohne Absender ein). Seit F3.3b läuft das E-Mail-Programm mit (`emailClient`), seine gesendeten und abgeholten Mails stehen im Schnappschuss (`emailClientsByNodeId`); ein Programm-Thread kann MX nachschlagen (`resolveDnsMailServerSteps`). Seit F3.3a läuft der E-Mail-Server mit (`emailServer`); was er annimmt oder löscht, steht im Schnappschuss (`emailServersByNodeId`), aus dem es zurück ins Projekt geht; die Uhrzeit für seine Benachrichtigungen kommt aus der Option `wallClock`. Seit F3.2c ändert die Seite `konfig` der Router und Heimrouter die Firewall zur Laufzeit; die Änderung steht im Schnappschuss (`firewallConfiguration`), aus dem sie zurück ins Projekt geht. Seit F3.2b starten Router und Heimrouter ihren Webserver auf Port 80 mit Javas Seiten (Javas Server lauscht wegen eines Wettlaufs in `setAktiv` manchmal nie, der Kern immer; `swiftRouters` lässt ihn für den Swift-Vergleich weg), die Firewall führt ein Protokoll je Knoten (Regeltreffer bei TCP und UDP, verworfenes ICMP), das wie Javas Protokollseite einen Neustart überlebt, eine RIP-Route kennt die öffentliche Adresse des meldenden Routers (`hopPublicIp`), und TCP zählt Sequenznummern und Segmentgröße wie Java in Zeichen (UTF-16-Einheiten) statt in Bytes, ohne ein Zeichenpaar zu zerreißen; Bytes, die kein Text sind, zählen als Bytes. Seit F3.2a schließt ein Programm wie Javas `schliessen()` nur auf Bitte (`requestTcpClose`): Das FIN geht zur selben Zeit, nach dem, was der Programm-Thread noch tut. 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/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.4a läuft die Befehlszeile mit (`terminal`, auf den Dateien des Knotens). Seit F3.4b nach Javas `ICMP`/`ICMPThread`, an Java-Filius gemessen: ein Echo im Programm-Thread (`icmpEchoSteps`: Warteschlange leeren, senden mit ARP-Wartezeit, eine RTT auf die erste ICMP-Nachricht warten, gleich welche), ein ICMP-Paket an den Broadcast eines eigenen Netzes geht mit TTL 1 an alle und wird von dort beantwortet, einer für ein fremdes Netz verworfen; Javas Routenwahl (`routeRow`), die ARP-Tabelle mit Javas Broadcast-Zeile (`arpTable`, `removeArpEntry`, `resetArpTable`), DNS im Programm-Thread mit Antwort- und Autoritätsteil (`resolveDnsSteps`), ein UDP-Socket weiß, dass er ohne Gegenstelle lauscht (`listening`); ein Rechner nimmt Pakete an 127.0.0.1 an. Eine ARP-Wartezeit endet wie Javas `wait` nach allem, was zur selben Zeit sonst geschieht (eine Antwort zur selben Millisekunde zählt noch). Seit F3.4c die Uhrzeit eines Frames (`wallTime`) für `tcpdump`. Seit F3.6a läuft das Gnutella-Programm mit (`gnutella`, Schnappschuss `gnutellaByNodeId`), mit eigener Zufallsquelle für die GUIDs (`drawGnutellaGuid`). Seit F3.5a schreibt der Mailserver seine Kontendatei `/mailserver/konten.txt` beim Start und bei jeder Änderung seiner Konten in die Dateien des Knotens (Java schreibt die Benachrichtigung an einen lokalen Absender erst beim nächsten Speichern hinein). Abweichend von Java: eine Echo-Antwort kommt von der gefragten Adresse (RFC 792; Java antwortet von 127.0.0.1, wenn die eigene Adresse gefragt war, und von der nahen Karte, wenn die ferne eines Routers gefragt war), und „Netz nicht erreichbar“ vom Router trägt wie „Host nicht erreichbar“ das Paket, wie er es weitergeleitet hätte, mit Absender (Java bettet ein halb gebautes ohne Absender ein). Seit F3.3b läuft das E-Mail-Programm mit (`emailClient`), seine gesendeten und abgeholten Mails stehen im Schnappschuss (`emailClientsByNodeId`); ein Programm-Thread kann MX nachschlagen (`resolveDnsMailServerSteps`). Seit F3.3a läuft der E-Mail-Server mit (`emailServer`); was er annimmt oder löscht, steht im Schnappschuss (`emailServersByNodeId`), aus dem es zurück ins Projekt geht; die Uhrzeit für seine Benachrichtigungen kommt aus der Option `wallClock`. Seit F3.2c ändert die Seite `konfig` der Router und Heimrouter die Firewall zur Laufzeit; die Änderung steht im Schnappschuss (`firewallConfiguration`), aus dem sie zurück ins Projekt geht. Seit F3.2b starten Router und Heimrouter ihren Webserver auf Port 80 mit Javas Seiten (Javas Server lauscht wegen eines Wettlaufs in `setAktiv` manchmal nie, der Kern immer; `swiftRouters` lässt ihn für den Swift-Vergleich weg), die Firewall führt ein Protokoll je Knoten (Regeltreffer bei TCP und UDP, verworfenes ICMP), das wie Javas Protokollseite einen Neustart überlebt, eine RIP-Route kennt die öffentliche Adresse des meldenden Routers (`hopPublicIp`), und TCP zählt Sequenznummern und Segmentgröße wie Java in Zeichen (UTF-16-Einheiten) statt in Bytes, ohne ein Zeichenpaar zu zerreißen; Bytes, die kein Text sind, zählen als Bytes. Seit F3.2a schließt ein Programm wie Javas `schliessen()` nur auf Bitte (`requestTcpClose`): Das FIN geht zur selben Zeit, nach dem, was der Programm-Thread noch tut. 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. Remote Link seit dem Modem-Nachtrag (11.10.2026) nach Swifts `forwardFrameThroughRemoteLink`/`deliverRemoteLinkFrame`/`remoteLinkCondition` (Paar im Projekt aus den zwei eingeschalteten Modems an einem Port, Kreisunterdrückung je Modem), das Zeitverhalten nach Javas `ModemFirmware`, an Java-Filius gemessen (`ModemRun`): keine Karte, also kein Mitschnitt am Modem, und keine eigene Verzögerung (Swift 20 ms je Richtung, mit der Geschwindigkeit skaliert). Neu: ein Modem eines anderen Projekts (`relay`) gibt seine Rahmen dem, der die Engine betreibt (Server F), und schickt, was von dort kommt, mit eigenen Rahmen- und Paketnummern auf sein Kabel (`receiveRelayedFrame`); nach `MAXIMUM_RELAY_HOPS` Wegen zwischen Projekten wird ein Rahmen verworfen. Swifts LAN-Transport über das Apple-Netzwerk nicht übernommen | | `src/runtime/engine.ts` (ganze Millisekunden) | Java `System.currentTimeMillis` (long) | behoben (F4.6b): `start` und `advance` runden auf ganze Millisekunden ab, wie Javas Uhr zählt. Gefunden im Browser: der Raum gab `performance.now()` mit Bruchteilen weiter, und ein Ping blieb nach der ersten Zeitüberschreitung hängen; alle Tests liefen zuvor mit glatten Takten | | `src/runtime/engine.ts` (leere Nachricht), `test/apps.test.ts` | Java `TCPSocket.erstelleSegmente`, `TCPSocket.run` (gemessen mit `EchoWindowRun`) | behoben (F4.6g1): ein leeres Segment mit PSH an der erwarteten Stelle ist eine leere Nachricht und wird ausgeliefert (vorher galt es als Wiederholung, weil seine Nummer die nächste nicht weiterzählt; Javas Echo beantwortet sie). Java beendet eine Nachricht mit jedem PSH, auch dem eines wiederholten Segments (eine leere Nachricht mehr, wenn ein ACK verloren geht); hier nur mit einem neuen — ein wiederholtes leeres ist an seiner Nummer nicht von einem neuen zu unterscheiden (benannt) | | `src/runtime/apps/dnsServer.ts`, `src/runtime/dnsHosts.ts`, `src/runtime/engine.ts` (DNS-Server), `src/runtime/snapshot.ts` (`DnsServerSnapshot`), `test/dnsWindow.test.ts`, `test/dnsWindow.java.json` | Java `DNSServer` (`hinzuRecord`, `loescheResourceRecord`, `changeSingleEntry`, `leseRecordListe`, `schreibeRecordListe`), `ResourceRecord`, `GUIApplicationDNSServerWindow`, `DNSConfigTable` (gemessen mit `DnsWindowRun`) | neu (F4.6g2): der DNS-Server als Programm, das sein Fenster ändert — an und aus (sein Socket auf Port 53), rekursive Auflösung, und die Einträge, die `/dns/hosts` sind und bei jeder Anfrage gelesen werden wie in Java (ein anderes Programm, der Texteditor, ändert sie sofort); ohne Datei die der Einstellungen. Der Schnappschuss nimmt jetzt jeden installierten DNS-Server mit, ob er läuft (`isActive`). Der Leser von `/dns/hosts` zieht aus `packages/fls` hierher (`isHostsDomainName` statt `isDomainName`), dazu `newHostsRecord`, `dnsHostsText`, `hostsRecordsOf`. Gegen Javas Fenster geprüft, auch die Antworten des Resolvers. Abweichend von Java: ein hinzugefügter Eintrag behält, was ein anderes Programm inzwischen in die Datei schrieb (Java schreibt seine alte Liste zurück); eine Zelle ändert nur ihren Eintrag (Java auch die folgenden Einträge anderer Art); eine entfernte Zeile ist dieser Eintrag (Java der erste des Namens); ein Eintrag braucht einen Namen (Java nimmt den leeren) | diff --git a/packages/core/src/runtime/config.ts b/packages/core/src/runtime/config.ts index bf04e9c..f600898 100644 --- a/packages/core/src/runtime/config.ts +++ b/packages/core/src/runtime/config.ts @@ -139,14 +139,25 @@ export interface GatewayPortForwardingRow { } /** - * The in-project pairing of two remote links (Java Modem). Swift's LAN - * transport settings belong to its Network-framework controller and are not - * part of the port. + * Where a modem's partner is when it is in another project (server F's + * relay): this modem offers a connection under a code derived from its + * project and itself, or joins the modem that offers `code`. Java's modem + * names the other FILIUS by IP address and port; a code takes their place. + */ +export type RemoteLinkRelay = { readonly role: 'offer' } | { readonly role: 'join'; readonly code: string }; + +/** + * A modem (Java Modem, Swift remote link): paired in the project with the one + * other modem on its port (`pairIdentifier`), or, with `relay`, with a modem + * of another project. Swift's LAN transport settings belong to its + * Network-framework controller and are not part of the port. */ export interface RemoteLinkConfiguration { readonly pairIdentifier: string; readonly latencyMilliseconds: number; readonly isEnabled: boolean; + /** A modem of another project; none: the pair in this project. */ + readonly relay?: RemoteLinkRelay; } export const DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS = 20; diff --git a/packages/core/src/runtime/engine.ts b/packages/core/src/runtime/engine.ts index 8e5c764..5783071 100644 --- a/packages/core/src/runtime/engine.ts +++ b/packages/core/src/runtime/engine.ts @@ -52,8 +52,12 @@ // Calls that block in Java (resolving an address, waiting for an echo reply) // advance the virtual clock inside the call, exactly as the Swift port does. // -// Not ported yet — each place throws NotPortedError instead of guessing: -// remote links. +// Modems (Java Modem, Swift remote links) after Java's timing: a modem has no +// card and no delay of its own; a frame that reaches it leaves its partner at +// once (Java's TCP between the two FILIUS). The pair after Swift's inProject: +// the two enabled modems on a port. A modem of another project (`relay`) +// hands its frames to whoever runs the engine (server F's relay) and puts on +// its cable what comes from there. import { compareIds, type Id } from '../ids.js'; import { forwardsIPv4ByDefault, isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js'; @@ -67,6 +71,7 @@ import type { InterfaceConfiguration, InterfaceKey, ManualRoute, + RemoteLinkConfiguration, } from './config.js'; import { ALL_PORTS, @@ -88,6 +93,7 @@ import { TCP_FLAG, type ArpPacket, type EthernetFrame, + type EthernetPayload, type IcmpMessage, type IcmpMessageKind, type IPv4Packet, @@ -148,6 +154,28 @@ export const LOCALHOST_IP_ADDRESS = '127.0.0.1'; /** The seed a simulation uses unless told otherwise. */ export const DEFAULT_SEED = 0xf11a5n; +/** How often a frame may go through modems into other projects before it is dropped (two pairs between the same networks make a circle). */ +export const MAXIMUM_RELAY_HOPS = 16; + +/** How a modem stands (Swift TopologyRemoteLinkRuntimeCondition, without its LAN transport). */ +export type RemoteLinkCondition = + | { readonly kind: 'stopped' } + | { readonly kind: 'missingConfiguration' } + | { readonly kind: 'disabled' } + | { readonly kind: 'unpaired' } + | { readonly kind: 'ambiguous'; readonly enabledNodeCount: number } + /** Its own cable or its partner's missing. */ + | { readonly kind: 'detached'; readonly partnerNodeId?: Id } + | { readonly kind: 'active'; readonly partnerNodeId: Id } + /** A modem of another project, on its cable: whether it has a partner, the relay knows. */ + | { readonly kind: 'relay' }; + +/** A modem's pair in the project, trimmed; undefined when it names none. */ +function pairOf(configuration: RemoteLinkConfiguration): string | undefined { + const pair = configuration.pairIdentifier.trim(); + return pair === '' ? undefined : pair; +} + /** A part of the Swift engine that has not been carried over yet. */ export class NotPortedError extends Error { constructor(feature: string) { @@ -193,22 +221,7 @@ export type ScheduledEventKind = readonly latencyMilliseconds: number; readonly frame: EthernetFrame; } - | { - readonly kind: 'remoteLinkDelivery'; - readonly sourceNodeId: Id; - readonly partnerNodeId: Id; - readonly partnerPortId: Id; - readonly pairIdentifier: string; - readonly latencyMilliseconds: number; - readonly frame: EthernetFrame; - } - | { - readonly kind: 'lanRemoteLinkTransmission'; - readonly nodeId: Id; - readonly pairIdentifier: string; - readonly latencyMilliseconds: number; - readonly frame: EthernetFrame; - }; + ; export interface ScheduledEvent { readonly id: number; @@ -729,6 +742,8 @@ export interface RuntimeState { switchForwardingTablesByNodeId: Map>; switchForwardingUpdatedAtMillisecondsByNodeId: Map>; switchSeenFrameIdentitiesByNodeId: Map>; + /** The frames each modem passed on (Swift's remote link cycle suppression). */ + remoteLinkSeenFrameIdentitiesByNodeId: Map>; deliveredIPv4PacketsByNodeId: Map; icmpObservationsByNodeId: Map; firewallDecisions: FirewallDecision[]; @@ -772,6 +787,7 @@ function initialState(seed: bigint): RuntimeState { switchForwardingTablesByNodeId: new Map(), switchForwardingUpdatedAtMillisecondsByNodeId: new Map(), switchSeenFrameIdentitiesByNodeId: new Map(), + remoteLinkSeenFrameIdentitiesByNodeId: new Map(), deliveredIPv4PacketsByNodeId: new Map(), icmpObservationsByNodeId: new Map(), firewallDecisions: [], @@ -867,7 +883,6 @@ function isNetworkDeliveryEvent(kind: ScheduledEventKind): boolean { return ( kind.kind === 'ethernetLinkDeparture' || kind.kind === 'ethernetLinkDelivery' || - kind.kind === 'remoteLinkDelivery' || kind.kind === 'arpAnswered' ); } @@ -951,6 +966,8 @@ export class NetworkRuntimeEngine { private readonly gnutellaPeersByNodeId = new Map(); /** The time of day a program writes into a mail (Java's new Date()). */ private readonly wallClock: () => number; + /** Where a modem of another project's frames go (the constructor's `relay`). */ + private readonly relay: ((nodeId: Id, frame: EthernetFrame) => void) | undefined; /** Each node's firewall log; it outlives a stop and a new start, as Java's log page does. */ private readonly firewallLogsByNodeId = new Map[]>(); /** Accepted connections a program has taken (acceptTcpSteps). */ @@ -973,6 +990,9 @@ export class NetworkRuntimeEngine { * * `wallClock`: the time of day in milliseconds, which a program writes * into a mail (the e-mail server's notice); the host's clock by default. + * + * `relay`: takes the frames a modem of another project passes on (server + * F's relay); without it they are lost, as at a modem that is not connected. */ constructor( seed: bigint | number = DEFAULT_SEED, @@ -984,9 +1004,11 @@ export class NetworkRuntimeEngine { readonly swiftRouters?: boolean; readonly captureRetention?: CaptureRetentionPolicy; readonly wallClock?: () => number; + readonly relay?: (nodeId: Id, frame: EthernetFrame) => void; } = {}, ) { this.wallClock = options.wallClock ?? Date.now; + this.relay = options.relay; this.packetCapture = new PacketCapture(options.captureRetention ?? PRODUCTION_CAPTURE_RETENTION); this.cablesCarryOneFrameAtATime = options.swiftCables !== true; this.swiftArp = options.swiftArp === true; @@ -1096,6 +1118,7 @@ export class NetworkRuntimeEngine { state.switchForwardingTablesByNodeId.clear(); state.switchForwardingUpdatedAtMillisecondsByNodeId.clear(); state.switchSeenFrameIdentitiesByNodeId.clear(); + state.remoteLinkSeenFrameIdentitiesByNodeId.clear(); state.deliveredIPv4PacketsByNodeId.clear(); state.icmpObservationsByNodeId.clear(); state.socketsById.clear(); @@ -1698,9 +1721,6 @@ export class NetworkRuntimeEngine { wake?.(); return; } - case 'remoteLinkDelivery': - case 'lanRemoteLinkTransmission': - throw new NotPortedError('remote links'); } } @@ -4568,9 +4588,10 @@ export class NetworkRuntimeEngine { return this.stateValue.topologySnapshot.nodes.find((node) => node.id === nodeId); } - /** Java logs a frame on a card; a switch has none. */ + /** Java logs a frame on a card; a switch and a modem have none. */ private captureOnCard(nodeId: Id, portId: Id, frame: EthernetFrame): void { - if (this.nodeSnapshot(nodeId)?.kind === 'networkSwitch') return; + const kind = this.nodeSnapshot(nodeId)?.kind; + if (kind === 'networkSwitch' || kind === 'remoteLink') return; this.packetCapture.record(portId, this.stateValue.currentTimeMilliseconds, frame); } @@ -4592,7 +4613,10 @@ export class NetworkRuntimeEngine { this.forwardFrameThroughSwitch(frame, node, incomingPortId); return; } - if (node.kind === 'remoteLink') throw new NotPortedError('remote links'); + if (node.kind === 'remoteLink') { + this.forwardFrameThroughRemoteLink(frame, node, incomingPortId); + return; + } this.recordTrace({ frameIdentity: frame.identity, @@ -4633,6 +4657,138 @@ export class NetworkRuntimeEngine { // ── Switch ─────────────────────────────────────────────────────────────── + // ── Modems ────────────────────────────────────────────────────────────── + + /** + * How a modem stands (Swift's remoteLinkCondition): its partner in the + * project, or why it has none; `relay` for a modem of another project, + * whose partner only the relay knows. Undefined for anything but a modem. + */ + remoteLinkCondition(nodeId: Id): RemoteLinkCondition | undefined { + const node = this.nodeSnapshot(nodeId); + if (node === undefined || node.kind !== 'remoteLink') return undefined; + if (this.stateValue.phase !== 'running') return { kind: 'stopped' }; + const configuration = this.stateValue.topologySnapshot.remoteLinkConfigurationsByNodeId.get(nodeId); + const pairIdentifier = configuration === undefined ? undefined : pairOf(configuration); + if (configuration === undefined || (pairIdentifier === undefined && configuration.relay === undefined)) return { kind: 'missingConfiguration' }; + if (!configuration.isEnabled) return { kind: 'disabled' }; + const attached = (id: Id) => { + const port = this.nodeSnapshot(id)?.ports[0]; + return port !== undefined && this.attachedEndpoint(id, port.id) !== undefined; + }; + if (configuration.relay !== undefined) return attached(nodeId) ? { kind: 'relay' } : { kind: 'detached' }; + const matches = this.stateValue.topologySnapshot.nodes + .filter((candidate) => { + if (candidate.kind !== 'remoteLink') return false; + const other = this.stateValue.topologySnapshot.remoteLinkConfigurationsByNodeId.get(candidate.id); + return other !== undefined && other.isEnabled && other.relay === undefined && pairOf(other) === pairIdentifier; + }) + .map((candidate) => candidate.id); + if (matches.length > 2) return { kind: 'ambiguous', enabledNodeCount: matches.length }; + const partnerNodeId = matches.length === 2 ? matches.find((id) => id !== nodeId) : undefined; + if (partnerNodeId === undefined) return { kind: 'unpaired' }; + return attached(nodeId) && attached(partnerNodeId) ? { kind: 'active', partnerNodeId } : { kind: 'detached', partnerNodeId }; + } + + /** + * A frame reached a modem on its cable: it leaves the partner modem at once + * (Java's modems pass it over TCP; no delay of their own), or goes to the + * relay for a modem of another project. Lost when the modem has no partner, + * as at Java's modem that is not connected. + */ + private forwardFrameThroughRemoteLink(frame: EthernetFrame, node: NodeSnapshot, incomingPortId: Id): void { + const drop = (detail: string) => + this.recordTrace({ + frameIdentity: frame.identity, + packetIdentity: packetIdentityInFrame(frame), + nodeId: node.id, + interfaceId: incomingPortId, + direction: 'inbound', + layer: 'dataLink', + operation: 'dropped', + beforeHeaders: ethernetHeaders(frame), + detail, + }); + const seen = getOrCreate(this.stateValue.remoteLinkSeenFrameIdentitiesByNodeId, node.id, () => new Set()); + if (seen.has(frame.identity)) { + drop('remote link cycle suppression'); + return; + } + seen.add(frame.identity); + const condition = this.remoteLinkCondition(node.id); + if (condition?.kind === 'relay') { + const hops = frame.relayHops ?? 0; + if (hops >= MAXIMUM_RELAY_HOPS) { + drop('remote link relay hop limit'); + return; + } + if (this.relay === undefined) { + drop('remote link relay unavailable'); + return; + } + this.recordTrace({ + frameIdentity: frame.identity, + packetIdentity: packetIdentityInFrame(frame), + nodeId: node.id, + interfaceId: incomingPortId, + direction: 'outbound', + layer: 'dataLink', + operation: 'forwarded', + beforeHeaders: ethernetHeaders(frame), + detail: 'remote link relayed', + }); + this.relay(node.id, { ...frame, relayHops: hops + 1 }); + return; + } + if (condition?.kind !== 'active') { + drop(`remote link ${condition?.kind ?? 'missingConfiguration'}`); + return; + } + const partnerPortId = this.nodeSnapshot(condition.partnerNodeId)!.ports[0]!.id; + getOrCreate(this.stateValue.remoteLinkSeenFrameIdentitiesByNodeId, condition.partnerNodeId, () => new Set()).add(frame.identity); + this.recordTrace({ + frameIdentity: frame.identity, + packetIdentity: packetIdentityInFrame(frame), + nodeId: condition.partnerNodeId, + interfaceId: partnerPortId, + direction: 'outbound', + layer: 'dataLink', + operation: 'forwarded', + beforeHeaders: ethernetHeaders(frame), + detail: `remote link delivered pair=${pairOf(this.stateValue.topologySnapshot.remoteLinkConfigurationsByNodeId.get(node.id)!)}`, + }); + this.sendEthernetFrame(condition.partnerNodeId, partnerPortId, frame); + } + + /** + * A frame that came from the partner of a modem of another project (server + * F's relay): it leaves the modem on its cable, with this engine's numbers + * for the frame and its packet — the other engine counts its own, and a + * switch would take a number it saw before for the same frame. False when + * the simulation does not run or the modem cannot take it. + */ + receiveRelayedFrame(nodeId: Id, frame: EthernetFrame): boolean { + if (this.remoteLinkCondition(nodeId)?.kind !== 'relay') return false; + const portId = this.nodeSnapshot(nodeId)!.ports[0]!.id; + const payload: EthernetPayload = + frame.payload.kind === 'ipv4' ? { kind: 'ipv4', packet: { ...frame.payload.packet, identity: this.allocatePacketIdentity() } } : frame.payload; + const local: EthernetFrame = { ...frame, identity: this.allocateFrameIdentity(), payload }; + getOrCreate(this.stateValue.remoteLinkSeenFrameIdentitiesByNodeId, nodeId, () => new Set()).add(local.identity); + this.recordTrace({ + frameIdentity: local.identity, + packetIdentity: packetIdentityInFrame(local), + nodeId, + interfaceId: portId, + direction: 'inbound', + layer: 'dataLink', + operation: 'received', + afterHeaders: ethernetHeaders(local), + detail: 'remote link relayed', + }); + this.sendEthernetFrame(nodeId, portId, local); + return true; + } + private forwardFrameThroughSwitch(frame: EthernetFrame, switchNode: NodeSnapshot, incomingPortId: Id): void { const state = this.stateValue; const seen = state.switchSeenFrameIdentitiesByNodeId.get(switchNode.id); diff --git a/packages/core/src/runtime/packets.ts b/packages/core/src/runtime/packets.ts index 9c17509..b687663 100644 --- a/packages/core/src/runtime/packets.ts +++ b/packages/core/src/runtime/packets.ts @@ -8,6 +8,8 @@ export interface EthernetFrame { readonly sourceMacAddress: string; readonly destinationMacAddress: string; readonly payload: EthernetPayload; + /** How often the frame went through a modem into another project (server F's relay); none: never. */ + readonly relayHops?: number; } export type EthernetPayload = diff --git a/packages/core/src/runtime/snapshot.ts b/packages/core/src/runtime/snapshot.ts index 4c40ecb..4a706c7 100644 --- a/packages/core/src/runtime/snapshot.ts +++ b/packages/core/src/runtime/snapshot.ts @@ -19,6 +19,7 @@ import type { InterfaceKey, ManualRoute, SimpleClientConfiguration, + RemoteLinkConfiguration, SwitchConfiguration, WebServerConfiguration, EmailServerAccount, @@ -95,6 +96,8 @@ export interface TopologySnapshot { /** A home router's port forwarding (Java GatewayFirmware.staticNAT), by node. */ readonly portForwardingRowsByNodeId: ReadonlyMap; readonly switchConfigurationsByNodeId: ReadonlyMap; + /** The modems' pairs and relays (Java Modem), by node. */ + readonly remoteLinkConfigurationsByNodeId: 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. */ @@ -132,6 +135,7 @@ export interface TopologySnapshotInput { firewallConfigurationsByNodeId?: ReadonlyMap; portForwardingRowsByNodeId?: ReadonlyMap; switchConfigurationsByNodeId?: ReadonlyMap; + remoteLinkConfigurationsByNodeId?: ReadonlyMap; ipForwardingByNodeId?: ReadonlyMap; echoServersByNodeId?: ReadonlyMap; simpleClientsByNodeId?: ReadonlyMap; @@ -159,6 +163,7 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot firewallConfigurationsByNodeId: input.firewallConfigurationsByNodeId ?? new Map(), portForwardingRowsByNodeId: input.portForwardingRowsByNodeId ?? new Map(), switchConfigurationsByNodeId: input.switchConfigurationsByNodeId ?? new Map(), + remoteLinkConfigurationsByNodeId: input.remoteLinkConfigurationsByNodeId ?? new Map(), ipForwardingByNodeId: input.ipForwardingByNodeId ?? new Map(), echoServersByNodeId: input.echoServersByNodeId ?? new Map(), simpleClientsByNodeId: input.simpleClientsByNodeId ?? new Map(), diff --git a/packages/core/test/fixtures.ts b/packages/core/test/fixtures.ts index 866dca8..6578803 100644 --- a/packages/core/test/fixtures.ts +++ b/packages/core/test/fixtures.ts @@ -51,7 +51,7 @@ export function uuid(suffix: number): Id { return `00000000-0000-0000-0000-${suffix.toString(16).toUpperCase().padStart(12, '0')}`; } -const FRAME_EVENTS = new Set(['ethernetLinkDeparture', 'ethernetLinkDelivery', 'remoteLinkDelivery', 'arpAnswered']); +const FRAME_EVENTS = new Set(['ethernetLinkDeparture', 'ethernetLinkDelivery', 'arpAnswered']); /** * Advances until every frame in flight has arrived — what Swift's send did diff --git a/packages/core/test/javaRun.ts b/packages/core/test/javaRun.ts index 9f7213c..a6bca7c 100644 --- a/packages/core/test/javaRun.ts +++ b/packages/core/test/javaRun.ts @@ -135,7 +135,7 @@ export function watchedEngine(scenario: Scenario, snapshot: TopologySnapshot = s /** Every frame when it first shows on a host's card (sent, or received). */ const frames = (): SeenFrame[] => { - const hosts = new Set(scenario.nodes.filter((node) => node.kind !== 'networkSwitch').map((node) => node.id)); + const hosts = new Set(scenario.nodes.filter((node) => node.kind !== 'networkSwitch' && node.kind !== 'remoteLink').map((node) => node.id)); const firstSeen = new Map(); for (const event of engine.state.packetTraces) { if (event.layer !== 'physical' || !hosts.has(event.nodeId) || event.frameIdentity === undefined) continue; @@ -185,10 +185,10 @@ export interface CaptureCard { readonly portId: string; } -/** A scenario's cards: its hosts' ports, under the node's id for the first card and `id#index` for the others (ScenarioNetwork). */ +/** A scenario's cards: its hosts' ports, under the node's id for the first card and `id#index` for the others (ScenarioNetwork); switches and modems have none. */ export function scenarioCards(scenario: Scenario): CaptureCard[] { return scenario.nodes - .filter((node) => node.kind !== 'networkSwitch') + .filter((node) => node.kind !== 'networkSwitch' && node.kind !== 'remoteLink') .flatMap((node) => node.ports.map((port, index) => ({ key: index === 0 ? node.id : `${node.id}#${index}`, portId: port.id }))); } diff --git a/packages/core/test/modemJava.test.ts b/packages/core/test/modemJava.test.ts new file mode 100644 index 0000000..f012873 --- /dev/null +++ b/packages/core/test/modemJava.test.ts @@ -0,0 +1,164 @@ +// Modems as Java FILIUS runs them. tools/java-oracle's ModemRun joined two +// networks by a pair of modems (a server and a client over TCP on localhost) +// and pinged across (modem-java-.java.json): the modem has no card, +// so no capture of its own, and adds no delay — a frame takes the cables on +// both sides, nothing more. The engine pairs the two modems on a port and must +// show every card the same frames at about the same times. +// +// Beside Java: the pair's cycle suppression, three modems on a port, a +// partner without its cable (Swift's conditions; Java's modems have no pair +// in the project). + +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 NetworkRuntimeEngine } from '../src/index.js'; +import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, type JavaFrame, type JavaTables } from './javaRun.js'; +import { snapshotOf, type Scenario } from './scenario.js'; + +const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); +const id = (number: number) => `00000000-0000-0000-0000-000000000${number}`; +const HOST = id(401); +const SERVER_MODEM = id(405); +const CLIENT_MODEM = id(406); + +interface JavaRun extends JavaTables { + frames: Record; + events: [number, string][]; +} + +/** A ping from HOST in Java's words: the ICMP answer's type and code, or null when none came in time. */ +function ping(engine: NetworkRuntimeEngine, target: string, sequenceNumber: number): string { + const result = engine.sendIcmpEcho(HOST, target, { sequenceNumber }); + if (result.type === 'icmpError' && result.kind === 'destinationHostUnreachable') return 'ICMP 3/1'; + const answered = (engine.state.icmpObservationsByNodeId.get(HOST) ?? []).some((observation) => observation.message.kind === 'echoReply' && observation.message.sequenceNumber === sequenceNumber); + return answered ? 'ICMP 0/0' : 'null'; +} + +const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'modem-lan.scenario.json'), 'utf8')) as Scenario; + +/** Without the client modem's configuration: the server modem has no partner, as Java's that is not connected. */ +const unpaired = () => { + const snapshot = snapshotOf(scenario); + return withSnapshot(snapshot, { remoteLinkConfigurationsByNodeId: new Map([...snapshot.remoteLinkConfigurationsByNodeId].filter(([node]) => node !== CLIENT_MODEM)) }); +}; + +const STORIES: Record ReturnType }> = { + ping: { pings: 2 }, + unconnected: { pings: 1, snapshot: unpaired }, +}; + +/** Java's events without its answer's sender ("from …"), which the engine's results do not name. */ +const javaEvent = (event: string) => event.replace(/ from .*$/, ''); + +describe('modems as Java FILIUS runs them', () => { + for (const [name, story] of Object.entries(STORIES)) { + it(name, () => { + const java = JSON.parse(readFileSync(join(scenarioDirectory, `modem-java-${name}.java.json`), 'utf8')) as JavaRun; + const { engine } = watchedEngine(scenario, story.snapshot?.()); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + const events: [number, string][] = []; + for (let sequence = 1; sequence <= story.pings; sequence += 1) { + events.push([engine.state.currentTimeMilliseconds, 'sendProbe 10.0.0.2 called']); + const result = ping(engine, '10.0.0.2', sequence); + events.push([engine.state.currentTimeMilliseconds, `sendProbe 10.0.0.2 returned ${result}`]); + } + engine.handle({ type: 'advance', toMilliseconds: 10_000 }); + + expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => javaEvent(event))); + for (const [index, [time]] of events.entries()) { + expect(Math.abs(time - events[0]![0] - (java.events[index]![0] - java.events[0]![0])), `time of ${events[index]![1]}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); + } + expectSameCaptures(engine, scenario, java.frames, { tables: java }); + // A modem has no card: nothing in a capture of its port. + expect(engine.capture.frames(id(451))).toEqual([]); + expect(engine.capture.frames(id(461))).toEqual([]); + }); + } +}); + +describe('a pair of modems in the project', () => { + it('stands active while running, stopped otherwise', () => { + const { engine } = watchedEngine(scenario); + expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'active', partnerNodeId: CLIENT_MODEM }); + expect(engine.remoteLinkCondition(CLIENT_MODEM)).toEqual({ kind: 'active', partnerNodeId: SERVER_MODEM }); + expect(engine.remoteLinkCondition(HOST)).toBeUndefined(); + engine.handle({ type: 'stop' }); + expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'stopped' }); + }); + + it('adds no delay of its own: across the pair a frame takes three cables, as two take two', () => { + const { engine } = watchedEngine(scenario); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + ping(engine, '10.0.0.2', 1); + const sent = engine.capture.frames(id(411))[0]!.timeMilliseconds; + // The ARP request: 10.0.0.3 behind the switch (two cables), 10.0.0.2 behind the pair (three). + const overSwitch = engine.capture.frames(id(417))[0]!.timeMilliseconds - sent; + const overPair = engine.capture.frames(id(413))[0]!.timeMilliseconds - sent; + expect(overSwitch).toBeGreaterThan(0); + expect(overPair).toBe((overSwitch / 2) * 3); + }); + + it('pairs only enabled modems', () => { + const snapshot = snapshotOf(scenario); + const off = withSnapshot(snapshot, { + remoteLinkConfigurationsByNodeId: new Map( + [...snapshot.remoteLinkConfigurationsByNodeId].map(([node, configuration]) => [node, node === CLIENT_MODEM ? { ...configuration, isEnabled: false } : configuration]), + ), + }); + const { engine } = watchedEngine(scenario, off); + expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'unpaired' }); + expect(engine.remoteLinkCondition(CLIENT_MODEM)).toEqual({ kind: 'disabled' }); + }); + + it('has no partner when it is alone on its port, and drops what comes', () => { + const { engine } = watchedEngine(scenario, unpaired()); + expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'unpaired' }); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + ping(engine, '10.0.0.2', 1); + expect(engine.state.packetTraces.some((event) => event.nodeId === SERVER_MODEM && event.operation === 'dropped' && event.detail === 'remote link unpaired')).toBe(true); + expect(engine.capture.frames(id(413))).toEqual([]); + }); + + it('is ambiguous with a third modem on the port, and passes nothing', () => { + const third = id(408); + const snapshot = snapshotOf(scenario); + const crowded = withSnapshot(snapshot, { + nodes: [...snapshot.nodes, { id: third, kind: 'remoteLink', ports: [{ id: id(481), label: 'remote0', macAddress: '02:00:00:00:04:81' }] }], + remoteLinkConfigurationsByNodeId: new Map([...snapshot.remoteLinkConfigurationsByNodeId, [third, { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true }]]), + }); + const { engine } = watchedEngine(scenario, crowded); + expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'ambiguous', enabledNodeCount: 3 }); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + expect(ping(engine, '10.0.0.2', 1)).toBe('ICMP 3/1'); + }); + + it('is detached when its partner has no cable', () => { + const snapshot = snapshotOf(scenario); + const { engine } = watchedEngine(scenario, withSnapshot(snapshot, { links: snapshot.links.filter((link) => link.sourceNodeId !== CLIENT_MODEM) })); + expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'detached', partnerNodeId: CLIENT_MODEM }); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + expect(ping(engine, '10.0.0.2', 1)).toBe('ICMP 3/1'); + }); + + it('passes a frame once: both modems on one switch make a circle that ends at the modems', () => { + // The client modem on the switch's third port instead of at 10.0.0.2: a broadcast goes into the server modem, out of the client modem, back into the switch. + const snapshot = snapshotOf(scenario); + const circle = withSnapshot(snapshot, { + nodes: snapshot.nodes.map((node) => (node.id === id(404) ? { ...node, ports: [...node.ports, { id: id(444), label: 'sw4', macAddress: '02:00:00:00:04:44' }] } : node)), + links: [ + ...snapshot.links.filter((link) => link.targetNodeId !== id(403)), + { id: '00000000-0000-0000-0000-00000000F405', sourceNodeId: CLIENT_MODEM, sourcePortId: id(461), targetNodeId: id(404), targetPortId: id(444) }, + ], + }); + const { engine } = watchedEngine(scenario, circle); + engine.handle({ type: 'advance', toMilliseconds: 500 }); + expect(ping(engine, '10.0.0.3', 1)).toBe('ICMP 0/0'); + engine.handle({ type: 'advance', toMilliseconds: 5_000 }); + // The switch floods the request to both modems: the client modem already passed it on as the server modem's partner. + expect(engine.state.packetTraces.some((event) => event.nodeId === CLIENT_MODEM && event.detail === 'remote link cycle suppression')).toBe(true); + // 10.0.0.3 saw the request once, not again and again. + expect(engine.capture.frames(id(417)).filter((each) => each.frame.payload.kind === 'arp' && each.frame.payload.packet.operation === 'request')).toHaveLength(1); + }); +}); diff --git a/packages/core/test/modemRelay.test.ts b/packages/core/test/modemRelay.test.ts new file mode 100644 index 0000000..941af9c --- /dev/null +++ b/packages/core/test/modemRelay.test.ts @@ -0,0 +1,266 @@ +// A modem of another project (server F's relay): the engine hands what +// reaches such a modem to `relay` and puts on the modem's cable what comes +// back through receiveRelayedFrame. Here two engines stand for two projects, +// joined by a queue that delivers between their steps, as F's threads do. + +import { describe, expect, it } from 'vitest'; +import { + deviceConfiguration, + MAXIMUM_RELAY_HOPS, + NetworkRuntimeEngine, + portSnapshot, + topologySnapshot, + type EthernetFrame, + type Id, + type NodeKind, + type RemoteLinkRelay, + type TopologySnapshot, +} from '../src/index.js'; + +const id = (number: number) => `00000000-0000-0000-0000-${String(number).padStart(12, '0')}`; + +interface Device { + readonly id: number; + readonly kind: NodeKind; + readonly ports: number; + readonly ip?: string; + readonly relay?: RemoteLinkRelay; +} + +/** A project: its devices (port n of device d is d * 10 + n) and cables between ports. */ +function project(devices: readonly Device[], cables: readonly [number, number][]): TopologySnapshot { + const owner = new Map(devices.flatMap((device) => Array.from({ length: device.ports }, (_, index) => [device.id * 10 + index, device.id] as const))); + return topologySnapshot({ + nodes: devices.map((device) => ({ + id: id(device.id), + kind: device.kind, + ports: Array.from({ length: device.ports }, (_, index) => portSnapshot(id(device.id * 10 + index), `p${index}`)), + })), + links: cables.map(([source, target], index) => ({ + id: id(900_000 + source * 1000 + target + index), + sourceNodeId: id(owner.get(source)!), + sourcePortId: id(source), + targetNodeId: id(owner.get(target)!), + targetPortId: id(target), + })), + deviceConfigurations: new Map(devices.filter((device) => device.ip !== undefined).map((device) => [id(device.id), deviceConfiguration(device.ip!, '255.255.255.0', '', '')])), + remoteLinkConfigurationsByNodeId: new Map( + devices + .filter((device) => device.kind === 'remoteLink') + .map((device) => [id(device.id), { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true, ...(device.relay === undefined ? {} : { relay: device.relay }) }]), + ), + }); +} + +/** + * Two engines whose relay modems are paired as `pairs` says (A's modem → + * B's); what one relays reaches the other at its next step. `relayed` + * counts what went across. + */ +function joined(a: TopologySnapshot, b: TopologySnapshot, pairs: readonly [number, number][]) { + const queue: { to: 'a' | 'b'; node: Id; frame: EthernetFrame }[] = []; + const toB = new Map(pairs.map(([from, to]) => [id(from), id(to)])); + const toA = new Map(pairs.map(([from, to]) => [id(to), id(from)])); + let relayed = 0; + const engineA = new NetworkRuntimeEngine(7, undefined, { + relay: (node, frame) => { + relayed += 1; + queue.push({ to: 'b', node: toB.get(node)!, frame }); + }, + }); + const engineB = new NetworkRuntimeEngine(8, undefined, { + relay: (node, frame) => { + relayed += 1; + queue.push({ to: 'a', node: toA.get(node)!, frame }); + }, + }); + engineA.handle({ type: 'start', snapshot: a, seed: 7, initialTimeMilliseconds: 0 }); + engineB.handle({ type: 'start', snapshot: b, seed: 8, initialTimeMilliseconds: 0 }); + /** Both engines a millisecond further, then what is queued delivered. */ + const step = () => { + for (const engine of [engineA, engineB]) engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 1 }); + for (const { to, node, frame } of queue.splice(0)) (to === 'a' ? engineA : engineB).receiveRelayedFrame(node, frame); + }; + const run = (milliseconds: number) => { + for (let index = 0; index < milliseconds; index += 1) step(); + }; + return { engineA, engineB, run, relayed: () => relayed }; +} + +/** A UDP datagram from `from` to `target` (a send returns at once, as in Java; ARP runs while the engines step). */ +function sendUdp(engine: NetworkRuntimeEngine, from: number, target: string): void { + const socket = engine.bindUdpSocket(id(from), { remoteIPAddress: target, remotePort: 9 })!; + engine.sendUdp(socket, new TextEncoder().encode('hallo')); +} + +/** Whether a card captured a frame of `kind` from `sender`'s address. */ +function captured(engine: NetworkRuntimeEngine, port: number, kind: 'arp reply' | 'udp', sender: string): boolean { + return engine.capture + .frames(id(port)) + .some(({ frame }) => + kind === 'arp reply' + ? frame.payload.kind === 'arp' && frame.payload.packet.operation === 'reply' && frame.payload.packet.senderIPAddress === sender + : frame.payload.kind === 'ipv4' && frame.payload.packet.protocol === 'udp' && frame.payload.packet.senderIPAddress === sender, + ); +} + +const A = project( + [ + { id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' }, + { id: 2, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } }, + ], + [[10, 20]], +); +const B = project( + [ + { id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'K7F3QX' } }, + { id: 4, kind: 'networkSwitch', ports: 3 }, + { id: 5, kind: 'pc', ports: 1, ip: '10.0.0.2' }, + ], + [ + [30, 40], + [41, 50], + ], +); + +describe('a modem of another project', () => { + it('stands as relay on its cable, detached without', () => { + const { engineA } = joined(A, B, [[2, 3]]); + expect(engineA.remoteLinkCondition(id(2))).toEqual({ kind: 'relay' }); + const alone = new NetworkRuntimeEngine(7); + alone.handle({ type: 'start', snapshot: project([{ id: 2, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } }], []), seed: 7, initialTimeMilliseconds: 0 }); + expect(alone.remoteLinkCondition(id(2))).toEqual({ kind: 'detached' }); + }); + + it('joins two projects: ARP goes across and back, then the datagram', () => { + const { engineA, engineB, run } = joined(A, B, [[2, 3]]); + sendUdp(engineA, 1, '10.0.0.2'); + run(3_000); + expect(captured(engineA, 10, 'arp reply', '10.0.0.2')).toBe(true); + expect(captured(engineB, 50, 'udp', '10.0.0.1')).toBe(true); + // A modem has no card; each side captures only its own cards. + expect(engineA.capture.frames(id(20))).toEqual([]); + }); + + it('numbers a frame from the other project anew, so that a switch does not take it for one it saw', () => { + // B with a second computer on the switch: their datagrams go between them, unicast, not across. + const busyB = project( + [ + { id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'K7F3QX' } }, + { id: 4, kind: 'networkSwitch', ports: 3 }, + { id: 5, kind: 'pc', ports: 1, ip: '10.0.0.2' }, + { id: 9, kind: 'pc', ports: 1, ip: '10.0.0.3' }, + ], + [ + [30, 40], + [41, 50], + [42, 90], + ], + ); + const { engineA, engineB, run } = joined(A, busyB, [[2, 3]]); + for (let index = 0; index < 5; index += 1) { + sendUdp(engineB, 5, '10.0.0.3'); + run(500); + } + // B's switch has seen the number A's next frame will carry. + expect(engineB.state.switchSeenFrameIdentitiesByNodeId.get(id(4))!.has(engineA.state.nextFrameIdentity)).toBe(true); + sendUdp(engineA, 1, '10.0.0.2'); + run(3_000); + expect(captured(engineB, 50, 'udp', '10.0.0.1')).toBe(true); + expect(engineB.state.packetTraces.some((event) => event.detail === 'switch cycle suppression')).toBe(false); + }); + + it(`drops a frame after ${MAXIMUM_RELAY_HOPS} trips between projects: two pairs between the same networks make no endless circle`, () => { + const a = project( + [ + { id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' }, + { id: 6, kind: 'networkSwitch', ports: 3 }, + { id: 2, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } }, + { id: 7, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } }, + ], + [ + [10, 60], + [61, 20], + [62, 70], + ], + ); + const b = project( + [ + { id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'AAAAAA' } }, + { id: 8, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'BBBBBB' } }, + { id: 4, kind: 'networkSwitch', ports: 2 }, + ], + [ + [30, 40], + [80, 41], + ], + ); + const { engineA, engineB, run, relayed } = joined(a, b, [ + [2, 3], + [7, 8], + ]); + sendUdp(engineA, 1, '10.0.0.9'); + run(20_000); + const dropped = [engineA, engineB].flatMap((engine) => engine.state.packetTraces).filter((event) => event.detail === 'remote link relay hop limit'); + expect(dropped.length).toBeGreaterThan(0); + // Two ARP requests (Java asks twice), each going round until its trips are used up. + expect(relayed()).toBeLessThanOrEqual(2 * 2 * MAXIMUM_RELAY_HOPS); + }); + + it('is no partner for a modem of this project on the same port', () => { + const engine = new NetworkRuntimeEngine(7); + const mixed = project( + [ + { id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' }, + { id: 2, kind: 'remoteLink', ports: 1 }, + { id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } }, + { id: 5, kind: 'pc', ports: 1, ip: '10.0.0.2' }, + ], + [ + [10, 20], + [30, 50], + ], + ); + engine.handle({ type: 'start', snapshot: mixed, seed: 7, initialTimeMilliseconds: 0 }); + expect(engine.remoteLinkCondition(id(2))).toEqual({ kind: 'unpaired' }); + }); + + it('loses what reaches it without a relay, as an unconnected modem does', () => { + const engine = new NetworkRuntimeEngine(7); + engine.handle({ type: 'start', snapshot: A, seed: 7, initialTimeMilliseconds: 0 }); + sendUdp(engine, 1, '10.0.0.2'); + engine.handle({ type: 'advance', toMilliseconds: 1_000 }); + expect(engine.state.packetTraces.some((event) => event.nodeId === id(2) && event.detail === 'remote link relay unavailable')).toBe(true); + }); + + it('takes nothing from the relay while stopped, nor at a modem of this project', () => { + const { engineA, engineB } = joined(A, B, [[2, 3]]); + const frame: EthernetFrame = { + identity: 1, + sourceMacAddress: '02:00:00:00:00:01', + destinationMacAddress: 'FF:FF:FF:FF:FF:FF', + payload: { + kind: 'arp', + packet: { operation: 'request', senderMacAddress: '02:00:00:00:00:01', senderIPAddress: '10.0.0.1', targetMacAddress: '00:00:00:00:00:00', targetIPAddress: '10.0.0.2' }, + }, + }; + expect(engineB.receiveRelayedFrame(id(5), frame)).toBe(false); + expect(engineB.receiveRelayedFrame(id(3), frame)).toBe(true); + engineA.handle({ type: 'stop' }); + expect(engineA.receiveRelayedFrame(id(2), frame)).toBe(false); + const inProject = new NetworkRuntimeEngine(7); + inProject.handle({ + type: 'start', + snapshot: project( + [ + { id: 2, kind: 'remoteLink', ports: 1 }, + { id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' }, + ], + [[10, 20]], + ), + seed: 7, + initialTimeMilliseconds: 0, + }); + expect(inProject.receiveRelayedFrame(id(2), frame)).toBe(false); + }); +}); diff --git a/packages/core/test/scenario.ts b/packages/core/test/scenario.ts index 746f47a..df93896 100644 --- a/packages/core/test/scenario.ts +++ b/packages/core/test/scenario.ts @@ -58,6 +58,11 @@ export interface Scenario { portForwarding?: Record; /** Routers with RIP switched on. */ rip?: string[]; + /** + * Modems: Java pairs a server and a client on a port over TCP; the core + * pairs the two modems on a port (the mode is Java's only). + */ + modems?: Record; /** Records as `name TYPE target` (Java's hinzuRecord: TTL 3600). */ dnsServers?: Record; steps: { @@ -124,6 +129,9 @@ export function snapshotOf(scenario: Scenario): TopologySnapshot { targetNodeId: link.targetNode, targetPortId: link.targetPort, })), + remoteLinkConfigurationsByNodeId: new Map( + Object.entries(scenario.modems ?? {}).map(([node, modem]) => [node, { pairIdentifier: `java-modem-port-${modem.port}`, latencyMilliseconds: 20, isEnabled: true }]), + ), deviceConfigurations: new Map( Object.entries(scenario.devices ?? {}).map(([node, device]) => [ node, diff --git a/packages/core/test/scenarios/modem-java-ping.java.json b/packages/core/test/scenarios/modem-java-ping.java.json new file mode 100644 index 0000000..a7dbf1f --- /dev/null +++ b/packages/core/test/scenarios/modem-java-ping.java.json @@ -0,0 +1,580 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000401": [ + { + "time": 734, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.0.1", + "targetIp": "10.0.0.2" + }, + { + "time": 1124, + "source": "02:00:00:00:04:13", + "destination": "02:00:00:00:04:11", + "arp": 2, + "senderIp": "10.0.0.2", + "targetIp": "10.0.0.1" + }, + { + "time": 1129, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:13", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.0.1", + "destinationIp": "10.0.0.2", + "sequence": 1 + }, + { + "time": 1466, + "source": "02:00:00:00:04:13", + "destination": "02:00:00:00:04:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.0.2", + "destinationIp": "10.0.0.1", + "sequence": 1 + }, + { + "time": 1469, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:13", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.0.1", + "destinationIp": "10.0.0.2", + "sequence": 2 + }, + { + "time": 1803, + "source": "02:00:00:00:04:13", + "destination": "02:00:00:00:04:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.0.2", + "destinationIp": "10.0.0.1", + "sequence": 2 + } + ], + "00000000-0000-0000-0000-000000000403": [ + { + "time": 915, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.0.1", + "targetIp": "10.0.0.2" + }, + { + "time": 915, + "source": "02:00:00:00:04:13", + "destination": "02:00:00:00:04:11", + "arp": 2, + "senderIp": "10.0.0.2", + "targetIp": "10.0.0.1" + }, + { + "time": 1298, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:13", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.0.1", + "destinationIp": "10.0.0.2", + "sequence": 1 + }, + { + "time": 1299, + "source": "02:00:00:00:04:13", + "destination": "02:00:00:00:04:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.0.2", + "destinationIp": "10.0.0.1", + "sequence": 1 + }, + { + "time": 1636, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:13", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.0.1", + "destinationIp": "10.0.0.2", + "sequence": 2 + }, + { + "time": 1636, + "source": "02:00:00:00:04:13", + "destination": "02:00:00:00:04:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.0.2", + "destinationIp": "10.0.0.1", + "sequence": 2 + } + ], + "00000000-0000-0000-0000-000000000407": [ + { + "time": 858, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.0.1", + "targetIp": "10.0.0.2" + } + ] + }, + "rows": { + "00000000-0000-0000-0000-000000000401": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.2", + "10.0.0.1", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:04:13 [op=REPLY, sender=02:00:00:00:04:13|10.0.0.2, target=02:00:00:00:04:11|10.0.0.1]" + ], + [ + "3", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "4", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "5", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 2" + ], + [ + "6", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 2" + ] + ], + "00000000-0000-0000-0000-000000000403": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.2", + "10.0.0.1", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:04:13 [op=REPLY, sender=02:00:00:00:04:13|10.0.0.2, target=02:00:00:00:04:11|10.0.0.1]" + ], + [ + "3", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "4", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "5", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 2" + ], + [ + "6", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 2" + ] + ], + "00000000-0000-0000-0000-000000000407": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ] + ] + }, + "layeredRows": { + "00000000-0000-0000-0000-000000000401": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.2", + "10.0.0.1", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:04:13 [op=REPLY, sender=02:00:00:00:04:13|10.0.0.2, target=02:00:00:00:04:11|10.0.0.1]" + ], + [ + "3", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "4", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "5", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 2" + ], + [ + "6", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 2" + ] + ], + "00000000-0000-0000-0000-000000000403": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.2", + "10.0.0.1", + "ARP", + "Vermittlung", + "MAC ist 02:00:00:00:04:13 [op=REPLY, sender=02:00:00:00:04:13|10.0.0.2, target=02:00:00:00:04:11|10.0.0.1]" + ], + [ + "3", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "3", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "4", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "4", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 1" + ], + [ + "5", + "02:00:00:00:04:11", + "02:00:00:00:04:13", + "", + "Netzzugang", + "0x800" + ], + [ + "5", + "10.0.0.1", + "10.0.0.2", + "ICMP", + "Vermittlung", + "ICMP Echo Request (ping), TTL: 64, Identifier: 1, Seq.-No.: 2" + ], + [ + "6", + "02:00:00:00:04:13", + "02:00:00:00:04:11", + "", + "Netzzugang", + "0x800" + ], + [ + "6", + "10.0.0.2", + "10.0.0.1", + "ICMP", + "Vermittlung", + "ICMP Echo Reply (pong), TTL: 64, Identifier: 1, Seq.-No.: 2" + ] + ], + "00000000-0000-0000-0000-000000000407": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ] + ] + }, + "events": [ + [ + 728, + "sendProbe 10.0.0.2 called" + ], + [ + 1468, + "sendProbe 10.0.0.2 returned ICMP 0/0 from 10.0.0.2" + ], + [ + 1468, + "sendProbe 10.0.0.2 called" + ], + [ + 1804, + "sendProbe 10.0.0.2 returned ICMP 0/0 from 10.0.0.2" + ] + ] +} diff --git a/packages/core/test/scenarios/modem-java-unconnected.java.json b/packages/core/test/scenarios/modem-java-unconnected.java.json new file mode 100644 index 0000000..38ee092 --- /dev/null +++ b/packages/core/test/scenarios/modem-java-unconnected.java.json @@ -0,0 +1,193 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000401": [ + { + "time": 717, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.0.1", + "targetIp": "10.0.0.2" + }, + { + "time": 3468, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.0.1", + "targetIp": "10.0.0.2" + } + ], + "00000000-0000-0000-0000-000000000403": [], + "00000000-0000-0000-0000-000000000407": [ + { + "time": 838, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.0.1", + "targetIp": "10.0.0.2" + }, + { + "time": 3579, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.0.1", + "targetIp": "10.0.0.2" + } + ] + }, + "rows": { + "00000000-0000-0000-0000-000000000401": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ] + ], + "00000000-0000-0000-0000-000000000403": [], + "00000000-0000-0000-0000-000000000407": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ] + ] + }, + "layeredRows": { + "00000000-0000-0000-0000-000000000401": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ] + ], + "00000000-0000-0000-0000-000000000403": [], + "00000000-0000-0000-0000-000000000407": [ + [ + "1", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "1", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ], + [ + "2", + "02:00:00:00:04:11", + "FF:FF:FF:FF:FF:FF", + "", + "Netzzugang", + "0x806" + ], + [ + "2", + "10.0.0.1", + "10.0.0.2", + "ARP", + "Vermittlung", + "Suche nach MAC für 10.0.0.2 [op=REQUEST, sender=02:00:00:00:04:11|10.0.0.1, target=FF:FF:FF:FF:FF:FF|10.0.0.2]" + ] + ] + }, + "events": [ + [ + 713, + "sendProbe 10.0.0.2 called" + ], + [ + 6224, + "sendProbe 10.0.0.2 returned ICMP 3/1 from 127.0.0.1" + ] + ] +} diff --git a/packages/core/test/scenarios/modem-lan.scenario.json b/packages/core/test/scenarios/modem-lan.scenario.json new file mode 100644 index 0000000..85ece9d --- /dev/null +++ b/packages/core/test/scenarios/modem-lan.scenario.json @@ -0,0 +1,128 @@ +{ + "seed": 67, + "nodes": [ + { + "id": "00000000-0000-0000-0000-000000000401", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000411", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000403", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000413", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000407", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000417", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000404", + "kind": "networkSwitch", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000441", + "label": "sw1" + }, + { + "id": "00000000-0000-0000-0000-000000000442", + "label": "sw2" + }, + { + "id": "00000000-0000-0000-0000-000000000443", + "label": "sw3" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000405", + "kind": "remoteLink", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000451", + "label": "remote0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000406", + "kind": "remoteLink", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000461", + "label": "remote0" + } + ] + } + ], + "links": [ + { + "id": "00000000-0000-0000-0000-00000000F401", + "sourceNode": "00000000-0000-0000-0000-000000000401", + "sourcePort": "00000000-0000-0000-0000-000000000411", + "targetNode": "00000000-0000-0000-0000-000000000404", + "targetPort": "00000000-0000-0000-0000-000000000441" + }, + { + "id": "00000000-0000-0000-0000-00000000F402", + "sourceNode": "00000000-0000-0000-0000-000000000407", + "sourcePort": "00000000-0000-0000-0000-000000000417", + "targetNode": "00000000-0000-0000-0000-000000000404", + "targetPort": "00000000-0000-0000-0000-000000000442" + }, + { + "id": "00000000-0000-0000-0000-00000000F403", + "sourceNode": "00000000-0000-0000-0000-000000000404", + "sourcePort": "00000000-0000-0000-0000-000000000443", + "targetNode": "00000000-0000-0000-0000-000000000405", + "targetPort": "00000000-0000-0000-0000-000000000451" + }, + { + "id": "00000000-0000-0000-0000-00000000F404", + "sourceNode": "00000000-0000-0000-0000-000000000406", + "sourcePort": "00000000-0000-0000-0000-000000000461", + "targetNode": "00000000-0000-0000-0000-000000000403", + "targetPort": "00000000-0000-0000-0000-000000000413" + } + ], + "devices": { + "00000000-0000-0000-0000-000000000401": { + "ip": "10.0.0.1", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000403": { + "ip": "10.0.0.2", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000407": { + "ip": "10.0.0.3", + "mask": "255.255.255.0" + } + }, + "modems": { + "00000000-0000-0000-0000-000000000405": { + "mode": "server", + "port": 12345 + }, + "00000000-0000-0000-0000-000000000406": { + "mode": "client", + "port": 12345 + } + }, + "steps": [] +} diff --git a/packages/fls/src/projectSnapshot.ts b/packages/fls/src/projectSnapshot.ts index 46188a1..835c400 100644 --- a/packages/fls/src/projectSnapshot.ts +++ b/packages/fls/src/projectSnapshot.ts @@ -59,6 +59,7 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot { firewallConfigurationsByNodeId: project.firewallConfigurationsByNodeId, portForwardingRowsByNodeId: project.portForwardingRowsByNodeId, switchConfigurationsByNodeId: project.switchConfigurations, + remoteLinkConfigurationsByNodeId: project.remoteLinkConfigurations, ipForwardingByNodeId: project.ipForwardingByNodeId, echoServersByNodeId: echoServers, simpleClientsByNodeId: project.simpleClientConfigurations, diff --git a/packages/sim/test/room.test.ts b/packages/sim/test/room.test.ts index ea1af76..904e64e 100644 --- a/packages/sim/test/room.test.ts +++ b/packages/sim/test/room.test.ts @@ -7,7 +7,7 @@ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import * as Y from 'yjs'; import { NetworkRuntimeEngine, type Id } from '@mdg-filius/core'; import { docSnapshot, readProject, writeProject } from '@mdg-filius/doc'; @@ -27,6 +27,22 @@ function exampleDoc(file = 'java-examples/internetwork_komplett.fls'): Y.Doc { } /** A room on a clock the test moves, and a client that keeps what it gets. */ +/** + * A core that fails at the first frame `node` sends — as it once did at a + * modem, before the core had modems. Every test lets it go again. + */ +function failingAt(node: Id): void { + const send = NetworkRuntimeEngine.prototype.sendEthernetFrame; + vi.spyOn(NetworkRuntimeEngine.prototype, 'sendEthernetFrame').mockImplementation(function (this: NetworkRuntimeEngine, from, port, frame) { + if (from === node) throw new Error('core broke'); + send.call(this, from, port, frame); + }); +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + function setUp(doc = exampleDoc()) { let now = 1_000_000; const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED, epoch: () => 1_700_000_000_000 + now }); @@ -102,11 +118,12 @@ describe('a simulation room', () => { }); it('forgets who started it when the core fails', () => { - const { room, run, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls')); + const { room, run, node } = setUp(); + failingAt(node('Webserver 5.10').id); const mia: HostMessage[] = []; const miaId = room.connect((message) => mia.push(message), 'Mia'); room.receive(miaId, { v: 1, kind: 'start' }); - room.receive(miaId, { v: 1, kind: 'input', node: node('Notebook 0.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' }); + room.receive(miaId, { v: 1, kind: 'input', node: node('Notebook 1.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' }); run(10_000); expect(ofKind(mia, 'state').at(-1)).toMatchObject({ mode: 'design' }); expect(ofKind(mia, 'state').at(-1)).not.toHaveProperty('startedBy'); @@ -287,16 +304,16 @@ describe('a simulation room', () => { }); it('tells every client when the core fails, and goes back to design mode', () => { - // FILIUS' modem example: remote links are not in the core yet, it throws at the first frame to a modem. - const { room, got, send, run, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls')); + const { room, got, send, run, node } = setUp(); + failingAt(node('Webserver 5.10').id); const other: HostMessage[] = []; room.connect((message) => other.push(message)); - const ping = () => send({ kind: 'input', node: node('Notebook 0.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' }); + const ping = () => send({ kind: 'input', node: node('Notebook 1.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' }); send({ kind: 'start' }); ping(); run(10_000); for (const each of [got, other]) { - expect(ofKind(each, 'error')).toEqual([{ v: 1, kind: 'error', code: 'engineFailed', detail: expect.stringContaining('remote links') }]); + expect(ofKind(each, 'error')).toEqual([{ v: 1, kind: 'error', code: 'engineFailed', detail: 'core broke' }]); expect(ofKind(each, 'state').at(-1)).toMatchObject({ mode: 'design' }); } expect(room.currentMode).toBe('design'); @@ -307,6 +324,18 @@ describe('a simulation room', () => { expect(ofKind(got, 'error')).toHaveLength(2); }); + it("runs FILIUS' modem example: a ping goes through the pair of modems", () => { + const { got, send, run, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls')); + const notebook = node('Notebook 0.10'); + send({ kind: 'sub', topics: [`app:${notebook.id}/terminal`] }); + send({ kind: 'start' }); + send({ kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' }); + run(10_000); + expect(ofKind(got, 'error')).toEqual([]); + const text = ofKind(got, 'app').flatMap((each) => each.lines as { text?: string }[]).map((line) => line.text ?? '').join(''); + expect(text).toContain('4 Paket(e) gesendet, 4 Paket(e) empfangen'); + }); + it('sends a card anew to a client that watches it again', () => { const { got, send, node, run } = setUp(); const notebook = node('Notebook 1.10'); @@ -340,14 +369,15 @@ describe('a simulation room', () => { }); it('reads the document anew at the start after a failure', () => { - const doc = exampleDoc('java-examples/modem_beispiel.fls'); + const doc = exampleDoc(); const { got, send, run, node } = setUp(doc); - const ping = () => send({ kind: 'input', node: node('Notebook 0.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' }); + failingAt(node('Webserver 5.10').id); + const ping = () => send({ kind: 'input', node: node('Notebook 1.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' }); send({ kind: 'start' }); ping(); run(10_000); expect(ofKind(got, 'error')).toHaveLength(1); - // Without the modems' cables, nothing reaches a modem: the next start runs. + // Without the cables, nothing reaches the web server, which fails the core: the next start runs. doc.getMap('links').clear(); send({ kind: 'start' }); ping(); diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 81046dd..c605e5b 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -345,6 +345,35 @@ seit F2.4b-2: ein Senden kehrt sofort zurück wie in Java (Swifts Senden wartete alle Frames angekommen waren), und je Knoten und Protokoll sendet ein Transport-Thread ein Segment nach dem anderen. +## Modems zur Laufzeit + +`ModemRun` verbindet auf dem Netz von `modem-lan` zwei Netze über ein +Modem-Paar: ein Server und ein Client auf `localhost`, Javas +`ModemFirmware` über einen echten TCP-Socket. `ScenarioNetwork` baut dazu +`Modem`s und stellt sie nach `modems` ein (Modus, Port). +`connectModems()` drückt den Knopf des Einstellungsfelds: erst lauschen die +Server, dann verbinden sich die Clients. 10.0.0.1 und 10.0.0.3 hängen an +einem Switch mit dem Server-Modem, 10.0.0.2 direkt am Client-Modem. Das +Werkzeug schreibt nach `modem-java-.java.json`. Abläufe: `ping` +(die Modems verbunden, zwei Pings von 10.0.0.1 an 10.0.0.2) und +`unconnected` (nicht verbunden, ein Ping). + +Gemessen (11.10.2026): + +* Das Modem hat **keine Karte**: Es erscheint in keinem Mitschnitt. +* Das Paar fügt **keine eigene Verzögerung** hinzu. Die ARP-Anfrage + braucht zu 10.0.0.3 zwei Kabel (124 ms), zu 10.0.0.2 drei (181 ms). Die + TCP-Strecke zwischen den Modems ist dagegen nicht messbar. Swifts 20 ms + je Richtung übernimmt der Kern deshalb nicht. +* Ohne Verbindung fragt ARP zweimal und bekommt keine Antwort, der Ping + endet nach 5,5 s mit „Host unreachable“ vom eigenen Rechner. Auf der + anderen Seite kommt nichts an. + +Der Kern spielt beide Abläufe wie Java (`packages/core/test/modemJava.test.ts`). +Er paart die beiden Modems an einem Port, und das Modem ohne Partner +verwirft. Dass die Verzögerung genau null ist, prüft ein eigener Test: Die +Toleranz des Vergleichs (120 ms) sähe 20 ms nicht. + ## RIP zur Laufzeit `RipRun` startet das Netz von `rip-chain` (PC 10.0.1.2 – R1 – R2 – R3 – PC diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index a45e425..e80de38 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" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" "$here/src/DocuRun.java" "$here/src/ExportRun.java" "$here/src/ReportRun.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/ModemRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" "$here/src/DocuRun.java" "$here/src/ExportRun.java" "$here/src/ReportRun.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. @@ -135,6 +135,18 @@ for run in dhcp-lan:8000 dhcp-two-servers:8000 dhcp-conflict:25000 dhcp-no-serve test -s "$output" done +# Modems as Java FILIUS runs them: modem-lan's two networks joined by a pair +# of modems (server and client over TCP on localhost); frames and the calls, +# for the core's modem comparison (packages/core/test/modemJava.test.ts). +for run in ping:8000 unconnected:9000; do + story="${run%%:*}" + output="$root/packages/core/test/scenarios/modem-java-$story.java.json" + rm -f "$output" + HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" ModemRun \ + "$root/packages/core/test/scenarios/modem-lan.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true + test -s "$output" +done + # TCP as Java FILIUS runs it: stories of socket calls on tcp-lan's network # (TcpRun); frames, calls and socket states, for the core's TCP comparison. for run in exchange:30000 closedPort:30000 lostData:45000 lostSynAck:70000 lostFin:60000 lostLastFin:60000; do diff --git a/tools/java-oracle/src/ModemRun.java b/tools/java-oracle/src/ModemRun.java new file mode 100644 index 0000000..e5ff350 --- /dev/null +++ b/tools/java-oracle/src/ModemRun.java @@ -0,0 +1,81 @@ +// Runs a story on modem-lan's network in Java FILIUS (ScenarioNetwork): two +// networks joined by a pair of modems, a server and a client on localhost +// (ModemFirmware over a real TCP socket). Records every frame each card saw +// and when each call started and ended. +// +// ModemRun +// +// 10.0.0.1 and 10.0.0.3 sit on a switch with the server modem; 10.0.0.2 is +// cabled straight to the client modem. The stories: +// ping the modems connected, 10.0.0.1 pings 10.0.0.2 twice +// unconnected the modems not connected, 10.0.0.1 pings 10.0.0.2: ARP +// goes into the modem and no further + +import filius.software.vermittlungsschicht.IcmpPaket; + +import java.util.ArrayList; +import java.util.List; + +public class ModemRun { + static final String HOST = "00000000-0000-0000-0000-000000000401"; + + static ScenarioNetwork network; + static final List events = new ArrayList<>(); + + interface Call { + String run() throws Exception; + } + + static synchronized void event(String what) { + events.add(new Object[] { System.currentTimeMillis() - network.start, what }); + } + + static void call(String label, Call call) { + event(label + " called"); + try { + event(label + " returned " + call.run()); + } catch (Exception e) { + event(label + " failed " + e.getClass().getSimpleName()); + } + } + + static String icmp(IcmpPaket packet) { + return packet == null ? "null" : "ICMP " + packet.getIcmpType() + "/" + packet.getIcmpCode() + " from " + packet.getSender(); + } + + public static void main(String[] args) throws Exception { + network = ScenarioNetwork.read(args[0]); + String story = args[1]; + long end = Long.parseLong(args[2]); + // Connected in the design, as one does with the settings panel's button before starting. + if (!story.equals("unconnected")) network.connectModems(); + network.start(); + Thread.sleep(500); + switch (story) { + case "ping": + call("sendProbe 10.0.0.2", () -> icmp(network.system(HOST).holeICMP().sendProbe("10.0.0.2", 64, 1, 1))); + call("sendProbe 10.0.0.2", () -> icmp(network.system(HOST).holeICMP().sendProbe("10.0.0.2", 64, 1, 2))); + break; + case "unconnected": + call("sendProbe 10.0.0.2", () -> icmp(network.system(HOST).holeICMP().sendProbe("10.0.0.2", 64, 1, 1))); + break; + default: + throw new IllegalArgumentException("unknown story " + story); + } + long rest = network.start + end - System.currentTimeMillis(); + if (rest > 0) Thread.sleep(rest); + + ScenarioNetwork.Json out = new ScenarioNetwork.Json(); + out.open('{'); + network.writeFrames(out); + synchronized (ModemRun.class) { + out.key("events").open('['); + for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']'); + out.close(']'); + } + out.close('}'); + network.stop(); + ScenarioNetwork.write(args[3], out); + System.exit(0); + } +} diff --git a/tools/java-oracle/src/ScenarioNetwork.java b/tools/java-oracle/src/ScenarioNetwork.java index 7a034f4..818184f 100644 --- a/tools/java-oracle/src/ScenarioNetwork.java +++ b/tools/java-oracle/src/ScenarioNetwork.java @@ -17,6 +17,7 @@ import filius.hardware.knoten.Gateway; import filius.hardware.knoten.InternetKnoten; import filius.hardware.knoten.Knoten; import filius.hardware.knoten.LokalerKnoten; +import filius.hardware.knoten.Modem; import filius.hardware.knoten.Notebook; import filius.hardware.knoten.Rechner; import filius.hardware.knoten.Switch; @@ -30,6 +31,7 @@ import filius.software.netzzugangsschicht.EthernetFrame; import filius.software.system.Betriebssystem; import filius.software.system.GatewayFirmware; import filius.software.system.InternetKnotenBetriebssystem; +import filius.software.system.ModemFirmware; import filius.software.system.SwitchFirmware; import filius.software.system.VermittlungsrechnerBetriebssystem; import filius.software.transportschicht.Segment; @@ -58,6 +60,8 @@ public class ScenarioNetwork { /** Each card's MAC address, under its node's id (the first card) or `id#index` (the others). */ final Map macByNode = new LinkedHashMap<>(); final List servers = new ArrayList<>(); + /** The modems of the scenario, servers and clients. */ + final List modems = new ArrayList<>(); /** Loaded from a .fls file (fromFile): started as the action mode starts it, in the file's order. */ boolean fromFile; /** fromFile: each card's MAC address and its node's name with the card's index, `name/index`. */ @@ -136,6 +140,10 @@ public class ScenarioNetwork { // Its first card is the WAN port, its second the LAN port. knoten = new Gateway(); break; + case "remoteLink": + // Java's modem: one port, no card; it joins its partner over TCP (modems below). + knoten = new Modem(); + break; default: throw new IllegalArgumentException("node kind " + kind + " is not built by this tool"); } @@ -235,6 +243,17 @@ public class ScenarioNetwork { for (Object each : list(scenario.getOrDefault("rip", new ArrayList<>()))) { ((VermittlungsrechnerBetriebssystem) system(nodes.get((String) each))).setRipEnabled(true); } + // Modems: Java pairs a server and a client over TCP on the port (the client + // connects to ipAdresse); connectModems() makes the connection, as the + // button of the settings panel does. + for (Map.Entry entry : map(scenario.getOrDefault("modems", new HashMap<>())).entrySet()) { + Map config = map(entry.getValue()); + ModemFirmware firmware = (ModemFirmware) nodes.get(entry.getKey()).getSystemSoftware(); + firmware.setMode("server".equals(config.get("mode")) ? ModemFirmware.SERVER : ModemFirmware.CLIENT); + firmware.setPort(((Number) config.get("port")).intValue()); + firmware.setIpAdresse("localhost"); + modems.add(entry.getKey()); + } // Firewalls: the core's defaults (inactive, drop, ICMP through, SYN only, UDP filtered) and its rules. for (Map.Entry entry : map(scenario.getOrDefault("firewalls", new HashMap<>())).entrySet()) { Map config = map(entry.getValue()); @@ -285,6 +304,20 @@ public class ScenarioNetwork { } } + /** Every server modem listens, then every client connects (JModemKonfiguration's button). */ + void connectModems() throws InterruptedException { + for (String id : modems) { + ModemFirmware firmware = (ModemFirmware) nodes.get(id).getSystemSoftware(); + if (firmware.getMode() == ModemFirmware.SERVER) firmware.starteServer(); + } + Thread.sleep(300); + for (String id : modems) { + ModemFirmware firmware = (ModemFirmware) nodes.get(id).getSystemSoftware(); + if (firmware.getMode() == ModemFirmware.CLIENT) firmware.starteClient(); + } + Thread.sleep(300); + } + InternetKnotenBetriebssystem system(String nodeId) { return (InternetKnotenBetriebssystem) nodes.get(nodeId).getSystemSoftware(); }