feat(core): the packet capture's table after Java's Lauscher, measured against Java's own tables

captureRows.ts builds Java's rows per frame (Lauscher.datenVorbereiten):
Ethernet; ARP, ICMP or IP; TCP or UDP; the data with Java's protocol
recognition; with addresses and ports or ports only. The comment stays
structured; captureCommentText says it with given words and numbers.

ScenarioNetwork writes Java's finished tables per card (Lauscher.getDaten,
German locale); all runtime runs are recorded again, NatRun has ttlOut.
expectSameCaptures compares both views for every frame. Fixed where the
engine differed: an ARP request's target MAC is the broadcast address,
an ICMP error's identifier and sequence number are 0 (RFC 792), and host
unreachable from a router carries the packet as forwarded. Not taken:
Java's lower-case broadcast address in a reply to 0.0.0.0, and the typo
'Exeeded'. RIP keeps its recordings: Java's staggered router start now
gives a different startup on this machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:11:05 +00:00
co-authored by Claude Opus 5.5
parent 0a34f3a4c4
commit 21a404693c
55 changed files with 37423 additions and 1321 deletions
+35 -2
View File
@@ -285,6 +285,31 @@ const scenarioNames = readdirSync(scenarioDirectory)
*/
const SWIFT_ERRORS: Record<string, (expected: Record<string, unknown>) => Record<string, unknown>> = {};
const ECHO_KINDS = new Set(['echoRequest', 'echoReply']);
/**
* Java instead of Swift in every scenario: an ICMP error's identifier and
* sequence number are 0 (RFC 792: unused; Java's sendeICMP), where Swift
* copies them from the echo request it is about. Measured with Java FILIUS
* (the capture tables of arpJava and natJava).
*/
function withJavaIcmpErrors(expected: Record<string, unknown>): Record<string, unknown> {
type Header = [string, string];
const headers = (fields: Header[]): Header[] => {
const kind = fields.find(([name]) => name === 'kind')?.[1];
if (kind === undefined || ECHO_KINDS.has(kind)) return fields;
return fields.map(([name, value]) => [name, name === 'identifier' || name === 'sequence' ? '0' : value]);
};
const traces = (expected.traces as { before: Header[]; after: Header[] }[]).map((trace) => ({ ...trace, before: headers(trace.before), after: headers(trace.after) }));
const observation = (item: { kind?: string }) => (item.kind === undefined || ECHO_KINDS.has(item.kind) ? item : { ...item, identifier: 0, sequence: 0 });
const observations = Object.fromEntries(
Object.entries(expected.icmpObservations as Record<string, { kind: string }[]>).map(([node, items]) => [node, items.map(observation)]),
);
// A traceroute's result is its observations (other results are lists too: a switch's table).
const results = (expected.results as unknown[]).map((result) => (Array.isArray(result) ? (result as { kind?: string }[]).map(observation) : result));
return { ...expected, traces, icmpObservations: observations, results };
}
/**
* Scenarios where the engine follows Java instead of Swift; the Java
* comparisons check that behaviour (dhcpJava.test.ts, tcpJava.test.ts,
@@ -319,6 +344,14 @@ describe('same result as the Swift engine', () => {
}
});
it('corrects ICMP errors where Swift has them', () => {
const corrected = scenarioNames.filter((name) => {
const expected = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.swift.json`), 'utf8')) as Record<string, unknown>;
return JSON.stringify(withJavaIcmpErrors(expected)) !== JSON.stringify(expected);
});
expect(corrected.length).toBeGreaterThan(0);
});
it('has scenarios with Swift results', () => {
expect(scenarioNames.length).toBeGreaterThan(0);
});
@@ -334,7 +367,7 @@ describe('same result as the Swift engine', () => {
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 swift = withJavaIcmpErrors(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[];
@@ -349,7 +382,7 @@ describe('same result as the Swift engine', () => {
it(name, () => {
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 expected = SWIFT_ERRORS[name]?.(swift) ?? swift;
const expected = withJavaIcmpErrors(SWIFT_ERRORS[name]?.(swift) ?? swift);
const actual = run(scenario);
// Traces first and one by one: the first difference is the one that matters.