Files
julian.kiedaischandClaude Opus 5.5 6809a71300 feat: F4.6g5 the switch and NAT tables and "Pakete verwerfen", measured against Java's
ViewersRun takes the rows of Java's viewers from the firmware: a switch's
table, a home router's NAT table, each after its reset, and the frames
"Pakete verwerfen" drops while it is held. The core writes a frame as Java
does and resets the NAT's dynamic entries; the room sends both tables as
views, drops while any client holds the button, and sends the dropped
frames, begun anew with each start, with the clock their times of day are
written from; the editor opens a table with a tap on a switch or home
router and holds "Pakete verwerfen" in its toolbar, which now wraps on a
phone. This completes F4.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:13:48 +00:00

129 lines
7.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<Record<string, number>>;
readonly answer?: string;
readonly answers?: Readonly<Record<string, string | number>>;
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 <n>". */
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 },
]);
});
});