// What FILIUS' viewers of a running simulation show, against Java's // (viewers-switch.java.json, viewers-nat.java.json; ViewersRun): a switch's // source address table after pings, after its reset and after the retention; // a home router's NAT table after a ping and a datagram, and after its reset // (the port forwarding's entry stays); the frames "Pakete verwerfen" drops // while it is held, in Java's words. Java's time of day is the viewer's to // write (the editor's), and its table of a switch comes in the order of // Java's hash map, here by address. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { javaFrameText, natViewRow, NetworkRuntimeEngine, type Steps } from '../src/index.js'; import { snapshotOf, type Scenario } from './scenario.js'; interface JavaStep { readonly step: string; readonly at: number; readonly rows?: readonly (readonly string[])[]; readonly updated?: Readonly>; readonly answer?: string; readonly answers?: Readonly>; readonly dropped?: readonly string[]; } const here = dirname(fileURLToPath(import.meta.url)); const javaRun = (name: string) => JSON.parse(readFileSync(join(here, `viewers-${name}.java.json`), 'utf8')) as { readonly steps: readonly JavaStep[] }; const scenarioOf = (name: string) => JSON.parse(readFileSync(join(here, `scenarios/${name}.scenario.json`), 'utf8')) as Scenario; function started(scenario: Scenario) { const engine = new NetworkRuntimeEngine(scenario.seed); engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 }); const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds }); /** A ping, returning when its answer came or its wait ended, as Java's sendProbe blocks. */ const ping = (from: string, target: string, sequenceNumber: number): string => { let answer: string | undefined; engine.startThread( (function* (): Steps { const reply = yield* engine.icmpEchoSteps(from, target, { identifier: 1, sequenceNumber }); answer = reply === undefined ? 'null' : `ICMP ${reply.message.kind === 'echoReply' ? '0/0' : reply.message.kind}`; })(), ); for (let waited = 0; answer === undefined && waited < 30_000; waited += 10) run(10); return answer ?? 'null'; }; return { engine, run, ping }; } describe("a switch's table and the dropped frames, against Java's", () => { const java = javaRun('switch'); const at = (name: string) => java.steps.find((step) => step.step === name)!; const SWITCH = '00000000-0000-0000-0000-000000000104'; const PC10 = '00000000-0000-0000-0000-000000000101'; it('learns, is reset, ages, and lists what is dropped while it is held', () => { const { engine, run, ping } = started(scenarioOf('switch-lan')); run(500); /** The table as Java's viewer shows it, without the time: address in capitals, "Port ". */ const table = () => engine.switchSatEntries(SWITCH).map((entry) => [entry.macAddress.toUpperCase(), `Port ${entry.portNumber}`]); const javaTable = (name: string) => at(name).rows!.map(([mac = '', port = '']) => [mac, port]); const sorted = (rows: string[][]) => [...rows].sort(([lhs], [rhs]) => (lhs! < rhs! ? -1 : 1)); expect([ping(PC10, '192.168.0.20', 1), ping(PC10, '192.168.0.30', 2)]).toEqual(Object.values(at('after pings').answers!)); // By address (Java's in its hash map's order). expect(table()).toEqual(sorted(javaTable('after pings'))); expect(table()).not.toEqual(javaTable('after pings')); engine.clearSwitchSat(SWITCH); expect(table()).toEqual(javaTable('reset')); expect(ping(PC10, '192.168.0.20', 3)).toBe(at('ping .20 again').answers!['ping .20']); expect(table()).toEqual(sorted(javaTable('ping .20 again'))); run(6000); expect(table()).toEqual(javaTable('after the retention')); // "Pakete verwerfen" held while .10 pings .30: the request is dropped on its first cable; released, the ping is answered. engine.setGlobalPacketLossEnabled(true); const dropped = ping(PC10, '192.168.0.30', 4); engine.setGlobalPacketLossEnabled(false); const text = (step: JavaStep) => step.dropped!.map((line) => line.slice(line.indexOf(' : ') + 3)); const ours = () => engine.capture.droppedFrames().map((each) => javaFrameText(each.frame)); const sameButIdentity = (lines: readonly string[]) => lines.map((line) => line.replace(/\[id=\d+;/, '[id=ID;')); expect(dropped).toBe(at('dropped while held').answer); expect(sameButIdentity(ours())).toEqual(sameButIdentity(text(at('dropped while held')))); expect(ping(PC10, '192.168.0.30', 5)).toBe(at('released').answer); expect(sameButIdentity(ours())).toEqual(sameButIdentity(text(at('released')))); // Java's time of the drop is the time of day with milliseconds. expect(at('released').dropped![0]).toMatch(/^\d\d:\d\d:\d\d\.\d\d\d : \[src=/); }); }); describe("a home router's NAT table, against Java's", () => { const java = javaRun('nat'); const at = (name: string) => java.steps.find((step) => step.step === name)!; const GATEWAY = '00000000-0000-0000-0000-000000000502'; const LAN_PC = '00000000-0000-0000-0000-000000000501'; it('shows the port forwarding, a ping and a datagram, and keeps the port forwarding when reset', () => { const { engine, run, ping } = started(scenarioOf('nat-home')); run(500); const rows = () => engine.natEntries(GATEWAY).map((entry) => { const row = natViewRow(entry); return [...row.cells, row.updatedAtMilliseconds === undefined ? '-' : 'TIME']; }); /** Java's rows with a time of day as TIME, and the datagram's ports (drawn) as PORT. */ const javaRows = (name: string) => at(name).rows!.map((row) => (row[0] === 'UDP' ? [row[0], 'PORT', row[2], row[3], 'PORT', 'TIME'] : row.map((cell, index) => (index === 5 && cell !== '-' ? 'TIME' : cell)))); expect(rows()).toEqual(javaRows('start')); expect(ping(LAN_PC, '42.0.0.20', 1)).toBe(at('after ping and datagram').answers!['ping 42.0.0.20']); const socket = engine.bindUdpSocket(LAN_PC, { remoteIPAddress: '42.0.0.20', remotePort: 7 })!; engine.sendUdp(socket, new TextEncoder().encode('one')); run(1500); expect(rows().map((row) => (row[0] === 'UDP' ? [row[0], 'PORT', row[2], row[3], 'PORT', row[5]] : row))).toEqual(javaRows('after ping and datagram')); engine.clearDynamicNatEntries(GATEWAY); expect(rows()).toEqual(javaRows('reset')); }); it('writes an entry as Java’s createEintrag does', () => { expect([ natViewRow({ protocol: 'tcp', remoteIPAddress: '', port: 8080, lan: { ipAddress: '192.168.0.10', port: 80 }, type: 'static', updatedAtMilliseconds: 5 }), natViewRow({ protocol: 'icmp', remoteIPAddress: '42.0.0.20', port: 3, lan: { ipAddress: '192.168.0.10', port: 4 }, type: 'dynamic', updatedAtMilliseconds: 7 }), natViewRow({ protocol: 'udp', remoteIPAddress: '42.0.0.30', port: 8080, lan: { ipAddress: '192.168.0.10', port: 80 }, type: 'dynamicFromStatic', updatedAtMilliseconds: 9 }), ]).toEqual([ { cells: ['TCP', '8080', '*', '192.168.0.10', '80'] }, { cells: ['ICMP', '0 (Identifier: 3)', '42.0.0.20', '192.168.0.10', '0 (Identifier: 4)'], updatedAtMilliseconds: 7 }, { cells: ['UDP', '8080', '42.0.0.30', '192.168.0.10', '80'], updatedAtMilliseconds: 9 }, ]); }); });