TCP follows Java's TCPSocket where it differs from Swift: each attempt and TIME_WAIT wait three round trips that follow the speed (8250 ms at 90 %), counted from when the segment is handed down, before ARP; the next segment ends TIME_WAIT; the other end closes by itself on FIN; data no one acknowledges closes with FIN; the accepted socket has the listener's initial sequence number, and the listener takes the next. Not taken from Java: three SYNs, not one (Java's retry loop ends after the first timeout), named in tcpJava.test.ts as JAVA_BUGS. tcpJava.test.ts compares TcpRun's three stories frame by frame and state by state; the Swift comparison only checks that the TCP scenarios differ. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
6.2 KiB
TypeScript
135 lines
6.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, except where a card sends two frames at once: Java's
|
|
// cable carries one frame at a time (SimplexVerbindung sleeps the delay with
|
|
// the port's buffer locked, and the sender waits), the engine's cable all of
|
|
// them together — up to three cables' delay, measured. A gap may differ by
|
|
// GAP_TOLERANCE_MILLISECONDS; a wrong round trip time (250 ms) may not.
|
|
|
|
import { NetworkRuntimeEngine, TCP_FLAG, type EthernetFrame } from '../src/index.js';
|
|
import { snapshotOf, type Scenario } from './scenario.js';
|
|
|
|
export const GAP_TOLERANCE_MILLISECONDS = 200;
|
|
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}`;
|
|
}
|
|
|
|
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`;
|
|
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 }));
|
|
}
|