An ARP lookup asks, waits a round trip, asks once more and gives up after another round trip; then host unreachable, observed at the sender itself or sent back by the router (for ICMP only for an echo request). A waiting sender wakes after what else happens at that time. A ping then waits a round trip for its answer, counted from when it left. Swift asks twice at once while events run and lets the packet wait without end when the address exists anywhere in the network; the option swiftArp keeps that for the Swift comparison, and three scenarios are compared with Swift until the trace where ARP fails or a ping goes unanswered. arpJava.test.ts compares ArpRun: both pings match; the two UDP stories wait for Java's transport thread (next step). DNS without a server now matches Java too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
6.5 KiB
TypeScript
145 lines
6.5 KiB
TypeScript
// What tools/java-oracle's DhcpRun and TcpRun recorded in Java FILIUS
|
|
// (<name>.java.json: every frame each card saw, FILIUS' own packet log), and
|
|
// the same view of an engine run: each frame when it first shows on any
|
|
// host's card, as a line of text. Shared by dhcpJava.test.ts and
|
|
// tcpJava.test.ts.
|
|
//
|
|
// Not only the frames of one card: FILIUS' log misses the DISCOVER of the
|
|
// DHCP client that sends second when two start together (measured in four of
|
|
// four runs of dhcp-lan; the server receives it) — a fault of the log, not of
|
|
// the network. Java logs no frame identity; the same text within
|
|
// DUPLICATE_WINDOW_MILLISECONDS is one frame (measured: a frame reaches the
|
|
// last card within some 130 ms; the closest two distinct frames of the same
|
|
// text are the DISCOVERs around a DECLINE, some 520 ms apart).
|
|
//
|
|
// Java runs on threads in real time, at the default speed of 90 % (55 ms a
|
|
// cable, a round trip time of 2750 ms). Its gaps match the engine's within a
|
|
// few milliseconds; where they do not, it is Java's threads (a sender blocked
|
|
// by a busy cable wakes late, up to some 110 ms measured), the log's lost
|
|
// DISCOVER above, or a story whose calls the engine makes one after the
|
|
// other. A gap may differ by GAP_TOLERANCE_MILLISECONDS; a wrong round trip
|
|
// time (250 ms) may not. A cable carries one frame after the other in both
|
|
// (engine.test.ts checks that: one cable's delay is below this tolerance).
|
|
|
|
import { NetworkRuntimeEngine, TCP_FLAG, type EthernetFrame } from '../src/index.js';
|
|
import { snapshotOf, type Scenario } from './scenario.js';
|
|
|
|
export const GAP_TOLERANCE_MILLISECONDS = 120;
|
|
const DUPLICATE_WINDOW_MILLISECONDS = 300;
|
|
|
|
export interface JavaFrame {
|
|
time: number;
|
|
source: string;
|
|
destination: string;
|
|
arp?: number;
|
|
senderIp?: string;
|
|
targetIp?: string;
|
|
sourceIp?: string;
|
|
destinationIp?: string;
|
|
protocol?: number;
|
|
sourcePort?: number;
|
|
destinationPort?: number;
|
|
payload?: string | null;
|
|
icmp?: number;
|
|
tcpFlags?: string;
|
|
sequence?: number;
|
|
acknowledgement?: number;
|
|
}
|
|
|
|
export interface SeenFrame {
|
|
readonly time: number;
|
|
readonly text: string;
|
|
}
|
|
|
|
function javaText(frame: JavaFrame): string {
|
|
const macs = `${frame.source} > ${frame.destination}`.toUpperCase();
|
|
if (frame.arp !== undefined) return `${macs} ARP ${frame.arp === 1 ? 'request' : 'reply'} ${frame.senderIp} > ${frame.targetIp}`;
|
|
if (frame.icmp !== undefined) return `${macs} ICMP ${frame.icmp}`;
|
|
const ends = `${frame.sourceIp}:${frame.sourcePort} > ${frame.destinationIp}:${frame.destinationPort}`;
|
|
if (frame.tcpFlags !== undefined) {
|
|
return `${macs} TCP ${ends} ${frame.tcpFlags || '-'} seq=${frame.sequence} ack=${frame.acknowledgement} length=${(frame.payload ?? '').length}`;
|
|
}
|
|
return `${macs} UDP ${ends} ${frame.payload}`;
|
|
}
|
|
|
|
/** The ICMP type numbers Java logs. */
|
|
const ICMP_TYPES: Record<string, number> = {
|
|
echoReply: 0,
|
|
destinationNetworkUnreachable: 3,
|
|
destinationHostUnreachable: 3,
|
|
echoRequest: 8,
|
|
timeExceeded: 11,
|
|
};
|
|
|
|
const FLAG_LETTERS: [number, string][] = [
|
|
[TCP_FLAG.synchronize, 'S'],
|
|
[TCP_FLAG.acknowledgement, 'A'],
|
|
[TCP_FLAG.finish, 'F'],
|
|
[TCP_FLAG.push, 'P'],
|
|
[TCP_FLAG.reset, 'R'],
|
|
];
|
|
|
|
function engineText(frame: EthernetFrame): string {
|
|
const macs = `${frame.sourceMacAddress} > ${frame.destinationMacAddress}`.toUpperCase();
|
|
if (frame.payload.kind === 'arp') {
|
|
const arp = frame.payload.packet;
|
|
return `${macs} ARP ${arp.operation} ${arp.senderIPAddress} > ${arp.targetIPAddress}`;
|
|
}
|
|
const packet = frame.payload.packet;
|
|
if (packet.payload.kind === 'icmp') return `${macs} ICMP ${ICMP_TYPES[packet.payload.message.kind]}`;
|
|
if (packet.payload.kind === 'tcp') {
|
|
const segment = packet.payload.segment;
|
|
const flags = FLAG_LETTERS.filter(([bit]) => (segment.flags & bit) !== 0).map(([, letter]) => letter).join('');
|
|
return `${macs} TCP ${packet.senderIPAddress}:${segment.sourcePort} > ${packet.receiverIPAddress}:${segment.destinationPort} ${flags || '-'} seq=${segment.sequenceNumber} ack=${segment.acknowledgementNumber} length=${segment.payload.length}`;
|
|
}
|
|
const datagram = packet.payload.datagram;
|
|
return `${macs} UDP ${packet.senderIPAddress}:${datagram.sourcePort} > ${packet.receiverIPAddress}:${datagram.destinationPort} ${new TextDecoder().decode(datagram.payload)}`;
|
|
}
|
|
|
|
/** A started engine on the scenario's network whose frames can be read afterwards as FILIUS logs them. */
|
|
export function watchedEngine(scenario: Scenario) {
|
|
const engine = new NetworkRuntimeEngine(scenario.seed);
|
|
const framesByIdentity = new Map<number, EthernetFrame>();
|
|
const send = engine.sendEthernetFrame.bind(engine);
|
|
engine.sendEthernetFrame = (fromNodeId, outgoingPortId, frame) => {
|
|
framesByIdentity.set(frame.identity, frame);
|
|
send(fromNodeId, outgoingPortId, frame);
|
|
};
|
|
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
|
|
|
|
/** Every frame when it first shows on a host's card (sent, or received). */
|
|
const frames = (): SeenFrame[] => {
|
|
const hosts = new Set(scenario.nodes.filter((node) => node.kind !== 'networkSwitch').map((node) => node.id));
|
|
const firstSeen = new Map<number, SeenFrame>();
|
|
for (const event of engine.state.packetTraces) {
|
|
if (event.layer !== 'physical' || !hosts.has(event.nodeId) || event.frameIdentity === undefined) continue;
|
|
if (!((event.direction === 'outbound' && event.operation === 'sent') || (event.direction === 'inbound' && event.operation === 'received'))) {
|
|
continue;
|
|
}
|
|
if (firstSeen.has(event.frameIdentity)) continue;
|
|
firstSeen.set(event.frameIdentity, { time: event.timeMilliseconds, text: engineText(framesByIdentity.get(event.frameIdentity)!) });
|
|
}
|
|
return [...firstSeen.values()];
|
|
};
|
|
return { engine, frames };
|
|
}
|
|
|
|
/** Java's frames of all cards, each once: the same text within the window is the same frame. */
|
|
export function javaFrames(framesByNode: Record<string, JavaFrame[]>): SeenFrame[] {
|
|
const all = Object.values(framesByNode)
|
|
.flat()
|
|
.map((frame) => ({ time: frame.time, text: javaText(frame) }))
|
|
.sort((lhs, rhs) => lhs.time - rhs.time);
|
|
const frames: SeenFrame[] = [];
|
|
for (const frame of all) {
|
|
if (frames.some((each) => each.text === frame.text && frame.time - each.time <= DUPLICATE_WINDOW_MILLISECONDS)) continue;
|
|
frames.push(frame);
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
/** Consecutive frames: the text, and the time since the one before. */
|
|
export function gaps(frames: readonly SeenFrame[]) {
|
|
return frames.map((frame, index) => ({ text: frame.text, gap: index === 0 ? 0 : frame.time - frames[index - 1]!.time }));
|
|
}
|