// 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-.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 end at about the same times and return the same. 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; 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'; } /** Records a call as ArpRun does: when it started, when it ended and what it returned. */ type Call = (label: string, run: () => string) => void; function udpSend(engine: NetworkRuntimeEngine, target: string, payload: string): string { const socket = engine.bindUdpSocket(HOST, { remoteIPAddress: target, remotePort: 9 })!; engine.sendUdp(socket, new TextEncoder().encode(payload)); return ''; } const STORIES: Record void> = { pingLan: (engine, call) => call('sendProbe 10.0.0.9', () => pingResult(engine.sendIcmpEcho(HOST, '10.0.0.9'), false)), pingRouted: (engine, call) => call('sendProbe 20.0.0.9', () => { const result = engine.sendIcmpEcho(HOST, '20.0.0.9'); return pingResult(result, result.type === 'delivered' && result.nodeId !== HOST); }), udpTwice: (engine, call) => { call('senden eins', () => udpSend(engine, '10.0.0.9', 'eins')); call('senden zwei', () => udpSend(engine, '10.0.0.9', 'zwei')); }, udpBehind: (engine, call) => { call('senden verloren', () => udpSend(engine, '10.0.0.9', 'verloren')); call('senden dahinter', () => udpSend(engine, '20.0.0.2', 'dahinter')); }, }; /** The client's free port as "client": Java draws it at random, the engine from its seed. */ function withClientPort(frames: readonly T[]): T[] { return frames.map((frame) => ({ ...frame, text: frame.text.replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole)) })); } /** Java's events without its answer's sender ("from …"), which the engine's results do not name. */ function javaEvent(event: string): string { return event.replace(/ from .*$/, ''); } 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 events: [number, string][] = []; story(engine, (label, run) => { events.push([engine.state.currentTimeMilliseconds, `${label} called`]); const result = run(); events.push([engine.state.currentTimeMilliseconds, `${label} returned ${result}`]); }); engine.handle({ type: 'advance', toMilliseconds: 16_000 }); const expected = gaps(withClientPort(javaFrames(java.frames))); const actual = gaps(withClientPort(frames())); expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => javaEvent(event))); for (const [index, [time]] of events.entries()) { expect(Math.abs(time - events[0]![0] - (java.events[index]![0] - java.events[0]![0])), `time of ${events[index]![1]}`).toBeLessThanOrEqual( GAP_TOLERANCE_MILLISECONDS, ); } 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); } }); } });