// What tools/java-oracle's DhcpRun and TcpRun recorded in Java FILIUS // (.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 = { 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(); 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(); 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): 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 })); }