From 37f5d7747797599eff33c34496a54d3ea76ded55 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Wed, 30 Sep 2026 19:41:56 +0000 Subject: [PATCH] test(core): RIP measured with Java FILIUS RipRun runs rip-chain's three routers in Java FILIUS, each router's random set by reflection to the core's ripSeed, and writes the frames, every change of each RIP table and the calls. converge: the tables fill, a ping at 45 s. expire: R3 falls silent at 10 s; R2 marks its network 16 hops only at its own next beacon, R1 passes it on at once, the ping gets ICMP 3/0. Both match: per sending card the same messages at the same times, every table through the same states. The first 5 s are compared by the state the tables reach, since Java's routers start one after the other. Measured on the JDK: Java's pause divides nextFloat() by 3 in float; the core does too now (a millisecond off in one draw of about 10,000 before). Scenarios name their RIP routers in the new field rip. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 3 +- packages/core/src/runtime/random.ts | 10 +- packages/core/test/engine.test.ts | 6 +- packages/core/test/ripJava.test.ts | 169 ++++++ packages/core/test/scenario.ts | 3 + .../test/scenarios/rip-chain.scenario.json | 165 +++++ .../scenarios/rip-java-converge.java.json | 569 ++++++++++++++++++ .../test/scenarios/rip-java-expire.java.json | 563 +++++++++++++++++ tools/java-oracle/README.md | 31 + tools/java-oracle/run.sh | 13 +- tools/java-oracle/src/RipRun.java | 157 +++++ tools/java-oracle/src/ScenarioNetwork.java | 12 + 12 files changed, 1695 insertions(+), 6 deletions(-) create mode 100644 packages/core/test/ripJava.test.ts create mode 100644 packages/core/test/scenarios/rip-chain.scenario.json create mode 100644 packages/core/test/scenarios/rip-java-converge.java.json create mode 100644 packages/core/test/scenarios/rip-java-expire.java.json create mode 100644 tools/java-oracle/src/RipRun.java diff --git a/CHANGES.md b/CHANGES.md index 108ca7d..7e64cec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,7 +13,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | | `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | -| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; seit F2.4c auf den Ereignissen statt blockierend und mit rekursivem Server nach `DNSServer.answerWithRemoteData` (Nameserver der Zone, dann der eigene DNS-Server, sonst Verweis), abweichend von Java fragt er sich nie selbst (Java endlos). RIP seit F2.5a (Z. ~4700–5090) nach Javas `filius.software.rip`: eine Route mit 0 Hops je eigenem Netz, die nie verfällt; der erste Beacon nach einer Sekunde, dann alle 25,2–35,2 s aus einem Zufall je Router (Java: ein ungesetzter `Random` je Router; der Kern setzt ihn aus Szenario und Router-ID, `ripSeed`; Swift: ein Zufall für alle); eine Nachricht je Schnittstelle ohne die Routen über sie (Split Horizon); eine neue Route oder mehr Hops vom selben Router senden sofort; eine gelernte Route wird erst beim nächsten eigenen Beacon auf 16 gesetzt, wenn sie verfallen ist (Swift: bei jedem Ereignis); Nachrichten von jedem Quellport (Swift: nur 521); ein RIP-Router leitet nur nach seiner Tabelle weiter, wenige Hops vor langer Maske. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, Remote Link, WLAN | +| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; seit F2.4c auf den Ereignissen statt blockierend und mit rekursivem Server nach `DNSServer.answerWithRemoteData` (Nameserver der Zone, dann der eigene DNS-Server, sonst Verweis), abweichend von Java fragt er sich nie selbst (Java endlos). RIP seit F2.5a (Z. ~4700–5090) nach Javas `filius.software.rip`, seit F2.5b an Java-Filius gemessen (die Pause in Javas `float`-Rechnung): eine Route mit 0 Hops je eigenem Netz, die nie verfällt; der erste Beacon nach einer Sekunde, dann alle 25,2–35,2 s aus einem Zufall je Router (Java: ein ungesetzter `Random` je Router; der Kern setzt ihn aus Szenario und Router-ID, `ripSeed`; Swift: ein Zufall für alle); eine Nachricht je Schnittstelle ohne die Routen über sie (Split Horizon); eine neue Route oder mehr Hops vom selben Router senden sofort; eine gelernte Route wird erst beim nächsten eigenen Beacon auf 16 gesetzt, wenn sie verfallen ist (Swift: bei jedem Ereignis); Nachrichten von jedem Quellport (Swift: nur 521); ein RIP-Router leitet nur nach seiner Tabelle weiter, wenige Hops vor langer Maske. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, Remote Link, WLAN | | `src/runtime/dns.ts` | Java `filius.software.dns.DNSNachricht`, `Query`, `ResourceRecord` | neu nach Java statt nach Swift (Swift ersetzt Javas Textnachrichten durch `FILIUS-DNS-TYPED/1`): Kopfzeile `ID=… QR=… RCODE=… QDCOUNT=… …`, Frage- und Eintragszeilen, Suche nach Namen ohne Rücksicht auf Groß- und Kleinschreibung, NS der längsten Zone, sonst der Wurzel | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | @@ -31,6 +31,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `test/oracle.test.ts`, `test/scenarios/` | — | neu: 14 Szenarien (davon 2 UDP, 3 TCP, 4 DHCP), Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`); Schritte für UDP, TCP und DHCP; die vier DHCP-Szenarien folgen seit F2.3b Java, die drei TCP-Szenarien seit F2.3c; sie müssen von Swift abweichen; drei weitere (`firewall-and-loss`, `routed-two-networks`, `switch-lan`) folgen seit F2.4b ab der Spur, an der ARP scheitert oder ein Ping unbeantwortet bleibt, Java und stimmen bis dahin mit Swift | | `test/dhcpJava.test.ts`, `test/scenarios/*.java.json`, `test/scenario.ts`, `test/javaRun.ts` | — | neu: die DHCP-Szenarien gegen Java-Filius (`tools/java-oracle`, `DhcpRun`): je Client dieselben Frames in derselben Reihenfolge, Abstände auf 200 ms, dieselben Adressen am Ende | | `test/arpJava.test.ts`, `test/scenarios/arp-routed.scenario.json`, `test/scenarios/arp-java-*.java.json` | — | neu: vier ARP-Abläufe gegen Java-Filius (`ArpRun`): Frames, Abstände und wann jeder Aufruf zurückkehrt; die zwei Pings stimmen seit F2.4b-1, die zwei UDP-Abläufe seit F2.4b-2 (Javas Transport-Thread), drei Abläufe durch den Router seit F2.4b-3 (IP- und ICMP-Thread) | +| `test/ripJava.test.ts`, `test/scenarios/rip-chain.scenario.json`, `test/scenarios/rip-java-*.java.json` | — | neu: zwei RIP-Abläufe gegen Java-Filius (`RipRun`, der Zufall jedes Routers auf `ripSeed` gesetzt): je Absenderkarte dieselben Nachrichten zur selben Zeit, jede Tabelle durch dieselben Zustände, derselbe Ping; die ersten 5 s (Javas gestaffelter Start) nur über den erreichten Zustand | | `test/dnsJava.test.ts`, `test/scenarios/dns-lan.scenario.json`, `test/scenarios/dns-java-*.java.json` | — | neu: fünf DNS-Abfragen gegen Java-Filius (`DnsRun`): Frames, Ergebnis und Dauer; seit F2.4b-1 alle wie Java; seit F2.4c dazu fünf mit rekursivem Server (`dns-recursive.scenario.json`, `dns-recursive-java-*.java.json`), einer nur bis zur Anfrage des Clients verglichen (`JAVA_UNTIL`: Javas Server fragt sich selbst ohne Ende) | | `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: sechs TCP-Abläufe gegen Java-Filius (`TcpRun`, drei davon mit genau einem verlorenen Frame, `loseFirst`): Frames mit Flags und Nummern, Socket-Zustände; benannte Java-Fehler (`JAVA_BUGS`: nur ein SYN, der Zustand bleibt nach dem Schließen stehen), `lostSynAck` nur bis zum zweiten SYN verglichen (`JAVA_UNTIL`) | | `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument | diff --git a/packages/core/src/runtime/random.ts b/packages/core/src/runtime/random.ts index b406cca..b786dcf 100644 --- a/packages/core/src/runtime/random.ts +++ b/packages/core/src/runtime/random.ts @@ -55,9 +55,13 @@ export class JavaCompatibleRandom { return (this.next(26) * 2 ** 27 + this.next(27)) / 2 ** 53; } - /** Java RIP: 30 s · (f/3 + 0.84), truncated — somewhere between 25.2 s and 35.2 s. */ + /** + * Java RIP: 30 s · (f/3 + 0.84), truncated — somewhere between 25.2 s and + * 35.2 s. f/3 divides a float by an int, in float (JLS 5.6.2): one draw in + * about 10,000 comes out a millisecond apart from dividing in double + * (measured on OpenJDK 21). + */ nextRipJitterMilliseconds(): number { - const javaFloat = this.nextFloat(); - return Math.trunc(30_000.0 * (javaFloat / 3.0 + 0.84)); + return Math.trunc(30_000 * (Math.fround(this.nextFloat() / 3) + 0.84)); } } diff --git a/packages/core/test/engine.test.ts b/packages/core/test/engine.test.ts index b222aca..16d04cc 100644 --- a/packages/core/test/engine.test.ts +++ b/packages/core/test/engine.test.ts @@ -431,11 +431,15 @@ describe('UDP sockets', () => { describe('Java-compatible randomness', () => { it('draws the same RIP jitter as java.util.Random', () => { + // Measured on OpenJDK 21 (30.09.2026): (int) (1000 * 30 * (r.nextFloat() / 3 + 0.84)). const random = new JavaCompatibleRandom(42); - expect(Array.from({ length: 5 }, () => random.nextRipJitterMilliseconds())).toEqual([ 32_475, 25_746, 32_032, 25_679, 28_287, ]); + // new Random(7), the 530th draw: 28145 with Java's float division, 28144 in double. + const seven = new JavaCompatibleRandom(7); + for (let draw = 0; draw < 529; draw += 1) seven.nextFloat(); + expect(seven.nextRipJitterMilliseconds()).toBe(28_145); }); it('matches java.util.Random.nextInt for bounded and power-of-two ranges', () => { diff --git a/packages/core/test/ripJava.test.ts b/packages/core/test/ripJava.test.ts new file mode 100644 index 0000000..fc7a9e4 --- /dev/null +++ b/packages/core/test/ripJava.test.ts @@ -0,0 +1,169 @@ +// RIP as Java FILIUS runs it. tools/java-oracle's RipRun ran rip-chain's +// network in Java FILIUS, each router's pauses drawn from the random the core +// seeds (ripSeed), and wrote every frame each card saw, every change of each +// router's RIP table and the story's calls (rip-java-.java.json). The +// engine plays the same story: the frames must come in the same order, with +// the same content, at about the same distance from each other (javaRun.ts); +// each table must go through the same states at about the same times; the +// calls must return the same. +// +// Java starts the routers some 200 ms after the network (ScenarioNetwork +// starts switches and servers first), one after the other; the engine starts +// all at once. The story's calls come at Java's times less that offset, taken +// from the first RIP message (a router's first beacon comes a second after +// its start). Frames that leave different cards within a few milliseconds +// come in Java in the order its threads happen to run, and its packet log +// misses a sender's own frame when several send at once (then the receiver's +// entry, a cable later, counts): the frames are compared per sending card, in +// order, each at about the same time. A table state Java's poll (every 2 ms) +// could not see — the engine's lasts less than STATE_UNSEEN_MILLISECONDS — is +// left out. +// +// The first seconds depend on the order Java's routers start in: one that +// hears a neighbour before its own first beacon sends the learned route with +// it, where the engine's routers, started together, send their own networks +// first and then at once what they learned. Up to STARTUP_MILLISECONDS +// (Java's time) the tables must reach the same state, and frames are not +// compared; after it, every frame and every change. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { RIP_FIRST_BEACON_DELAY_MILLISECONDS, type Id, type IPv4DeliveryResult, type NetworkRuntimeEngine } from '../src/index.js'; +import { GAP_TOLERANCE_MILLISECONDS, javaFrames, watchedEngine, type JavaFrame, type SeenFrame } from './javaRun.js'; +import type { Scenario } from './scenario.js'; + +const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); +const PC_A = '00000000-0000-0000-0000-000000000401'; +const R3 = '00000000-0000-0000-0000-000000000404'; +const STATE_UNSEEN_MILLISECONDS = 5; +const STARTUP_MILLISECONDS = 5_000; + +/** The frames by the card that sent them (the source MAC address), each list in order. */ +function bySender(frames: readonly SeenFrame[], shift: number): Map { + const result = new Map(); + for (const frame of frames) { + const sender = frame.text.slice(0, 17); + const list = result.get(sender) ?? []; + list.push({ time: frame.time - shift, text: frame.text }); + result.set(sender, list); + } + return result; +} + +interface JavaRun { + frames: Record; + events: [number, string][]; + tables: [number, Id, string][]; +} + +/** A ping's result in Java's words: the answer's type and code, or null. */ +function pingResult(result: IPv4DeliveryResult, engine: NetworkRuntimeEngine): string { + if (result.type === 'icmpError') return result.kind === 'destinationNetworkUnreachable' ? 'ICMP 3/0' : `ICMP ${result.kind}`; + const answered = (engine.state.icmpObservationsByNodeId.get(PC_A) ?? []).some((observation) => observation.message.kind === 'echoReply'); + return answered ? 'ICMP 0/0' : 'null'; +} + +type Story = (engine: NetworkRuntimeEngine, at: (javaMilliseconds: number) => void, call: (label: string, run: () => string) => void) => void; + +const STORIES: Record = { + converge: (engine, at, call) => { + at(45_000); + call('sendProbe 10.0.3.2', () => pingResult(engine.sendIcmpEcho(PC_A, '10.0.3.2'), engine)); + }, + expire: (engine, at, call) => { + at(10_000); + // RipRun sets R3's next beacon to never: it sends no more (unless a change sends at once). + engine.state.pendingEvents = engine.state.pendingEvents.filter((event) => !(event.kind.kind === 'ripBeacon' && event.kind.nodeId === R3)); + at(125_000); + call('sendProbe 10.0.3.2', () => pingResult(engine.sendIcmpEcho(PC_A, '10.0.3.2'), engine)); + }, +}; + +/** The table as RipRun writes it: `network/mask gateway interface hops; …`. */ +function tableText(engine: NetworkRuntimeEngine, nodeId: Id): string { + return (engine.state.ripTablesByNodeId.get(nodeId) ?? []) + .map((route) => `${route.destinationNetwork}/${route.subnetMask} ${route.nextHop} ${route.interfaceIPAddress} ${route.metric}`) + .join('; '); +} + +describe('RIP as Java FILIUS runs it', () => { + const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'rip-chain.scenario.json'), 'utf8')) as Scenario; + + for (const [name, story] of Object.entries(STORIES)) { + it(name, () => { + const java = JSON.parse(readFileSync(join(scenarioDirectory, `rip-java-${name}.java.json`), 'utf8')) as JavaRun; + const offset = Math.min(...javaFrames(java.frames).filter((frame) => frame.text.includes(':521 >')).map((frame) => frame.time)) - RIP_FIRST_BEACON_DELAY_MILLISECONDS; + const { engine, frames } = watchedEngine(scenario); + + // Every change of a table, after each event. + const tables: [number, Id, string][] = []; + const routers = [...engine.state.ripTablesByNodeId.keys()]; + const last = new Map(); + const poll = () => { + for (const router of routers) { + const text = tableText(engine, router); + if (last.get(router) === text) continue; + last.set(router, text); + tables.push([engine.state.currentTimeMilliseconds, router, text]); + } + }; + poll(); + const target = engine as unknown as { processScheduledRuntimeEvent: (kind: unknown) => void }; + const process = target.processScheduledRuntimeEvent.bind(engine); + target.processScheduledRuntimeEvent = (kind) => { + process(kind); + poll(); + }; + + const events: [number, string][] = []; + story( + engine, + (javaMilliseconds) => engine.handle({ type: 'advance', toMilliseconds: javaMilliseconds - offset }), + (label, run) => { + events.push([engine.state.currentTimeMilliseconds, `${label} called`]); + const result = run(); + events.push([engine.state.currentTimeMilliseconds, `${label} returned ${result}`]); + }, + ); + const end = Math.max(...Object.values(java.frames).flat().map((frame) => frame.time), ...java.tables.map(([time]) => time)); + engine.handle({ type: 'advance', toMilliseconds: end - offset + 1_000 }); + + const startup = STARTUP_MILLISECONDS - offset; + const expected = bySender(javaFrames(java.frames).filter((frame) => frame.time >= STARTUP_MILLISECONDS), offset); + const actual = bySender(frames().filter((frame) => frame.time >= startup), 0); + expect([...actual.keys()].sort()).toEqual([...expected.keys()].sort()); + for (const [sender, javaList] of expected) { + const engineList = actual.get(sender)!; + for (let index = 0; index < Math.max(javaList.length, engineList.length); index += 1) { + expect(engineList[index]?.text, `${sender} frame #${index + 1}`).toBe(javaList[index]?.text); + expect(Math.abs(engineList[index]!.time - javaList[index]!.time), `${sender} time of frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); + } + } + + // Each router's table: the same state when the startup is over, then the same changes at about the same times. + for (const router of routers) { + const settled = (states: T[], until: number): T[] => { + const before = states.filter(([time]) => time < until); + return [...before.slice(-1), ...states.filter(([time]) => time >= until)]; + }; + const javaStates = settled(java.tables.filter(([, node]) => node === router), STARTUP_MILLISECONDS); + const all = tables.filter(([, node]) => node === router); + const engineStates = settled( + all.filter(([time], index) => index === all.length - 1 || all[index + 1]![0] - time >= STATE_UNSEEN_MILLISECONDS), + startup, + ); + expect(engineStates.map(([, , text]) => text), router).toEqual(javaStates.map(([, , text]) => text)); + for (const [index, [time]] of engineStates.entries()) { + if (index === 0) continue; + expect(Math.abs(time - (javaStates[index]![0] - offset)), `${router} state #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); + } + } + + const javaCalls = java.events.filter(([, event]) => event.startsWith('sendProbe')); + expect(events.map(([, event]) => event)).toEqual(javaCalls.map(([, event]) => event.replace(/ from .*$/, ''))); + expect(Math.abs(events[1]![0] - events[0]![0] - (javaCalls[1]![0] - javaCalls[0]![0])), 'duration of the ping').toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); + }); + } +}); diff --git a/packages/core/test/scenario.ts b/packages/core/test/scenario.ts index 14e4d1f..38cd255 100644 --- a/packages/core/test/scenario.ts +++ b/packages/core/test/scenario.ts @@ -51,6 +51,8 @@ export interface Scenario { { active?: boolean; lower?: string; upper?: string; gateway?: string; dns?: string; own?: boolean; staticAssignments?: [string, string][] } >; dhcpClients?: string[]; + /** Routers with RIP switched on. */ + rip?: string[]; /** Records as `name TYPE target` (Java's hinzuRecord: TTL 3600). */ dnsServers?: Record; steps: { @@ -161,6 +163,7 @@ export function snapshotOf(scenario: Scenario): TopologySnapshot { ]), ), dhcpClientConfigurationsByNodeId: new Map((scenario.dhcpClients ?? []).map((node) => [node, { isEnabled: true }])), + ripEnabledByNodeId: new Map((scenario.rip ?? []).map((node) => [node, true])), switchConfigurationsByNodeId: new Map( Object.entries(scenario.switches ?? {}).map(([node, config]) => [ node, diff --git a/packages/core/test/scenarios/rip-chain.scenario.json b/packages/core/test/scenarios/rip-chain.scenario.json new file mode 100644 index 0000000..7616418 --- /dev/null +++ b/packages/core/test/scenarios/rip-chain.scenario.json @@ -0,0 +1,165 @@ +{ + "seed": 71, + "nodes": [ + { + "id": "00000000-0000-0000-0000-000000000401", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000411", + "label": "eth0" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000402", + "kind": "router", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000421", + "label": "eth0" + }, + { + "id": "00000000-0000-0000-0000-000000000422", + "label": "eth1" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000403", + "kind": "router", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000431", + "label": "eth0" + }, + { + "id": "00000000-0000-0000-0000-000000000432", + "label": "eth1" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000404", + "kind": "router", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000441", + "label": "eth0" + }, + { + "id": "00000000-0000-0000-0000-000000000442", + "label": "eth1" + } + ] + }, + { + "id": "00000000-0000-0000-0000-000000000405", + "kind": "pc", + "ports": [ + { + "id": "00000000-0000-0000-0000-000000000451", + "label": "eth0" + } + ] + } + ], + "links": [ + { + "id": "00000000-0000-0000-0000-00000000F401", + "sourceNode": "00000000-0000-0000-0000-000000000401", + "sourcePort": "00000000-0000-0000-0000-000000000411", + "targetNode": "00000000-0000-0000-0000-000000000402", + "targetPort": "00000000-0000-0000-0000-000000000421" + }, + { + "id": "00000000-0000-0000-0000-00000000F402", + "sourceNode": "00000000-0000-0000-0000-000000000402", + "sourcePort": "00000000-0000-0000-0000-000000000422", + "targetNode": "00000000-0000-0000-0000-000000000403", + "targetPort": "00000000-0000-0000-0000-000000000431" + }, + { + "id": "00000000-0000-0000-0000-00000000F403", + "sourceNode": "00000000-0000-0000-0000-000000000403", + "sourcePort": "00000000-0000-0000-0000-000000000432", + "targetNode": "00000000-0000-0000-0000-000000000404", + "targetPort": "00000000-0000-0000-0000-000000000441" + }, + { + "id": "00000000-0000-0000-0000-00000000F404", + "sourceNode": "00000000-0000-0000-0000-000000000404", + "sourcePort": "00000000-0000-0000-0000-000000000442", + "targetNode": "00000000-0000-0000-0000-000000000405", + "targetPort": "00000000-0000-0000-0000-000000000451" + } + ], + "devices": { + "00000000-0000-0000-0000-000000000401": { + "ip": "10.0.1.2", + "mask": "255.255.255.0", + "gateway": "10.0.1.1" + }, + "00000000-0000-0000-0000-000000000402": { + "ip": "10.0.1.1", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000403": { + "ip": "10.0.12.2", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000404": { + "ip": "10.0.23.3", + "mask": "255.255.255.0" + }, + "00000000-0000-0000-0000-000000000405": { + "ip": "10.0.3.2", + "mask": "255.255.255.0", + "gateway": "10.0.3.1" + } + }, + "interfaces": [ + { + "node": "00000000-0000-0000-0000-000000000402", + "port": "00000000-0000-0000-0000-000000000421", + "ip": "10.0.1.1", + "mask": "255.255.255.0" + }, + { + "node": "00000000-0000-0000-0000-000000000402", + "port": "00000000-0000-0000-0000-000000000422", + "ip": "10.0.12.1", + "mask": "255.255.255.0" + }, + { + "node": "00000000-0000-0000-0000-000000000403", + "port": "00000000-0000-0000-0000-000000000431", + "ip": "10.0.12.2", + "mask": "255.255.255.0" + }, + { + "node": "00000000-0000-0000-0000-000000000403", + "port": "00000000-0000-0000-0000-000000000432", + "ip": "10.0.23.2", + "mask": "255.255.255.0" + }, + { + "node": "00000000-0000-0000-0000-000000000404", + "port": "00000000-0000-0000-0000-000000000441", + "ip": "10.0.23.3", + "mask": "255.255.255.0" + }, + { + "node": "00000000-0000-0000-0000-000000000404", + "port": "00000000-0000-0000-0000-000000000442", + "ip": "10.0.3.1", + "mask": "255.255.255.0" + } + ], + "rip": [ + "00000000-0000-0000-0000-000000000402", + "00000000-0000-0000-0000-000000000403", + "00000000-0000-0000-0000-000000000404" + ], + "steps": [] +} diff --git a/packages/core/test/scenarios/rip-java-converge.java.json b/packages/core/test/scenarios/rip-java-converge.java.json new file mode 100644 index 0000000..2a370a9 --- /dev/null +++ b/packages/core/test/scenarios/rip-java-converge.java.json @@ -0,0 +1,569 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000401": [ + { + "time": 1292, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0" + }, + { + "time": 1355, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1" + }, + { + "time": 1411, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 27401, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 45001, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.1.2", + "targetIp": "10.0.1.1" + }, + { + "time": 45114, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "arp": 2, + "senderIp": "10.0.1.1", + "targetIp": "10.0.1.2" + }, + { + "time": 45116, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:21", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.1.2", + "destinationIp": "10.0.3.2", + "sequence": 1 + }, + { + "time": 45896, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.3.2", + "destinationIp": "10.0.1.2", + "sequence": 1 + }, + { + "time": 58186, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + } + ], + "00000000-0000-0000-0000-000000000402": [ + { + "time": 1296, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1" + }, + { + "time": 1355, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 27345, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 45058, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.1.2", + "targetIp": "10.0.1.1" + }, + { + "time": 45059, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "arp": 2, + "senderIp": "10.0.1.1", + "targetIp": "10.0.1.2" + }, + { + "time": 45173, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:21", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.1.2", + "destinationIp": "10.0.3.2", + "sequence": 1 + }, + { + "time": 45840, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.3.2", + "destinationIp": "10.0.1.2", + "sequence": 1 + }, + { + "time": 58130, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + } + ], + "00000000-0000-0000-0000-000000000403": [ + { + "time": 1292, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 1296, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.3.0 255.255.255.0 1" + }, + { + "time": 1355, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 1411, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 27401, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 29222, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.3.0 255.255.255.0 1" + }, + { + "time": 45228, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.12.1", + "targetIp": "10.0.12.2" + }, + { + "time": 45228, + "source": "02:00:00:00:04:31", + "destination": "02:00:00:00:04:22", + "arp": 2, + "senderIp": "10.0.12.2", + "targetIp": "10.0.12.1" + }, + { + "time": 45339, + "source": "02:00:00:00:04:22", + "destination": "02:00:00:00:04:31", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.1.2", + "destinationIp": "10.0.3.2", + "sequence": 1 + }, + { + "time": 45785, + "source": "02:00:00:00:04:31", + "destination": "02:00:00:00:04:22", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.3.2", + "destinationIp": "10.0.1.2", + "sequence": 1 + }, + { + "time": 58186, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + } + ], + "00000000-0000-0000-0000-000000000404": [ + { + "time": 1217, + "source": "02:00:00:00:04:41", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.3", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.3\n10.0.23.3\n16\n75000\n10.0.3.0 255.255.255.0 0" + }, + { + "time": 1292, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0" + }, + { + "time": 1296, + "source": "02:00:00:00:04:41", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.3", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.3\n10.0.23.3\n16\n75000\n10.0.3.0 255.255.255.0 0" + }, + { + "time": 1355, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.1.0 255.255.255.0 1" + }, + { + "time": 1355, + "source": "02:00:00:00:04:41", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.3", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.3\n10.0.23.3\n16\n75000\n10.0.3.0 255.255.255.0 0" + }, + { + "time": 29277, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.1.0 255.255.255.0 1" + }, + { + "time": 31606, + "source": "02:00:00:00:04:41", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.3", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.3\n10.0.23.3\n16\n75000\n10.0.3.0 255.255.255.0 0" + }, + { + "time": 45395, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.23.2", + "targetIp": "10.0.23.3" + }, + { + "time": 45395, + "source": "02:00:00:00:04:41", + "destination": "02:00:00:00:04:32", + "arp": 2, + "senderIp": "10.0.23.3", + "targetIp": "10.0.23.2" + }, + { + "time": 45506, + "source": "02:00:00:00:04:32", + "destination": "02:00:00:00:04:41", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.1.2", + "destinationIp": "10.0.3.2", + "sequence": 1 + }, + { + "time": 45729, + "source": "02:00:00:00:04:41", + "destination": "02:00:00:00:04:32", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.3.2", + "destinationIp": "10.0.1.2", + "sequence": 1 + }, + { + "time": 57795, + "source": "02:00:00:00:04:41", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.3", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.3\n10.0.23.3\n16\n75000\n10.0.3.0 255.255.255.0 0" + } + ], + "00000000-0000-0000-0000-000000000405": [ + { + "time": 1292, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0" + }, + { + "time": 1352, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.12.0 255.255.255.0 1" + }, + { + "time": 1411, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.12.0 255.255.255.0 1\n10.0.1.0 255.255.255.0 2" + }, + { + "time": 31662, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.12.0 255.255.255.0 1\n10.0.1.0 255.255.255.0 2" + }, + { + "time": 45562, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.3.1", + "targetIp": "10.0.3.2" + }, + { + "time": 45562, + "source": "02:00:00:00:04:51", + "destination": "02:00:00:00:04:42", + "arp": 2, + "senderIp": "10.0.3.2", + "targetIp": "10.0.3.1" + }, + { + "time": 45673, + "source": "02:00:00:00:04:42", + "destination": "02:00:00:00:04:51", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.1.2", + "destinationIp": "10.0.3.2", + "sequence": 1 + }, + { + "time": 45673, + "source": "02:00:00:00:04:51", + "destination": "02:00:00:00:04:42", + "icmp": 0, + "icmpCode": 0, + "sourceIp": "10.0.3.2", + "destinationIp": "10.0.1.2", + "sequence": 1 + }, + { + "time": 57850, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.12.0 255.255.255.0 1\n10.0.1.0 255.255.255.0 2" + } + ] + }, + "events": [ + [ + 213, + "seeded" + ], + [ + 45001, + "sendProbe 10.0.3.2 called" + ], + [ + 45897, + "sendProbe 10.0.3.2 returned ICMP 0/0 from 10.0.3.2" + ] + ], + "tables": [ + [ + 213, + "00000000-0000-0000-0000-000000000402", + "10.0.1.0/255.255.255.0 10.0.1.1 10.0.1.1 0; 10.0.12.0/255.255.255.0 10.0.12.1 10.0.12.1 0" + ], + [ + 213, + "00000000-0000-0000-0000-000000000403", + "10.0.12.0/255.255.255.0 10.0.12.2 10.0.12.2 0; 10.0.23.0/255.255.255.0 10.0.23.2 10.0.23.2 0" + ], + [ + 213, + "00000000-0000-0000-0000-000000000404", + "10.0.23.0/255.255.255.0 10.0.23.3 10.0.23.3 0; 10.0.3.0/255.255.255.0 10.0.3.1 10.0.3.1 0" + ], + [ + 1297, + "00000000-0000-0000-0000-000000000402", + "10.0.1.0/255.255.255.0 10.0.1.1 10.0.1.1 0; 10.0.12.0/255.255.255.0 10.0.12.1 10.0.12.1 0; 10.0.23.0/255.255.255.0 10.0.12.2 10.0.12.1 1" + ], + [ + 1297, + "00000000-0000-0000-0000-000000000403", + "10.0.12.0/255.255.255.0 10.0.12.2 10.0.12.2 0; 10.0.23.0/255.255.255.0 10.0.23.2 10.0.23.2 0; 10.0.1.0/255.255.255.0 10.0.12.1 10.0.12.2 1; 10.0.3.0/255.255.255.0 10.0.23.3 10.0.23.2 1" + ], + [ + 1297, + "00000000-0000-0000-0000-000000000404", + "10.0.23.0/255.255.255.0 10.0.23.3 10.0.23.3 0; 10.0.3.0/255.255.255.0 10.0.3.1 10.0.3.1 0; 10.0.12.0/255.255.255.0 10.0.23.2 10.0.23.3 1" + ], + [ + 1357, + "00000000-0000-0000-0000-000000000402", + "10.0.1.0/255.255.255.0 10.0.1.1 10.0.1.1 0; 10.0.12.0/255.255.255.0 10.0.12.1 10.0.12.1 0; 10.0.23.0/255.255.255.0 10.0.12.2 10.0.12.1 1; 10.0.3.0/255.255.255.0 10.0.12.2 10.0.12.1 2" + ], + [ + 1357, + "00000000-0000-0000-0000-000000000404", + "10.0.23.0/255.255.255.0 10.0.23.3 10.0.23.3 0; 10.0.3.0/255.255.255.0 10.0.3.1 10.0.3.1 0; 10.0.12.0/255.255.255.0 10.0.23.2 10.0.23.3 1; 10.0.1.0/255.255.255.0 10.0.23.2 10.0.23.3 2" + ] + ] +} diff --git a/packages/core/test/scenarios/rip-java-expire.java.json b/packages/core/test/scenarios/rip-java-expire.java.json new file mode 100644 index 0000000..7f36140 --- /dev/null +++ b/packages/core/test/scenarios/rip-java-expire.java.json @@ -0,0 +1,563 @@ +{ + "frames": { + "00000000-0000-0000-0000-000000000401": [ + { + "time": 1293, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0" + }, + { + "time": 1353, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1" + }, + { + "time": 1409, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 27398, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 58184, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 86804, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 16" + }, + { + "time": 113461, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 16" + }, + { + "time": 125001, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.1.2", + "targetIp": "10.0.1.1" + }, + { + "time": 125114, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "arp": 2, + "senderIp": "10.0.1.1", + "targetIp": "10.0.1.2" + }, + { + "time": 125116, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:21", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.1.2", + "destinationIp": "10.0.3.2", + "sequence": 1 + }, + { + "time": 125228, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "icmp": 3, + "icmpCode": 0, + "sourceIp": "10.0.1.1", + "destinationIp": "10.0.1.2", + "sequence": 0 + } + ], + "00000000-0000-0000-0000-000000000402": [ + { + "time": 1297, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1" + }, + { + "time": 1353, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 27343, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 58129, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 2" + }, + { + "time": 86749, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 16" + }, + { + "time": 113405, + "source": "02:00:00:00:04:21", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.1.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.1.1\n10.0.1.1\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.23.0 255.255.255.0 1\n10.0.3.0 255.255.255.0 16" + }, + { + "time": 125058, + "source": "02:00:00:00:04:11", + "destination": "FF:FF:FF:FF:FF:FF", + "arp": 1, + "senderIp": "10.0.1.2", + "targetIp": "10.0.1.1" + }, + { + "time": 125059, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "arp": 2, + "senderIp": "10.0.1.1", + "targetIp": "10.0.1.2" + }, + { + "time": 125173, + "source": "02:00:00:00:04:11", + "destination": "02:00:00:00:04:21", + "icmp": 8, + "icmpCode": 0, + "sourceIp": "10.0.1.2", + "destinationIp": "10.0.3.2", + "sequence": 1 + }, + { + "time": 125173, + "source": "02:00:00:00:04:21", + "destination": "02:00:00:00:04:11", + "icmp": 3, + "icmpCode": 0, + "sourceIp": "10.0.1.1", + "destinationIp": "10.0.1.2", + "sequence": 0 + } + ], + "00000000-0000-0000-0000-000000000403": [ + { + "time": 1218, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0" + }, + { + "time": 1293, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 1297, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.3.0 255.255.255.0 1" + }, + { + "time": 1353, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 1409, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 27398, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 29222, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.3.0 255.255.255.0 1" + }, + { + "time": 58184, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 61273, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.3.0 255.255.255.0 1" + }, + { + "time": 86692, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.3.0 255.255.255.0 16" + }, + { + "time": 86804, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 113461, + "source": "02:00:00:00:04:22", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.1\n10.0.1.1\n16\n75000\n10.0.1.0 255.255.255.0 0" + }, + { + "time": 114340, + "source": "02:00:00:00:04:31", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.12.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.12.2\n10.0.12.2\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.3.0 255.255.255.0 16" + } + ], + "00000000-0000-0000-0000-000000000404": [ + { + "time": 1293, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0" + }, + { + "time": 1297, + "source": "02:00:00:00:04:41", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.3", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.3\n10.0.23.3\n16\n75000\n10.0.3.0 255.255.255.0 0" + }, + { + "time": 1353, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.1.0 255.255.255.0 1" + }, + { + "time": 1354, + "source": "02:00:00:00:04:41", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.3", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.3\n10.0.23.3\n16\n75000\n10.0.3.0 255.255.255.0 0" + }, + { + "time": 29278, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.1.0 255.255.255.0 1" + }, + { + "time": 61329, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.1.0 255.255.255.0 1" + }, + { + "time": 86748, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.1.0 255.255.255.0 1" + }, + { + "time": 114396, + "source": "02:00:00:00:04:32", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.23.2", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.23.2\n10.0.12.2\n16\n75000\n10.0.12.0 255.255.255.0 0\n10.0.1.0 255.255.255.0 1" + } + ], + "00000000-0000-0000-0000-000000000405": [ + { + "time": 1293, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0" + }, + { + "time": 1353, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.12.0 255.255.255.0 1" + }, + { + "time": 1409, + "source": "02:00:00:00:04:42", + "destination": "FF:FF:FF:FF:FF:FF", + "sourceIp": "10.0.3.1", + "destinationIp": "255.255.255.255", + "protocol": 17, + "sourcePort": 521, + "destinationPort": 520, + "payload": "10.0.3.1\n10.0.23.3\n16\n75000\n10.0.23.0 255.255.255.0 0\n10.0.12.0 255.255.255.0 1\n10.0.1.0 255.255.255.0 2" + } + ] + }, + "events": [ + [ + 216, + "seeded" + ], + [ + 10000, + "R3 silent" + ], + [ + 125000, + "sendProbe 10.0.3.2 called" + ], + [ + 125233, + "sendProbe 10.0.3.2 returned ICMP 3/0 from 10.0.1.1" + ] + ], + "tables": [ + [ + 218, + "00000000-0000-0000-0000-000000000402", + "10.0.1.0/255.255.255.0 10.0.1.1 10.0.1.1 0; 10.0.12.0/255.255.255.0 10.0.12.1 10.0.12.1 0" + ], + [ + 218, + "00000000-0000-0000-0000-000000000403", + "10.0.12.0/255.255.255.0 10.0.12.2 10.0.12.2 0; 10.0.23.0/255.255.255.0 10.0.23.2 10.0.23.2 0" + ], + [ + 218, + "00000000-0000-0000-0000-000000000404", + "10.0.23.0/255.255.255.0 10.0.23.3 10.0.23.3 0; 10.0.3.0/255.255.255.0 10.0.3.1 10.0.3.1 0" + ], + [ + 1299, + "00000000-0000-0000-0000-000000000402", + "10.0.1.0/255.255.255.0 10.0.1.1 10.0.1.1 0; 10.0.12.0/255.255.255.0 10.0.12.1 10.0.12.1 0; 10.0.23.0/255.255.255.0 10.0.12.2 10.0.12.1 1" + ], + [ + 1299, + "00000000-0000-0000-0000-000000000403", + "10.0.12.0/255.255.255.0 10.0.12.2 10.0.12.2 0; 10.0.23.0/255.255.255.0 10.0.23.2 10.0.23.2 0; 10.0.1.0/255.255.255.0 10.0.12.1 10.0.12.2 1; 10.0.3.0/255.255.255.0 10.0.23.3 10.0.23.2 1" + ], + [ + 1299, + "00000000-0000-0000-0000-000000000404", + "10.0.23.0/255.255.255.0 10.0.23.3 10.0.23.3 0; 10.0.3.0/255.255.255.0 10.0.3.1 10.0.3.1 0; 10.0.12.0/255.255.255.0 10.0.23.2 10.0.23.3 1" + ], + [ + 1354, + "00000000-0000-0000-0000-000000000402", + "10.0.1.0/255.255.255.0 10.0.1.1 10.0.1.1 0; 10.0.12.0/255.255.255.0 10.0.12.1 10.0.12.1 0; 10.0.23.0/255.255.255.0 10.0.12.2 10.0.12.1 1; 10.0.3.0/255.255.255.0 10.0.12.2 10.0.12.1 2" + ], + [ + 1354, + "00000000-0000-0000-0000-000000000404", + "10.0.23.0/255.255.255.0 10.0.23.3 10.0.23.3 0; 10.0.3.0/255.255.255.0 10.0.3.1 10.0.3.1 0; 10.0.12.0/255.255.255.0 10.0.23.2 10.0.23.3 1; 10.0.1.0/255.255.255.0 10.0.23.2 10.0.23.3 2" + ], + [ + 86694, + "00000000-0000-0000-0000-000000000403", + "10.0.12.0/255.255.255.0 10.0.12.2 10.0.12.2 0; 10.0.23.0/255.255.255.0 10.0.23.2 10.0.23.2 0; 10.0.1.0/255.255.255.0 10.0.12.1 10.0.12.2 1; 10.0.3.0/255.255.255.0 10.0.23.3 10.0.23.2 16" + ], + [ + 86749, + "00000000-0000-0000-0000-000000000402", + "10.0.1.0/255.255.255.0 10.0.1.1 10.0.1.1 0; 10.0.12.0/255.255.255.0 10.0.12.1 10.0.12.1 0; 10.0.23.0/255.255.255.0 10.0.12.2 10.0.12.1 1; 10.0.3.0/255.255.255.0 10.0.12.2 10.0.12.1 16" + ] + ] +} diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index b7d3112..f3f20c0 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -344,3 +344,34 @@ Frames, Abstände und die Zeiten, zu denen die Aufrufe zurückkehren). Die UDP-A seit F2.4b-2: ein Senden kehrt sofort zurück wie in Java (Swifts Senden wartete, bis alle Frames angekommen waren), und je Knoten und Protokoll sendet ein Transport-Thread ein Segment nach dem anderen. + +## RIP zur Laufzeit + +`RipRun` startet das Netz von `rip-chain` (PC 10.0.1.2 – R1 – R2 – R3 – PC +10.0.3.2, jeder Router mit RIP; `ScenarioNetwork` schaltet RIP für die im +Feld `rip` genannten Router ein) und schreibt nach `rip-java-.java.json` +die Frames, jede Änderung jeder RIP-Tabelle (alle 2 ms abgefragt, Zeile +`netz/maske gateway schnittstelle hops; …`) und die Aufrufe. Java würfelt die +Pausen eines Routers aus einem neuen `Random`; `RipRun` setzt jedem Router +per Reflection (`VermittlungsrechnerBetriebssystem.ripbeacon`, +`RIPBeacon.rand`) den Startwert, den der Kern ihm gibt (`ripSeed`: Startwert +des Szenarios, `0x52495000` und FNV-1a der Router-ID). Dann würfeln beide +dieselben Pausen. + +* `converge`: die Tabellen füllen sich, bei 45 s pingt 10.0.1.2 10.0.3.2. +* `expire`: bei 10 s schweigt R3 (`RIPTable.setNextBeacon(Long.MAX_VALUE)`), + bei 125 s der Ping noch einmal. + +Gemessen (30.09.2026, dazu die Pausenformel am JDK): + +* Die Pause ist `(int) (30000 * (rand.nextFloat() / 3 + 0.84))`; `nextFloat() / 3` + rechnet Java in `float`, in etwa einem von 10.000 Fällen eine Millisekunde + anders als in `double`. +* Eine gelernte Route bekommt 16 Hops erst beim nächsten Beacon ihres Routers + nach dem Verfallen (`RIPTable.check`): R3s Netz verfiel bei R2 nach rund + 76,4 s, R2 setzte es bei seinem Beacon um 86,7 s auf 16; R1 hörte die 16 vom + selben Router und meldete sofort. Der Ping bekommt dann ICMP 3/0 von R1. +* Die Router starten nacheinander, rund 20 ms auseinander; wer einen + Nachbarn vor seinem ersten Beacon hört, sendet die gelernte Route gleich + mit. Senden mehrere zugleich, fehlt im Lauscher mancher eigene Frame. + diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index eb2cc0c..0908f7d 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" +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" # 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. @@ -152,6 +152,17 @@ for run in address:5000 referral:5000 unknown:5000 unknownInZone:5000 unknownInO test -s "$output" done +# RIP as Java FILIUS runs it: rip-chain's routers, each with the core's seed +# for its pauses (RipRun); frames, table changes and the ping. Real time. +for run in converge:60000 expire:135000; do + story="${run%%:*}" + output="$root/packages/core/test/scenarios/rip-java-$story.java.json" + rm -f "$output" + HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" RipRun \ + "$root/packages/core/test/scenarios/rip-chain.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true + test -s "$output" +done + # ARP as Java FILIUS runs it: sends to addresses no card has on arp-routed's # network (ArpRun); frames and the calls, for the core's ARP comparison. for story in pingLan pingRouted udpTwice udpBehind routedUdpBehind routedPingBesideUdp routedPingBehindPing; do diff --git a/tools/java-oracle/src/RipRun.java b/tools/java-oracle/src/RipRun.java new file mode 100644 index 0000000..278e288 --- /dev/null +++ b/tools/java-oracle/src/RipRun.java @@ -0,0 +1,157 @@ +// Runs rip-chain's network in Java FILIUS (ScenarioNetwork) and records what +// RIP really does: every frame each card saw, every change of each router's +// RIP table, and the calls of the story. +// +// RipRun +// +// PC 10.0.1.2 - R1 (10.0.1.1, 10.0.12.1) - R2 (10.0.12.2, 10.0.23.2) - +// R3 (10.0.23.3, 10.0.3.1) - PC 10.0.3.2, each router with RIP. Java draws a +// router's pauses from a new Random; this tool gives each the seed the core +// gives it (ripSeed: the scenario's seed, 0x52495000 and the FNV-1a hash of +// the router's id). The stories: +// converge the tables fill; at 45 s 10.0.1.2 pings 10.0.3.2 +// expire at 10 s R3 falls silent (its next beacon never comes); its +// network expires at R2, which tells R1; at 125 s the ping again + +import filius.hardware.knoten.Knoten; +import filius.software.rip.RIPBeacon; +import filius.software.rip.RIPRoute; +import filius.software.rip.RIPTable; +import filius.software.system.VermittlungsrechnerBetriebssystem; +import filius.software.vermittlungsschicht.IcmpPaket; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class RipRun { + static final String PC_A = "00000000-0000-0000-0000-000000000401"; + static final String R3 = "00000000-0000-0000-0000-000000000404"; + + static ScenarioNetwork network; + static final List events = new ArrayList<>(); + static final List tables = new ArrayList<>(); + static final Map lastTables = new LinkedHashMap<>(); + + static synchronized void event(String what) { + events.add(new Object[] { System.currentTimeMillis() - network.start, what }); + } + + /** The table as one line: `network/mask gateway interface hops; …`, in its order. */ + static String tableText(RIPTable table) { + StringBuilder text = new StringBuilder(); + synchronized (table) { + for (RIPRoute route : table.routes) { + if (text.length() > 0) text.append("; "); + text.append(route.getNetAddress()).append('/').append(route.getNetMask()).append(' ').append(route.getGateway()) + .append(' ').append(route.getInterfaceIpAddress()).append(' ').append(route.hops); + } + } + return text.toString(); + } + + static synchronized void pollTables() { + for (Map.Entry entry : network.nodes.entrySet()) { + if (!(entry.getValue().getSystemSoftware() instanceof VermittlungsrechnerBetriebssystem)) continue; + RIPTable table = ((VermittlungsrechnerBetriebssystem) entry.getValue().getSystemSoftware()).getRIPTable(); + if (table == null) continue; + String text = tableText(table); + if (!text.equals(lastTables.get(entry.getKey()))) { + lastTables.put(entry.getKey(), text); + tables.add(new Object[] { System.currentTimeMillis() - network.start, entry.getKey(), text }); + } + } + } + + /** The core's ripSeed. */ + static long ripSeed(long seed, String nodeId) { + int hash = 0x811c9dc5; + for (int index = 0; index < nodeId.length(); index++) { + hash = (hash ^ nodeId.charAt(index)) * 0x01000193; + } + return seed ^ 0x52495000L ^ (hash & 0xffffffffL); + } + + static String icmp(IcmpPaket packet) { + return packet == null ? "null" : "ICMP " + packet.getIcmpType() + "/" + packet.getIcmpCode() + " from " + packet.getSender(); + } + + static void ping(String target) { + event("sendProbe " + target + " called"); + IcmpPaket answer = network.system(PC_A).holeICMP().sendProbe(target, 64, 1, 1); + event("sendProbe " + target + " returned " + icmp(answer)); + } + + static void sleepUntil(long milliseconds) throws InterruptedException { + long rest = network.start + milliseconds - System.currentTimeMillis(); + if (rest > 0) Thread.sleep(rest); + } + + public static void main(String[] args) throws Exception { + network = ScenarioNetwork.read(args[0]); + String story = args[1]; + long end = Long.parseLong(args[2]); + long seed = ScenarioNetwork.seed(args[0]); + network.start(); + Field beaconField = VermittlungsrechnerBetriebssystem.class.getDeclaredField("ripbeacon"); + beaconField.setAccessible(true); + Field randomField = RIPBeacon.class.getDeclaredField("rand"); + randomField.setAccessible(true); + for (Map.Entry entry : network.nodes.entrySet()) { + if (!(entry.getValue().getSystemSoftware() instanceof VermittlungsrechnerBetriebssystem)) continue; + VermittlungsrechnerBetriebssystem os = (VermittlungsrechnerBetriebssystem) entry.getValue().getSystemSoftware(); + if (!os.isRipEnabled()) continue; + randomField.set(beaconField.get(os), new Random(ripSeed(seed, entry.getKey()))); + } + event("seeded"); + Thread poller = new Thread(() -> { + while (true) { + pollTables(); + try { + Thread.sleep(2); + } catch (InterruptedException e) { + return; + } + } + }); + poller.setDaemon(true); + poller.start(); + + switch (story) { + case "converge": + sleepUntil(45_000); + ping("10.0.3.2"); + break; + case "expire": + sleepUntil(10_000); + ((VermittlungsrechnerBetriebssystem) network.system(R3)).getRIPTable().setNextBeacon(Long.MAX_VALUE); + event("R3 silent"); + sleepUntil(125_000); + ping("10.0.3.2"); + break; + default: + throw new IllegalArgumentException("unknown story " + story); + } + sleepUntil(end); + pollTables(); + + ScenarioNetwork.Json out = new ScenarioNetwork.Json(); + out.open('{'); + network.writeFrames(out); + synchronized (RipRun.class) { + out.key("events").open('['); + for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']'); + out.close(']'); + out.key("tables").open('['); + for (Object[] each : tables) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).close(']'); + out.close(']'); + } + out.close('}'); + network.stop(); + ScenarioNetwork.write(args[3], out); + System.exit(0); + } +} diff --git a/tools/java-oracle/src/ScenarioNetwork.java b/tools/java-oracle/src/ScenarioNetwork.java index eff37c8..badd9bd 100644 --- a/tools/java-oracle/src/ScenarioNetwork.java +++ b/tools/java-oracle/src/ScenarioNetwork.java @@ -22,6 +22,7 @@ import filius.software.dhcp.DHCPServer; import filius.software.dns.DNSServer; import filius.software.netzzugangsschicht.EthernetFrame; import filius.software.system.InternetKnotenBetriebssystem; +import filius.software.system.VermittlungsrechnerBetriebssystem; import filius.software.transportschicht.Segment; import filius.software.transportschicht.TcpSegment; import filius.software.vermittlungsschicht.ArpPaket; @@ -58,6 +59,14 @@ public class ScenarioNetwork { return network; } + /** The scenario's seed. */ + @SuppressWarnings("unchecked") + static long seed(String path) throws Exception { + Map scenario = (Map) new JsonReader( + new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value(); + return ((Number) scenario.get("seed")).longValue(); + } + private void build(Map scenario) { Map ports = new HashMap<>(); Map nicsByPort = new HashMap<>(); @@ -156,6 +165,9 @@ public class ScenarioNetwork { for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) { system(nodes.get((String) each)).setDHCPKonfiguration(true); } + for (Object each : list(scenario.getOrDefault("rip", new ArrayList<>()))) { + ((VermittlungsrechnerBetriebssystem) system(nodes.get((String) each))).setRipEnabled(true); + } }