Every packet that comes in goes through its node's IP thread (UDP, TCP) or ICMP thread (ICMP), as Java's IPThread and ICMPThread: deliver, answer an echo request or forward, one after the other; while one waits for ARP, the next wait behind it. A packet a node sends to itself is delivered at once, as Java's IP.senden does. The transport send threads and these are one mechanism now (handToProtocolThread). ArpRun has three stories through the router (routedUdpBehind, routedPingBesideUdp, routedPingBehindPing); the oracle writes ICMP frames with type, code, addresses and sequence number. All seven ArpRun stories match Java FILIUS. Measured and not taken: Java's ARP wakes one waiter with notify, not the one whose answer came; the core wakes all that wait for the address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
6.0 KiB
TypeScript
123 lines
6.0 KiB
TypeScript
// 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-<story>.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 { icmpMessage, type 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<string, JavaFrame[]>;
|
|
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 = 1): 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';
|
|
}
|
|
|
|
/** 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<string, (engine: NetworkRuntimeEngine, call: Call) => void> = {
|
|
pingLan: (engine, call) => call('sendProbe 10.0.0.9', () => ping(engine, '10.0.0.9')),
|
|
pingRouted: (engine, call) => call('sendProbe 20.0.0.9', () => ping(engine, '20.0.0.9')),
|
|
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'));
|
|
},
|
|
routedUdpBehind: (engine, call) => {
|
|
call('senden verloren', () => udpSend(engine, '20.0.0.9', 'verloren'));
|
|
call('senden dahinter', () => udpSend(engine, '20.0.0.2', 'dahinter'));
|
|
},
|
|
routedPingBesideUdp: (engine, call) => {
|
|
call('sendProbe 20.0.0.2', () => ping(engine, '20.0.0.2'));
|
|
call('senden verloren', () => udpSend(engine, '20.0.0.9', 'verloren'));
|
|
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 100 });
|
|
call('sendProbe 20.0.0.2', () => ping(engine, '20.0.0.2', 2));
|
|
},
|
|
routedPingBehindPing: (engine, call) => {
|
|
// Java's sendeICMP sends in the caller's thread, which waits for ARP.
|
|
call('sendeICMP 20.0.0.9', () => {
|
|
engine.resolveMacAddress(HOST, '10.0.0.1');
|
|
engine.sendIPv4Packet(HOST, {
|
|
identity: engine.allocatePacketIdentity(),
|
|
senderIPAddress: '10.0.0.2',
|
|
receiverIPAddress: '20.0.0.9',
|
|
timeToLive: 64,
|
|
protocol: 'icmp',
|
|
payload: { kind: 'icmp', message: icmpMessage('echoRequest', { identifier: 2, sequenceNumber: 2, data: new Uint8Array() }) },
|
|
});
|
|
return '';
|
|
});
|
|
call('sendProbe 20.0.0.2', () => ping(engine, '20.0.0.2'));
|
|
},
|
|
};
|
|
|
|
/** The client's free port as "client": Java draws it at random, the engine from its seed. */
|
|
function withClientPort<T extends { text: string }>(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);
|
|
}
|
|
});
|
|
}
|
|
});
|