// Modems as Java FILIUS runs them. tools/java-oracle's ModemRun joined two // networks by a pair of modems (a server and a client over TCP on localhost) // and pinged across (modem-java-.java.json): the modem has no card, // so no capture of its own, and adds no delay — a frame takes the cables on // both sides, nothing more. The engine pairs the two modems on a port and must // show every card the same frames at about the same times. // // Beside Java: the pair's cycle suppression, three modems on a port, a // partner without its cable (Swift's conditions; Java's modems have no pair // in the project). import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { withSnapshot, type NetworkRuntimeEngine } from '../src/index.js'; import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, type JavaFrame, type JavaTables } from './javaRun.js'; import { snapshotOf, type Scenario } from './scenario.js'; const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); const id = (number: number) => `00000000-0000-0000-0000-000000000${number}`; const HOST = id(401); const SERVER_MODEM = id(405); const CLIENT_MODEM = id(406); interface JavaRun extends JavaTables { frames: Record; events: [number, string][]; } /** A ping from HOST in Java's words: the ICMP answer's type and code, or null when none came in time. */ function ping(engine: NetworkRuntimeEngine, target: string, sequenceNumber: number): string { const result = engine.sendIcmpEcho(HOST, target, { sequenceNumber }); if (result.type === 'icmpError' && result.kind === 'destinationHostUnreachable') return 'ICMP 3/1'; const answered = (engine.state.icmpObservationsByNodeId.get(HOST) ?? []).some((observation) => observation.message.kind === 'echoReply' && observation.message.sequenceNumber === sequenceNumber); return answered ? 'ICMP 0/0' : 'null'; } const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'modem-lan.scenario.json'), 'utf8')) as Scenario; /** Without the client modem's configuration: the server modem has no partner, as Java's that is not connected. */ const unpaired = () => { const snapshot = snapshotOf(scenario); return withSnapshot(snapshot, { remoteLinkConfigurationsByNodeId: new Map([...snapshot.remoteLinkConfigurationsByNodeId].filter(([node]) => node !== CLIENT_MODEM)) }); }; const STORIES: Record ReturnType }> = { ping: { pings: 2 }, unconnected: { pings: 1, snapshot: unpaired }, }; /** Java's events without its answer's sender ("from …"), which the engine's results do not name. */ const javaEvent = (event: string) => event.replace(/ from .*$/, ''); describe('modems as Java FILIUS runs them', () => { for (const [name, story] of Object.entries(STORIES)) { it(name, () => { const java = JSON.parse(readFileSync(join(scenarioDirectory, `modem-java-${name}.java.json`), 'utf8')) as JavaRun; const { engine } = watchedEngine(scenario, story.snapshot?.()); engine.handle({ type: 'advance', toMilliseconds: 500 }); const events: [number, string][] = []; for (let sequence = 1; sequence <= story.pings; sequence += 1) { events.push([engine.state.currentTimeMilliseconds, 'sendProbe 10.0.0.2 called']); const result = ping(engine, '10.0.0.2', sequence); events.push([engine.state.currentTimeMilliseconds, `sendProbe 10.0.0.2 returned ${result}`]); } engine.handle({ type: 'advance', toMilliseconds: 10_000 }); expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => javaEvent(event))); for (const [index, [time]] of events.entries()) { expect(Math.abs(time - events[0]![0] - (java.events[index]![0] - java.events[0]![0])), `time of ${events[index]![1]}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); } expectSameCaptures(engine, scenario, java.frames, { tables: java }); // A modem has no card: nothing in a capture of its port. expect(engine.capture.frames(id(451))).toEqual([]); expect(engine.capture.frames(id(461))).toEqual([]); }); } }); describe('a pair of modems in the project', () => { it('stands active while running, stopped otherwise', () => { const { engine } = watchedEngine(scenario); expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'active', partnerNodeId: CLIENT_MODEM }); expect(engine.remoteLinkCondition(CLIENT_MODEM)).toEqual({ kind: 'active', partnerNodeId: SERVER_MODEM }); expect(engine.remoteLinkCondition(HOST)).toBeUndefined(); engine.handle({ type: 'stop' }); expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'stopped' }); }); it('adds no delay of its own: across the pair a frame takes three cables, as two take two', () => { const { engine } = watchedEngine(scenario); engine.handle({ type: 'advance', toMilliseconds: 500 }); ping(engine, '10.0.0.2', 1); const sent = engine.capture.frames(id(411))[0]!.timeMilliseconds; // The ARP request: 10.0.0.3 behind the switch (two cables), 10.0.0.2 behind the pair (three). const overSwitch = engine.capture.frames(id(417))[0]!.timeMilliseconds - sent; const overPair = engine.capture.frames(id(413))[0]!.timeMilliseconds - sent; expect(overSwitch).toBeGreaterThan(0); expect(overPair).toBe((overSwitch / 2) * 3); }); it('pairs only enabled modems', () => { const snapshot = snapshotOf(scenario); const off = withSnapshot(snapshot, { remoteLinkConfigurationsByNodeId: new Map( [...snapshot.remoteLinkConfigurationsByNodeId].map(([node, configuration]) => [node, node === CLIENT_MODEM ? { ...configuration, isEnabled: false } : configuration]), ), }); const { engine } = watchedEngine(scenario, off); expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'unpaired' }); expect(engine.remoteLinkCondition(CLIENT_MODEM)).toEqual({ kind: 'disabled' }); }); it('has no partner when it is alone on its port, and drops what comes', () => { const { engine } = watchedEngine(scenario, unpaired()); expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'unpaired' }); engine.handle({ type: 'advance', toMilliseconds: 500 }); ping(engine, '10.0.0.2', 1); expect(engine.state.packetTraces.some((event) => event.nodeId === SERVER_MODEM && event.operation === 'dropped' && event.detail === 'remote link unpaired')).toBe(true); expect(engine.capture.frames(id(413))).toEqual([]); }); it('is ambiguous with a third modem on the port, and passes nothing', () => { const third = id(408); const snapshot = snapshotOf(scenario); const crowded = withSnapshot(snapshot, { nodes: [...snapshot.nodes, { id: third, kind: 'remoteLink', ports: [{ id: id(481), label: 'remote0', macAddress: '02:00:00:00:04:81' }] }], remoteLinkConfigurationsByNodeId: new Map([...snapshot.remoteLinkConfigurationsByNodeId, [third, { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true }]]), }); const { engine } = watchedEngine(scenario, crowded); expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'ambiguous', enabledNodeCount: 3 }); engine.handle({ type: 'advance', toMilliseconds: 500 }); expect(ping(engine, '10.0.0.2', 1)).toBe('ICMP 3/1'); }); it('is detached when its partner has no cable', () => { const snapshot = snapshotOf(scenario); const { engine } = watchedEngine(scenario, withSnapshot(snapshot, { links: snapshot.links.filter((link) => link.sourceNodeId !== CLIENT_MODEM) })); expect(engine.remoteLinkCondition(SERVER_MODEM)).toEqual({ kind: 'detached', partnerNodeId: CLIENT_MODEM }); engine.handle({ type: 'advance', toMilliseconds: 500 }); expect(ping(engine, '10.0.0.2', 1)).toBe('ICMP 3/1'); }); it('passes a frame once: both modems on one switch make a circle that ends at the modems', () => { // The client modem on the switch's third port instead of at 10.0.0.2: a broadcast goes into the server modem, out of the client modem, back into the switch. const snapshot = snapshotOf(scenario); const circle = withSnapshot(snapshot, { nodes: snapshot.nodes.map((node) => (node.id === id(404) ? { ...node, ports: [...node.ports, { id: id(444), label: 'sw4', macAddress: '02:00:00:00:04:44' }] } : node)), links: [ ...snapshot.links.filter((link) => link.targetNodeId !== id(403)), { id: '00000000-0000-0000-0000-00000000F405', sourceNodeId: CLIENT_MODEM, sourcePortId: id(461), targetNodeId: id(404), targetPortId: id(444) }, ], }); const { engine } = watchedEngine(scenario, circle); engine.handle({ type: 'advance', toMilliseconds: 500 }); expect(ping(engine, '10.0.0.3', 1)).toBe('ICMP 0/0'); engine.handle({ type: 'advance', toMilliseconds: 5_000 }); // The switch floods the request to both modems: the client modem already passed it on as the server modem's partner. expect(engine.state.packetTraces.some((event) => event.nodeId === CLIENT_MODEM && event.detail === 'remote link cycle suppression')).toBe(true); // 10.0.0.3 saw the request once, not again and again. expect(engine.capture.frames(id(417)).filter((each) => each.frame.payload.kind === 'arp' && each.frame.payload.packet.operation === 'request')).toHaveLength(1); }); });