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>
157 lines
7.2 KiB
TypeScript
157 lines
7.2 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;
|
|
icmpCode?: 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 icmpText(macs, frame.icmp, frame.icmpCode!, frame.sourceIp!, frame.destinationIp!, frame.sequence!);
|
|
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. */
|
|
/** Type and code of each ICMP message the engine knows. */
|
|
const ICMP_TYPES: Record<string, [number, number]> = {
|
|
echoReply: [0, 0],
|
|
destinationNetworkUnreachable: [3, 0],
|
|
destinationHostUnreachable: [3, 1],
|
|
echoRequest: [8, 0],
|
|
timeExceeded: [11, 0],
|
|
};
|
|
|
|
/** The sequence number only for echo messages: an error carries none of its own. */
|
|
function icmpText(macs: string, type: number, code: number, source: string, destination: string, sequence: number): string {
|
|
const echo = type === 0 || type === 8 ? ` seq=${sequence}` : '';
|
|
return `${macs} ICMP ${type}/${code} ${source} > ${destination}${echo}`;
|
|
}
|
|
|
|
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') {
|
|
const message = packet.payload.message;
|
|
const [type, code] = ICMP_TYPES[message.kind]!;
|
|
return icmpText(macs, type, code, packet.senderIPAddress, packet.receiverIPAddress, message.sequenceNumber);
|
|
}
|
|
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 }));
|
|
}
|