feat(core): ARP after Java, measured with Java FILIUS

An ARP lookup asks, waits a round trip, asks once more and gives up after
another round trip; then host unreachable, observed at the sender itself or
sent back by the router (for ICMP only for an echo request). A waiting
sender wakes after what else happens at that time. A ping then waits a
round trip for its answer, counted from when it left. Swift asks twice at
once while events run and lets the packet wait without end when the
address exists anywhere in the network; the option swiftArp keeps that for
the Swift comparison, and three scenarios are compared with Swift until the
trace where ARP fails or a ping goes unanswered.

arpJava.test.ts compares ArpRun: both pings match; the two UDP stories wait
for Java's transport thread (next step). DNS without a server now matches
Java too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:11:23 +00:00
co-authored by Claude Opus 5.5
parent 67fb608412
commit dbbc1fd70d
8 changed files with 414 additions and 152 deletions
+3 -2
View File
@@ -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/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/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/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. Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht), 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. 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; noch ohne Rekursion. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, 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. Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht), 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. 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; noch ohne Rekursion. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, 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/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/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
@@ -28,8 +28,9 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `test/virtualFileSystem.test.ts`, `test/vfs.*.json` | `TopologyProjectPersistenceTests.swift`, `TopologyEditorReducerTests.swift` (drei Dateisystem-Tests, als Fälle) | übertragen und erweitert: 11 Fälle, Ergebnis aus dem Swift-Kern | | `test/virtualFileSystem.test.ts`, `test/vfs.*.json` | `TopologyProjectPersistenceTests.swift`, `TopologyEditorReducerTests.swift` (drei Dateisystem-Tests, als Fälle) | übertragen und erweitert: 11 Fälle, Ergebnis aus dem Swift-Kern |
| `test/strings.test.ts`, `test/strings.*.json` | — | neu: Fälle und ganze Zeichenmengen, Ergebnis aus dem Swift-Kern | | `test/strings.test.ts`, `test/strings.*.json` | — | neu: Fälle und ganze Zeichenmengen, Ergebnis aus dem Swift-Kern |
| `test/engine.test.ts` | `TopologyNetworkRuntimeEngineTests.swift` (21 von 72 Tests) | übertragen; `nextInt`- und `nextDouble`-Referenzwerte an OpenJDK 21 gemessen; die DHCP-Client-Tests seit F2.3b nach Javas Verhalten (neun, davon zwei benannte Java-Fehler, die der Kern nicht übernimmt) | | `test/engine.test.ts` | `TopologyNetworkRuntimeEngineTests.swift` (21 von 72 Tests) | übertragen; `nextInt`- und `nextDouble`-Referenzwerte an OpenJDK 21 gemessen; die DHCP-Client-Tests seit F2.3b nach Javas Verhalten (neun, davon zwei benannte Java-Fehler, die der Kern nicht übernimmt) |
| `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 | | `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/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`); die zwei Pings stimmen, die zwei UDP-Abläufe warten auf Javas Transport-Thread |
| `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; eine noch nicht wie Java (fehlender Server: Javas ARP) | | `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; eine noch nicht wie Java (fehlender Server: Javas ARP) |
| `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: drei TCP-Abläufe gegen Java-Filius (`TcpRun`): Frames mit Flags und Nummern, Socket-Zustände; ein benannter Java-Fehler (`JAVA_BUGS`: nur ein SYN) | | `test/tcpJava.test.ts`, `test/scenarios/tcp-java-*.java.json` | — | neu: drei TCP-Abläufe gegen Java-Filius (`TcpRun`): Frames mit Flags und Nummern, Socket-Zustände; ein benannter Java-Fehler (`JAVA_BUGS`: nur ein SYN) |
| `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument | | `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
+231 -135
View File
@@ -17,6 +17,9 @@
// processScheduledRuntimeEvent, dropForGlobalPacketLoss (lines ~4753–4850) // processScheduledRuntimeEvent, dropForGlobalPacketLoss (lines ~4753–4850)
// A cable carries one frame after the other, as Java's SimplexVerbindung // A cable carries one frame after the other, as Java's SimplexVerbindung
// (Swift: all at once; the option swiftCables keeps that for the comparison). // (Swift: all at once; the option swiftCables keeps that for the comparison).
// ARP after Java's holeARPTabellenEintrag: a lookup asks twice, a round trip
// each, then host unreachable (Swift: all at once, or without end when the
// address exists anywhere; the option swiftArp keeps that for the comparison).
// DNS after Java (filius.software.dns: DNSServer, DNSServerMitarbeiter, // DNS after Java (filius.software.dns: DNSServer, DNSServerMitarbeiter,
// Resolver, DNSQueryAgent), not Swift's typed format; recursion not yet. // Resolver, DNSQueryAgent), not Swift's typed format; recursion not yet.
// Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht, // Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht,
@@ -52,7 +55,6 @@ import {
firewallConfiguration, firewallConfiguration,
interfaceKey, interfaceKey,
} from './config.js'; } from './config.js';
import { deepEqual } from '../util/equal.js';
import { maskAddress, parseIPv4, sameSubnet } from './ipv4.js'; import { maskAddress, parseIPv4, sameSubnet } from './ipv4.js';
import { import {
forwardingClone, forwardingClone,
@@ -120,6 +122,10 @@ export type ScheduledEventKind =
| { readonly kind: 'ripBeacon'; readonly nodeId: Id } | { readonly kind: 'ripBeacon'; readonly nodeId: Id }
| { readonly kind: 'dhcpClientStart'; readonly nodeId: Id } | { readonly kind: 'dhcpClientStart'; readonly nodeId: Id }
| { readonly kind: 'dhcpTimeout'; readonly nodeId: Id } | { readonly kind: 'dhcpTimeout'; readonly nodeId: Id }
/** An ARP lookup waited a round trip for its answer. */
| { readonly kind: 'arpTimeout'; readonly lookupId: number }
/** The lookups waiting for an address wake up, after what else happens at that time. */
| { readonly kind: 'arpAnswered'; readonly nodeId: Id; readonly ipAddress: string; readonly macAddress: string }
| { readonly kind: 'tcpTimeout'; readonly sessionId: Id } | { readonly kind: 'tcpTimeout'; readonly sessionId: Id }
| { | {
/** A frame that waited in the port's output buffer leaves: the cable was busy. */ /** A frame that waited in the port's output buffer leaves: the cable was busy. */
@@ -522,10 +528,26 @@ type PacketRoute =
| { readonly type: 'egress'; readonly interface: NetworkInterface; readonly nextHop: string } | { readonly type: 'egress'; readonly interface: NetworkInterface; readonly nextHop: string }
| { readonly type: 'unavailable' }; | { readonly type: 'unavailable' };
interface PendingIPv4Transmission { /** Java's IP asks ARP this many times, a round trip each (holeARPTabellenEintrag(ziel, 2, …)). */
readonly packet: IPv4Packet; export const ARP_ATTEMPTS = 2;
readonly interface: NetworkInterface;
readonly operation: TraceOperation; interface ArpLookup {
readonly id: number;
readonly nodeId: Id;
readonly ipAddress: string;
readonly networkInterface: NetworkInterface;
attemptsLeft: number;
/** Swift, for the comparison: a lookup made while events run waits in a list emptied by the answer itself. */
readonly swiftQueued: boolean;
readonly done: (macAddress: string | undefined) => void;
}
/** What a sender learns when its packet has left, or when it could not. */
interface IPv4SendCallbacks {
/** The packet left (or failed) — at once, or when ARP has answered or given up. */
readonly onFinished?: (result: IPv4DeliveryResult) => void;
/** ARP found no one for the next hop (Java: the sender sends host unreachable). */
readonly onUnresolved?: () => void;
} }
function toSeed(seed: bigint | number): bigint { function toSeed(seed: bigint | number): bigint {
@@ -534,12 +556,14 @@ function toSeed(seed: bigint | number): bigint {
/** A departure waits for the delivery of the frame before it, so draining the deliveries reaches it. */ /** A departure waits for the delivery of the frame before it, so draining the deliveries reaches it. */
function isNetworkDeliveryEvent(kind: ScheduledEventKind): boolean { function isNetworkDeliveryEvent(kind: ScheduledEventKind): boolean {
return kind.kind === 'ethernetLinkDeparture' || kind.kind === 'ethernetLinkDelivery' || kind.kind === 'remoteLinkDelivery'; return (
kind.kind === 'ethernetLinkDeparture' ||
kind.kind === 'ethernetLinkDelivery' ||
kind.kind === 'remoteLinkDelivery' ||
kind.kind === 'arpAnswered'
);
} }
function isRemoteLinkWaitEvent(kind: ScheduledEventKind): boolean {
return isNetworkDeliveryEvent(kind) || kind.kind === 'lanRemoteLinkTransmission';
}
function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V { function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V {
let value = map.get(key); let value = map.get(key);
@@ -562,10 +586,13 @@ export class NetworkRuntimeEngine {
private runtimeSpeed: RuntimeSpeed; private runtimeSpeed: RuntimeSpeed;
private readonly packetTraceRetentionPolicy: TraceRetentionPolicy; private readonly packetTraceRetentionPolicy: TraceRetentionPolicy;
private isProcessingScheduledEvents = false; private isProcessingScheduledEvents = false;
private pendingIPv4TransmissionsByArpKey = new Map<string, PendingIPv4Transmission[]>(); /** ARP lookups waiting for their answer (Java: a thread in holeARPTabellenEintrag). */
private arpLookups: ArpLookup[] = [];
private nextArpLookupId = 1;
/** Per cable end (node and port): when the last frame sent there arrives. */ /** Per cable end (node and port): when the last frame sent there arrives. */
private cableBusyUntilByPort = new Map<string, number>(); private cableBusyUntilByPort = new Map<string, number>();
private readonly cablesCarryOneFrameAtATime: boolean; private readonly cablesCarryOneFrameAtATime: boolean;
private readonly swiftArp: boolean;
/** /**
* `swiftCables`: a cable carries all frames at once, as in the Swift * `swiftCables`: a cable carries all frames at once, as in the Swift
@@ -577,9 +604,10 @@ export class NetworkRuntimeEngine {
constructor( constructor(
seed: bigint | number = DEFAULT_SEED, seed: bigint | number = DEFAULT_SEED,
packetTraceRetentionPolicy: TraceRetentionPolicy = PRODUCTION_TRACE_RETENTION, packetTraceRetentionPolicy: TraceRetentionPolicy = PRODUCTION_TRACE_RETENTION,
options: { readonly swiftCables?: boolean } = {}, options: { readonly swiftCables?: boolean; readonly swiftArp?: boolean } = {},
) { ) {
this.cablesCarryOneFrameAtATime = options.swiftCables !== true; this.cablesCarryOneFrameAtATime = options.swiftCables !== true;
this.swiftArp = options.swiftArp === true;
const normalizedSeed = toSeed(seed); const normalizedSeed = toSeed(seed);
this.stateValue = initialState(normalizedSeed); this.stateValue = initialState(normalizedSeed);
this.random = new JavaCompatibleRandom(normalizedSeed); this.random = new JavaCompatibleRandom(normalizedSeed);
@@ -626,7 +654,6 @@ export class NetworkRuntimeEngine {
this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n); this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n);
this.dnsIdRandom = new JavaCompatibleRandom(seed ^ 0x444e_5349n); this.dnsIdRandom = new JavaCompatibleRandom(seed ^ 0x444e_5349n);
this.isProcessingScheduledEvents = false; this.isProcessingScheduledEvents = false;
this.pendingIPv4TransmissionsByArpKey.clear();
this.cableBusyUntilByPort.clear(); this.cableBusyUntilByPort.clear();
this.initializeRipState(); this.initializeRipState();
this.initializeDhcpState(); this.initializeDhcpState();
@@ -641,7 +668,7 @@ export class NetworkRuntimeEngine {
state.phase = 'stopped'; state.phase = 'stopped';
state.globalPacketLossEnabled = false; state.globalPacketLossEnabled = false;
this.isProcessingScheduledEvents = false; this.isProcessingScheduledEvents = false;
this.pendingIPv4TransmissionsByArpKey.clear(); this.arpLookups = [];
state.pendingEvents = []; state.pendingEvents = [];
state.arpCachesByNodeId.clear(); state.arpCachesByNodeId.clear();
state.switchForwardingTablesByNodeId.clear(); state.switchForwardingTablesByNodeId.clear();
@@ -834,6 +861,12 @@ export class NetworkRuntimeEngine {
case 'dhcpTimeout': case 'dhcpTimeout':
this.handleDhcpClientTimeout(kind.nodeId); this.handleDhcpClientTimeout(kind.nodeId);
return; return;
case 'arpTimeout':
this.handleArpTimeout(kind.lookupId);
return;
case 'arpAnswered':
this.completeArpLookups(kind.nodeId, kind.ipAddress, kind.macAddress);
return;
case 'tcpTimeout': case 'tcpTimeout':
this.handleTcpTimeout(kind.sessionId); this.handleTcpTimeout(kind.sessionId);
return; return;
@@ -2651,15 +2684,15 @@ export class NetworkRuntimeEngine {
/** /**
* The MAC address for `targetIPAddress`, asking by ARP broadcast when it is * The MAC address for `targetIPAddress`, asking by ARP broadcast when it is
* not cached. Blocks like Java: the frames travel (virtual time passes) * not cached. Blocks like Java: each request waits a round trip for its
* before the call returns. * answer (virtual time passes) before the next, or before the call gives up.
*/ */
resolveMacAddress( resolveMacAddress(
nodeId: Id, nodeId: Id,
targetIPAddress: string, targetIPAddress: string,
options: { preferredInterfaceId?: Id | undefined; maxRetries?: number; selectInterfaceByAddress?: boolean } = {}, options: { preferredInterfaceId?: Id | undefined; maxRetries?: number; selectInterfaceByAddress?: boolean } = {},
): string | undefined { ): string | undefined {
const maxRetries = options.maxRetries ?? 2; const maxRetries = options.maxRetries ?? ARP_ATTEMPTS;
const selectInterfaceByAddress = options.selectInterfaceByAddress ?? true; const selectInterfaceByAddress = options.selectInterfaceByAddress ?? true;
const interfaces = this.networkInterfaces(nodeId); const interfaces = this.networkInterfaces(nodeId);
if (interfaces.length === 0) return undefined; if (interfaces.length === 0) return undefined;
@@ -2676,13 +2709,76 @@ export class NetworkRuntimeEngine {
} }
if (selectedInterface === undefined) return undefined; if (selectedInterface === undefined) return undefined;
for (let attempt = 0; attempt < Math.max(0, maxRetries); attempt += 1) { let finished = false;
this.sendArpRequest(nodeId, selectedInterface, targetIPAddress); let resolved: string | undefined;
this.advanceRemoteLinkEventsUntilArpCacheResolves(nodeId, targetIPAddress); this.lookUpMacAddress(nodeId, targetIPAddress, selectedInterface, maxRetries, (macAddress) => {
const resolved = this.stateValue.arpCachesByNodeId.get(nodeId)?.get(targetIPAddress); finished = true;
if (resolved !== undefined) return resolved.macAddress; resolved = macAddress;
});
if (!finished && !this.isProcessingScheduledEvents) {
this.advanceUntil(() => finished, this.stateValue.currentTimeMilliseconds + (maxRetries + 1) * this.runtimeSpeed.roundTripTimeMilliseconds);
} }
return undefined; return resolved;
}
/**
* Java's holeARPTabellenEintrag as a lookup of its own: a known address at
* once; otherwise an ARP request and a round trip's wait for the answer,
* `attempts` times, then undefined. Java wakes on any change of the ARP
* table and gives up when it finds nothing; the lookup waits for its
* address (as the DHCP client's check does).
*/
private lookUpMacAddress(
nodeId: Id,
ipAddress: string,
networkInterface: NetworkInterface,
attempts: number,
done: (macAddress: string | undefined) => void,
): void {
const known = this.knownMacAddress(nodeId, ipAddress);
if (known !== undefined || attempts <= 0) {
done(known);
return;
}
const swiftQueued = this.swiftArp && this.isProcessingScheduledEvents;
const lookup: ArpLookup = { id: this.nextArpLookupId++, nodeId, ipAddress, networkInterface, attemptsLeft: attempts - 1, swiftQueued, done };
this.arpLookups.push(lookup);
if (swiftQueued) {
// Swift, only for the comparison: every request at once, then the packet waits without end.
for (let attempt = 0; attempt < attempts; attempt += 1) this.sendArpRequest(nodeId, networkInterface, ipAddress);
lookup.attemptsLeft = 0;
return;
}
this.schedule(this.stateValue.currentTimeMilliseconds + this.runtimeSpeed.roundTripTimeMilliseconds, { kind: 'arpTimeout', lookupId: lookup.id });
this.sendArpRequest(nodeId, networkInterface, ipAddress);
}
/** The node learned `ipAddress`: every lookup waiting for it has its answer. */
private completeArpLookups(nodeId: Id, ipAddress: string, macAddress: string, onlySwiftQueued = false): void {
const answered = this.arpLookups.filter(
(lookup) => lookup.nodeId === nodeId && lookup.ipAddress === ipAddress && (!onlySwiftQueued || lookup.swiftQueued),
);
if (answered.length === 0) return;
const ids = new Set(answered.map((lookup) => lookup.id));
this.arpLookups = this.arpLookups.filter((lookup) => !ids.has(lookup.id));
this.stateValue.pendingEvents = this.stateValue.pendingEvents.filter(
(event) => !(event.kind.kind === 'arpTimeout' && ids.has(event.kind.lookupId)),
);
for (const lookup of answered) lookup.done(macAddress);
}
/** No answer within the round trip: ask again, or give up. */
private handleArpTimeout(lookupId: number): void {
const lookup = this.arpLookups.find((each) => each.id === lookupId);
if (lookup === undefined) return;
if (lookup.attemptsLeft > 0) {
lookup.attemptsLeft -= 1;
this.schedule(this.stateValue.currentTimeMilliseconds + this.runtimeSpeed.roundTripTimeMilliseconds, { kind: 'arpTimeout', lookupId });
this.sendArpRequest(lookup.nodeId, lookup.networkInterface, lookup.ipAddress);
return;
}
this.arpLookups = this.arpLookups.filter((each) => each.id !== lookupId);
lookup.done(undefined);
} }
/** The MAC address for `ipAddress` without asking: localhost, one of the node's own, or cached. */ /** The MAC address for `ipAddress` without asking: localhost, one of the node's own, or cached. */
@@ -2724,39 +2820,6 @@ export class NetworkRuntimeEngine {
this.sendEthernetFrame(nodeId, networkInterface.portId, frame); this.sendEthernetFrame(nodeId, networkInterface.portId, frame);
} }
private advanceRemoteLinkEventsUntilArpCacheResolves(nodeId: Id, targetIPAddress: string): void {
if (this.isProcessingScheduledEvents) return;
this.advancePendingRemoteLinkEvents(
() => this.stateValue.arpCachesByNodeId.get(nodeId)?.get(targetIPAddress) !== undefined,
);
}
private advanceRemoteLinkEventsUntilIcmpObservation(
nodeId: Id,
observationCount: number,
identifier: number,
sequenceNumber: number,
): void {
if (this.isProcessingScheduledEvents) return;
this.advancePendingRemoteLinkEvents(() => {
const newObservations = (this.stateValue.icmpObservationsByNodeId.get(nodeId) ?? []).slice(observationCount);
return newObservations.some(
(observation) =>
observation.message.identifier === identifier && observation.message.sequenceNumber === sequenceNumber,
);
});
}
private advancePendingRemoteLinkEvents(condition: () => boolean): void {
const maximumEvents = 1_024;
let processedEventCount = 0;
while (!condition() && processedEventCount < maximumEvents) {
const nextDeadline = this.earliestDeadline(isRemoteLinkWaitEvent);
if (nextDeadline === undefined) return;
this.advance(nextDeadline);
processedEventCount += 1;
}
}
private earliestDeadline(filter: (kind: ScheduledEventKind) => boolean): number | undefined { private earliestDeadline(filter: (kind: ScheduledEventKind) => boolean): number | undefined {
let earliest: number | undefined; let earliest: number | undefined;
@@ -3086,7 +3149,16 @@ export class NetworkRuntimeEngine {
macAddress: packet.senderMacAddress, macAddress: packet.senderMacAddress,
updatedAtMilliseconds: this.stateValue.currentTimeMilliseconds, updatedAtMilliseconds: this.stateValue.currentTimeMilliseconds,
}); });
this.flushIPv4TransmissionsAwaitingArp(nodeId, packet.senderIPAddress, packet.senderMacAddress); // Java's waiting thread wakes when the ARP thread is done (Swift, for the comparison: at once).
this.completeArpLookups(nodeId, packet.senderIPAddress, packet.senderMacAddress, true);
if (this.arpLookups.some((lookup) => lookup.nodeId === nodeId && lookup.ipAddress === packet.senderIPAddress)) {
this.schedule(this.stateValue.currentTimeMilliseconds, {
kind: 'arpAnswered',
nodeId,
ipAddress: packet.senderIPAddress,
macAddress: packet.senderMacAddress,
});
}
this.dhcpClientLearnedAddress(nodeId, packet.senderIPAddress); this.dhcpClientLearnedAddress(nodeId, packet.senderIPAddress);
} }
this.recordTrace({ this.recordTrace({
@@ -3191,8 +3263,27 @@ export class NetworkRuntimeEngine {
detail: 'ICMP echo request', detail: 'ICMP echo request',
}); });
const observationCount = this.stateValue.icmpObservationsByNodeId.get(fromNodeId)?.length ?? 0; const observationCount = this.stateValue.icmpObservationsByNodeId.get(fromNodeId)?.length ?? 0;
const result = this.sendIPv4Packet(fromNodeId, packet, sourceInterface.portId); const answered = () =>
this.advanceRemoteLinkEventsUntilIcmpObservation(fromNodeId, observationCount, identifier, sequenceNumber); (this.stateValue.icmpObservationsByNodeId.get(fromNodeId) ?? [])
.slice(observationCount)
.some((observation) => observation.message.identifier === identifier && observation.message.sequenceNumber === sequenceNumber);
let sent: IPv4DeliveryResult | undefined;
let sentAt = this.stateValue.currentTimeMilliseconds;
const handedOn = this.sendIPv4PacketWithOptions(fromNodeId, packet, sourceInterface.portId, true, 'sent', {
onFinished: (outcome) => {
sent = outcome;
sentAt = this.stateValue.currentTimeMilliseconds;
},
});
if (!this.isProcessingScheduledEvents) {
// Java's sendProbe: the caller waits for ARP (up to two round trips),
// then a round trip for the answer (ICMPThread.waitForIcmpResponse),
// counted from when the packet left — the engine may have run past it.
const rtt = this.runtimeSpeed.roundTripTimeMilliseconds;
this.advanceUntil(() => sent !== undefined, this.stateValue.currentTimeMilliseconds + (ARP_ATTEMPTS + 1) * rtt);
if (sentAt + rtt > this.stateValue.currentTimeMilliseconds) this.advanceUntil(answered, sentAt + rtt);
}
const result = sent ?? handedOn;
const newObservations = (this.stateValue.icmpObservationsByNodeId.get(fromNodeId) ?? []).slice(observationCount); const newObservations = (this.stateValue.icmpObservationsByNodeId.get(fromNodeId) ?? []).slice(observationCount);
const observation = newObservations.findLast( const observation = newObservations.findLast(
(candidate) => candidate.message.identifier === identifier && candidate.message.sequenceNumber === sequenceNumber, (candidate) => candidate.message.identifier === identifier && candidate.message.sequenceNumber === sequenceNumber,
@@ -3254,8 +3345,78 @@ export class NetworkRuntimeEngine {
preferredInterfaceId: Id | undefined, preferredInterfaceId: Id | undefined,
allowIcmpErrorGeneration: boolean, allowIcmpErrorGeneration: boolean,
operation: TraceOperation, operation: TraceOperation,
callbacks: IPv4SendCallbacks = {},
): IPv4DeliveryResult { ): IPv4DeliveryResult {
const route = this.routeForPacket(fromNodeId, packet.receiverIPAddress, preferredInterfaceId); const route = this.routeForPacket(fromNodeId, packet.receiverIPAddress, preferredInterfaceId);
if (route.type === 'egress') return this.sendIPv4ToNextHop(fromNodeId, packet, route, allowIcmpErrorGeneration, operation, callbacks);
const result = this.sendIPv4Directly(fromNodeId, packet, route, preferredInterfaceId, allowIcmpErrorGeneration, operation);
callbacks.onFinished?.(result);
return result;
}
/**
* To the next hop, once ARP knows it; until then the packet waits for its
* lookup and the result says it was handed on. When ARP finds no one: host
* unreachable — observed at the sender itself, or sent back by the router
* (onUnresolved).
*/
private sendIPv4ToNextHop(
fromNodeId: Id,
packet: IPv4Packet,
route: { readonly interface: NetworkInterface; readonly nextHop: string },
allowIcmpErrorGeneration: boolean,
operation: TraceOperation,
callbacks: IPv4SendCallbacks,
): IPv4DeliveryResult {
let finished: IPv4DeliveryResult | undefined;
this.lookUpMacAddress(fromNodeId, route.nextHop, route.interface, ARP_ATTEMPTS, (macAddress) => {
if (macAddress !== undefined) {
this.transmitIPv4Packet(fromNodeId, packet, route.interface, macAddress, operation);
finished = { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
} else {
this.recordTrace({
packetIdentity: packet.identity,
nodeId: fromNodeId,
interfaceId: route.interface.portId,
direction: 'outbound',
layer: 'network',
operation: 'dropped',
beforeHeaders: ipv4Headers(packet),
detail: `no ARP answer for ${route.nextHop}`,
});
if (allowIcmpErrorGeneration) {
this.observeLocalIcmpError(fromNodeId, route.interface.portId, 'destinationHostUnreachable', packet);
finished = { type: 'icmpError', packetIdentity: packet.identity, nodeId: fromNodeId, kind: 'destinationHostUnreachable' };
} else {
callbacks.onUnresolved?.();
finished = { type: 'dropped', packetIdentity: packet.identity, nodeId: fromNodeId };
}
}
callbacks.onFinished?.(finished);
});
if (finished === undefined && this.swiftArp && this.isProcessingScheduledEvents) {
this.recordTrace({
packetIdentity: packet.identity,
nodeId: fromNodeId,
interfaceId: route.interface.portId,
direction: 'outbound',
layer: 'network',
operation: 'created',
beforeHeaders: ipv4Headers(packet),
detail: `waiting for ARP resolution of ${route.nextHop}`,
});
}
return finished ?? { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
}
private sendIPv4Directly(
fromNodeId: Id,
packet: IPv4Packet,
route: PacketRoute,
preferredInterfaceId: Id | undefined,
allowIcmpErrorGeneration: boolean,
operation: TraceOperation,
): IPv4DeliveryResult {
switch (route.type) { switch (route.type) {
case 'local': { case 'local': {
const networkInterface = route.interface ?? this.networkInterfaces(fromNodeId)[0]; const networkInterface = route.interface ?? this.networkInterfaces(fromNodeId)[0];
@@ -3281,30 +3442,8 @@ export class NetworkRuntimeEngine {
return { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId }; return { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
} }
case 'egress': { case 'egress':
const destinationMacAddress = this.resolveMacAddress(fromNodeId, route.nextHop, { return this.sendIPv4ToNextHop(fromNodeId, packet, route, allowIcmpErrorGeneration, operation, {});
preferredInterfaceId: route.interface.portId,
maxRetries: 2,
});
if (destinationMacAddress === undefined) {
if (this.isProcessingScheduledEvents && this.hasConfiguredInterface(route.nextHop)) {
this.enqueueIPv4TransmissionAwaitingArp(fromNodeId, route.nextHop, packet, route.interface, operation);
return { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
}
if (allowIcmpErrorGeneration) {
this.observeLocalIcmpError(fromNodeId, route.interface.portId, 'destinationHostUnreachable', packet);
return {
type: 'icmpError',
packetIdentity: packet.identity,
nodeId: fromNodeId,
kind: 'destinationHostUnreachable',
};
}
return { type: 'dropped', packetIdentity: packet.identity, nodeId: fromNodeId };
}
this.transmitIPv4Packet(fromNodeId, packet, route.interface, destinationMacAddress, operation);
return { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
}
case 'unavailable': case 'unavailable':
if (allowIcmpErrorGeneration) { if (allowIcmpErrorGeneration) {
@@ -3320,46 +3459,6 @@ export class NetworkRuntimeEngine {
} }
} }
private hasConfiguredInterface(ipAddress: string): boolean {
return this.stateValue.topologySnapshot.nodes.some((node) =>
this.networkInterfaces(node.id).some((candidate) => candidate.ipAddress === ipAddress),
);
}
private enqueueIPv4TransmissionAwaitingArp(
fromNodeId: Id,
nextHop: string,
packet: IPv4Packet,
networkInterface: NetworkInterface,
operation: TraceOperation,
): void {
const key = arpKey(fromNodeId, nextHop);
const transmission: PendingIPv4Transmission = { packet, interface: networkInterface, operation };
const pending = this.pendingIPv4TransmissionsByArpKey.get(key);
if (pending?.some((candidate) => deepEqual(candidate, transmission)) === true) return;
getOrCreate(this.pendingIPv4TransmissionsByArpKey, key, () => []).push(transmission);
this.recordTrace({
packetIdentity: packet.identity,
nodeId: fromNodeId,
interfaceId: networkInterface.portId,
direction: 'outbound',
layer: 'network',
operation: 'created',
beforeHeaders: ipv4Headers(packet),
detail: `waiting for ARP resolution of ${nextHop}`,
});
}
private flushIPv4TransmissionsAwaitingArp(nodeId: Id, ipAddress: string, macAddress: string): void {
const key = arpKey(nodeId, ipAddress);
const transmissions = this.pendingIPv4TransmissionsByArpKey.get(key);
if (transmissions === undefined) return;
this.pendingIPv4TransmissionsByArpKey.delete(key);
for (const transmission of transmissions) {
this.transmitIPv4Packet(nodeId, transmission.packet, transmission.interface, macAddress, transmission.operation);
}
}
private transmitIPv4Packet( private transmitIPv4Packet(
fromNodeId: Id, fromNodeId: Id,
packet: IPv4Packet, packet: IPv4Packet,
@@ -3451,15 +3550,16 @@ export class NetworkRuntimeEngine {
afterHeaders: ipv4Headers(forwarded), afterHeaders: ipv4Headers(forwarded),
detail: 'TTL decremented', detail: 'TTL decremented',
}); });
const result = this.sendIPv4PacketWithOptions(nodeId, forwarded, undefined, false, 'forwarded'); // Java: a router whose ARP finds no one answers host unreachable; for
if (result.type === 'dropped') { // ICMP only an echo request (ICMP.sendUnicastToNextHop).
const route = this.routeForPacket(nodeId, packetForProcessing.receiverIPAddress); const echoOrNotIcmp = packet.payload.kind !== 'icmp' || packet.payload.message.kind === 'echoRequest';
this.emitIcmpError( const result = this.sendIPv4PacketWithOptions(nodeId, forwarded, undefined, false, 'forwarded', {
nodeId, onUnresolved: () => {
networkInterface, if (echoOrNotIcmp) this.emitIcmpError(nodeId, networkInterface, 'destinationHostUnreachable', packet);
route.type === 'unavailable' ? 'destinationNetworkUnreachable' : 'destinationHostUnreachable', },
packet, });
); if (result.type === 'dropped' && this.routeForPacket(nodeId, packetForProcessing.receiverIPAddress).type === 'unavailable') {
this.emitIcmpError(nodeId, networkInterface, 'destinationNetworkUnreachable', packet);
} }
} }
@@ -3900,10 +4000,6 @@ function portKey(nodeId: Id, portId: Id): string {
return `${nodeId}|${portId}`; return `${nodeId}|${portId}`;
} }
function arpKey(nodeId: Id, ipAddress: string): string {
return `${nodeId}|${ipAddress}`;
}
function bestInterface(targetIPAddress: string, interfaces: readonly NetworkInterface[]): NetworkInterface | undefined { function bestInterface(targetIPAddress: string, interfaces: readonly NetworkInterface[]): NetworkInterface | undefined {
let selected: NetworkInterface | undefined; let selected: NetworkInterface | undefined;
let selectedMask: number | undefined; let selectedMask: number | undefined;
+81
View File
@@ -0,0 +1,81 @@
// ARP as Java FILIUS runs it. tools/java-oracle's ArpRun sent to addresses no
// card has on arp-routed's network in Java FILIUS and wrote every frame each
// card saw and when each call started and ended (arp-java-<story>.java.json).
// The engine makes the same calls: the frames must come in the same order,
// with the same content, at about the same distance from each other
// (javaRun.ts), the calls must return the same, after about as long.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import type { IPv4DeliveryResult, NetworkRuntimeEngine } from '../src/index.js';
import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame } from './javaRun.js';
import type { Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const HOST = '00000000-0000-0000-0000-000000000301';
interface JavaRun {
frames: Record<string, JavaFrame[]>;
events: [number, string][];
}
/** A ping's result in Java's words: the ICMP answer's type and code, or null when none came. */
function pingResult(result: IPv4DeliveryResult, answered: boolean): string {
if (result.type === 'icmpError' && result.kind === 'destinationHostUnreachable') return 'ICMP 3/1';
return answered ? 'ICMP 0/0' : 'null';
}
const STORIES: Record<string, (engine: NetworkRuntimeEngine) => string> = {
pingLan: (engine) => pingResult(engine.sendIcmpEcho(HOST, '10.0.0.9'), false),
pingRouted: (engine) => {
const result = engine.sendIcmpEcho(HOST, '20.0.0.9');
return pingResult(result, result.type === 'delivered' && result.nodeId !== HOST);
},
udpTwice: () => '',
udpBehind: () => '',
};
/**
* Stories the engine does not play like Java yet, with the reason; each must
* still differ from Java.
*
* udpTwice, udpBehind: Java's transport thread sends one segment after the
* other and holds its queue while ARP waits, so the next send blocks — the
* engine's threads come in the next step.
*/
const NOT_YET_LIKE_JAVA: Record<string, string> = {
udpTwice: "Java's transport thread: the second datagram waits for the first one's ARP",
udpBehind: "Java's transport thread: a datagram to a reachable host waits behind one whose ARP fails",
};
describe('ARP as Java FILIUS runs it', () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'arp-routed.scenario.json'), 'utf8')) as Scenario;
for (const [name, story] of Object.entries(STORIES)) {
it(name, () => {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `arp-java-${name}.java.json`), 'utf8')) as JavaRun;
const { engine, frames } = watchedEngine(scenario);
engine.handle({ type: 'advance', toMilliseconds: 500 });
const called = engine.state.currentTimeMilliseconds;
const result = story(engine);
const returned = engine.state.currentTimeMilliseconds;
engine.handle({ type: 'advance', toMilliseconds: 16_000 });
const expected = gaps(javaFrames(java.frames));
const actual = gaps(frames());
if (name in NOT_YET_LIKE_JAVA) {
expect(JSON.stringify(actual), NOT_YET_LIKE_JAVA[name]).not.toBe(JSON.stringify(expected));
return;
}
const [[javaCalled], [javaReturned, javaOutcome]] = java.events as [[number, string], [number, string]];
expect(javaOutcome.replace(/^.* returned /, '').replace(/ from .*$/, '')).toBe(result);
expect(Math.abs(returned - called - (javaReturned - javaCalled)), 'duration of the call').toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) {
expect(actual[index]?.text, `frame #${index + 1}`).toBe(expected[index]?.text);
expect(Math.abs(actual[index]!.gap - expected[index]!.gap), `gap before frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
}
});
}
});
+2 -11
View File
@@ -53,17 +53,8 @@ function normalized(frames: readonly SeenFrame[]): SeenFrame[] {
})); }));
} }
/** /** Stories the engine does not play like Java yet, with the reason; each must still differ from Java. */
* Stories the engine does not play like Java yet, with the reason; each must const NOT_YET_LIKE_JAVA: Record<string, string> = {};
* still differ from Java (else it belongs to the others) and return the same.
*
* noServer: Java's transport thread sends while the resolver already waits,
* and asks ARP twice, a round trip apart. The engine's ARP (Swift's) blocks
* the call and asks again at once — rebuilt after Java in its own step.
*/
const NOT_YET_LIKE_JAVA: Record<string, string> = {
noServer: "ARP: Java's second request comes a round trip after the first, while the resolver waits",
};
describe('DNS as Java FILIUS runs it', () => { describe('DNS as Java FILIUS runs it', () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'dns-lan.scenario.json'), 'utf8')) as Scenario; const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'dns-lan.scenario.json'), 'utf8')) as Scenario;
+57
View File
@@ -1531,3 +1531,60 @@ describe('DNS', () => {
expect(() => lan({ '10.30.0.2': [] }, true)).toThrow(NotPortedError); expect(() => lan({ '10.30.0.2': [] }, true)).toThrow(NotPortedError);
}); });
}); });
describe('ARP', () => {
const requestsFor = (engine: NetworkRuntimeEngine, ipAddress: string) =>
engine.state.packetTraces.filter((trace) => trace.detail === `ARP request for ${ipAddress}`).map((trace) => trace.timeMilliseconds);
it('asks twice, a round trip apart, then gives up', () => {
const { engine, sourceNodeId } = routedTopology();
const called = engine.state.currentTimeMilliseconds;
expect(engine.resolveMacAddress(sourceNodeId, '10.0.0.9')).toBeUndefined();
expect(requestsFor(engine, '10.0.0.9')).toEqual([called, called + 2_750]);
expect(engine.state.currentTimeMilliseconds).toBe(called + 2 * 2_750);
});
it('lets a router answer host unreachable for an echo request, not for an echo reply', () => {
const { engine, sourceNodeId } = routedTopology();
const toNowhere = (kind: 'echoRequest' | 'echoReply', identity: number): IPv4Packet => ({
identity,
senderIPAddress: '10.0.0.2',
receiverIPAddress: '20.0.0.9',
timeToLive: 64,
protocol: 'icmp',
payload: { kind: 'icmp', message: icmpMessage(kind, { identifier: identity, sequenceNumber: 1, data: new Uint8Array() }) },
});
const observed = () => (engine.state.icmpObservationsByNodeId.get(sourceNodeId) ?? []).map((each) => each.message.kind);
engine.sendIPv4Packet(sourceNodeId, toNowhere('echoReply', 901));
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
expect(observed()).toEqual([]);
engine.sendIPv4Packet(sourceNodeId, toNowhere('echoRequest', 902));
engine.handle({ type: 'advance', toMilliseconds: 20_000 });
expect(observed()).toEqual(['destinationHostUnreachable']);
});
it('forgets the lookups that wait on stop', () => {
const [firstId, firstPortId, secondId, secondPortId] = [0x1a01, 0x1a02, 0x1a03, 0x1a04].map(uuid) as [string, string, string, string];
const input = (secondIp: string) => ({
nodes: [pc(firstId, firstPortId), pc(secondId, secondPortId)],
links: [link(firstId, firstPortId, secondId, secondPortId)],
deviceConfigurations: new Map([
[firstId, device('10.0.0.2')],
[secondId, device(secondIp)],
]),
});
const engine = startedEngine(input('10.0.0.8'));
const socket = engine.bindUdpSocket(firstId, { remoteIPAddress: '10.0.0.9', remotePort: 9 })!;
engine.sendUdp(socket, new TextEncoder().encode('lost'));
engine.handle({ type: 'stop' });
engine.handle({ type: 'start', snapshot: topologySnapshot(input('10.0.0.9')), seed: 14, initialTimeMilliseconds: 0 });
expect(engine.resolveMacAddress(firstId, '10.0.0.9')).toBeDefined();
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
expect(engine.state.packetTraces.filter((trace) => trace.layer === 'network' && trace.operation === 'sent')).toEqual([]);
});
});
+10 -1
View File
@@ -62,6 +62,15 @@ function javaText(frame: JavaFrame): string {
return `${macs} UDP ${ends} ${frame.payload}`; return `${macs} UDP ${ends} ${frame.payload}`;
} }
/** The ICMP type numbers Java logs. */
const ICMP_TYPES: Record<string, number> = {
echoReply: 0,
destinationNetworkUnreachable: 3,
destinationHostUnreachable: 3,
echoRequest: 8,
timeExceeded: 11,
};
const FLAG_LETTERS: [number, string][] = [ const FLAG_LETTERS: [number, string][] = [
[TCP_FLAG.synchronize, 'S'], [TCP_FLAG.synchronize, 'S'],
[TCP_FLAG.acknowledgement, 'A'], [TCP_FLAG.acknowledgement, 'A'],
@@ -77,7 +86,7 @@ function engineText(frame: EthernetFrame): string {
return `${macs} ARP ${arp.operation} ${arp.senderIPAddress} > ${arp.targetIPAddress}`; return `${macs} ARP ${arp.operation} ${arp.senderIPAddress} > ${arp.targetIPAddress}`;
} }
const packet = frame.payload.packet; const packet = frame.payload.packet;
if (packet.payload.kind === 'icmp') return `${macs} ICMP`; if (packet.payload.kind === 'icmp') return `${macs} ICMP ${ICMP_TYPES[packet.payload.message.kind]}`;
if (packet.payload.kind === 'tcp') { if (packet.payload.kind === 'tcp') {
const segment = packet.payload.segment; const segment = packet.payload.segment;
const flags = FLAG_LETTERS.filter(([bit]) => (segment.flags & bit) !== 0).map(([, letter]) => letter).join(''); const flags = FLAG_LETTERS.filter(([bit]) => (segment.flags & bit) !== 0).map(([, letter]) => letter).join('');
+26 -2
View File
@@ -73,7 +73,7 @@ function observationJson(item: IcmpObservation): Record<string, unknown> {
function run(scenario: Scenario): Record<string, unknown> { function run(scenario: Scenario): Record<string, unknown> {
// Swift's cables carry all frames at once; everything else is compared as it is. // Swift's cables carry all frames at once; everything else is compared as it is.
const engine = new NetworkRuntimeEngine(scenario.seed, undefined, { swiftCables: true }); const engine = new NetworkRuntimeEngine(scenario.seed, undefined, { swiftCables: true, swiftArp: true });
if (scenario.speedPercent !== undefined) engine.setSimulationSpeed(scenario.speedPercent); if (scenario.speedPercent !== undefined) engine.setSimulationSpeed(scenario.speedPercent);
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 }); engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
@@ -301,6 +301,16 @@ const JAVA_INSTEAD_OF_SWIFT: Record<string, string> = {
'tcp-closing': 'TCP: TIME_WAIT lasts 3 round trips and ends with the next segment; the other end closes by itself', 'tcp-closing': 'TCP: TIME_WAIT lasts 3 round trips and ends with the next segment; the other end closes by itself',
}; };
/**
* Scenarios that follow Java from a trace on: the traces before it must match
* Swift, the rest must differ (the Java comparisons check that behaviour).
*/
const SWIFT_UNTIL_TRACE: Record<string, { readonly trace: number; readonly reason: string }> = {
'firewall-and-loss': { trace: 36, reason: "a ping without an answer waits a round trip (Java's sendProbe), Swift returns at once" },
'routed-two-networks': { trace: 156, reason: 'the router answers host unreachable when ARP has given up after two round trips, Swift at once' },
'switch-lan': { trace: 155, reason: 'ARP for an address no card has asks again after a round trip, then host unreachable' },
};
describe('same result as the Swift engine', () => { describe('same result as the Swift engine', () => {
it('corrects Swift only where it errs', () => { it('corrects Swift only where it errs', () => {
for (const [name, correct] of Object.entries(SWIFT_ERRORS)) { for (const [name, correct] of Object.entries(SWIFT_ERRORS)) {
@@ -321,7 +331,21 @@ describe('same result as the Swift engine', () => {
}); });
} }
for (const name of scenarioNames.filter((each) => !(each in JAVA_INSTEAD_OF_SWIFT))) { for (const [name, { trace, reason }] of Object.entries(SWIFT_UNTIL_TRACE)) {
it(`${name} matches Swift until trace #${trace}, then follows Java: ${reason}`, () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.scenario.json`), 'utf8')) as Scenario;
const swift = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.swift.json`), 'utf8')) as Record<string, unknown>;
const actual = run(scenario);
const expectedTraces = swift.traces as unknown[];
const actualTraces = actual.traces as unknown[];
for (let index = 0; index < trace - 1; index += 1) {
expect(actualTraces[index], `trace event #${index + 1}`).toEqual(expectedTraces[index]);
}
expect(actualTraces[trace - 1]).not.toEqual(expectedTraces[trace - 1]);
});
}
for (const name of scenarioNames.filter((each) => !(each in JAVA_INSTEAD_OF_SWIFT) && !(each in SWIFT_UNTIL_TRACE))) {
it(name, () => { it(name, () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.scenario.json`), 'utf8')) as Scenario; const scenario = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.scenario.json`), 'utf8')) as Scenario;
const swift = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.swift.json`), 'utf8')) as Record<string, unknown>; const swift = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.swift.json`), 'utf8')) as Record<string, unknown>;
+4 -1
View File
@@ -36,7 +36,10 @@ npm test # vergleicht mit den geschriebenen *.swift.json
(`tcpJava.test.ts`, `TcpRun`). Seit F2.3d befördert ein Kabel des Kerns (`tcpJava.test.ts`, `TcpRun`). Seit F2.3d befördert ein Kabel des Kerns
einen Frame nach dem anderen wie Java; der Swift-Vergleich startet die einen Frame nach dem anderen wie Java; der Swift-Vergleich startet die
Engine mit `swiftCables` (alle Frames zugleich, wie Swift) und prüft so Engine mit `swiftCables` (alle Frames zugleich, wie Swift) und prüft so
alles andere weiter Spur für Spur. alles andere weiter Spur für Spur. Ebenso stellt `swiftArp` Swifts ARP in
der Ereignisverarbeitung nach (beide Anfragen zugleich, Warteliste ohne
Ende); drei Szenarien vergleicht der Test nur bis zu der Spur, an der ARP
scheitert oder ein Ping unbeantwortet bleibt (`SWIFT_UNTIL_TRACE`).
Das Ergebnis enthält die Sockets mit der Länge ihrer Warteschlange, dem Das Ergebnis enthält die Sockets mit der Länge ihrer Warteschlange, dem
TCP-Zustand, dem annehmenden Socket und den Sitzungswerten. TCP-Zustand, dem annehmenden Socket und den Sitzungswerten.