feat(core): packet capture per card after Java's Lauscher, measured card by card
The engine keeps Java's packet capture (capture.ts): per network card every frame it sent (when it leaves on the cable) or received (also one for another card), once, with time and number; switches log nothing; frames the packet loss dropped are kept apart; the capture outlives a stop and a new start, as in Java, until clearCapture. Per port instead of per MAC, at most 10,000 frames a card. expectSameCaptures compares each card's capture with Java's list of that card in the ARP, WLAN, DNS, TCP, NAT, RIP and single-client DHCP runs. It found that ScenarioNetwork ignored a switch's learning time (Java ran 300 s, the core the scenarios' 5 s); the oracle sets it now, and the TCP and DHCP runs are recorded again. A core test found that a new start numbers frames from 1 again and the capture took them as seen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ export * from './runtime/routeTable.js';
|
||||
export * from './runtime/snapshot.js';
|
||||
export * from './runtime/speed.js';
|
||||
export * from './runtime/trace.js';
|
||||
export * from './runtime/capture.js';
|
||||
export * from './util/equal.js';
|
||||
export * from './util/javaIp.js';
|
||||
export * from './util/swiftString.js';
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// The packet capture as Java FILIUS keeps it (filius.rahmenprogramm.nachrichten
|
||||
// .Lauscher, the data exchange window): per network card every frame it sent
|
||||
// or received, once, with the time; and the frames the packet loss dropped.
|
||||
//
|
||||
// Java logs a frame on a card when the card hands it to the cable
|
||||
// (Ethernet.senden, after the cable is free) and when a frame arrives at the
|
||||
// card (EthernetThread, or before it the firewall or NAT thread of a router
|
||||
// or home router) — every frame that arrives, also one addressed to another
|
||||
// card, also one a firewall then drops. Switches have no cards and log
|
||||
// nothing. A frame shows on a card at most once (EthernetFrame
|
||||
// .readByLauscherForMac). The log is not cleared when the simulation stops
|
||||
// or starts again, only for a new or loaded project and by the user.
|
||||
//
|
||||
// Departures from Java: a card's list is kept under its port, not its MAC
|
||||
// address (Java merges two cards with the same MAC into one list), and it
|
||||
// keeps at most `maximumFrameCount` frames, dropping the oldest
|
||||
// `evictionBatchSize` at once (Java keeps all); a frame keeps its number.
|
||||
|
||||
import type { Id } from '../ids.js';
|
||||
import type { EthernetFrame } from './packets.js';
|
||||
|
||||
export interface CapturedFrame {
|
||||
/** Its row number in the card's list, from 1, kept when older frames are dropped (Java: "Nr."). */
|
||||
readonly number: number;
|
||||
readonly timeMilliseconds: number;
|
||||
readonly frame: EthernetFrame;
|
||||
}
|
||||
|
||||
/** How many frames a list keeps, and how many it drops at once when full. */
|
||||
export interface CaptureRetentionPolicy {
|
||||
readonly maximumFrameCount: number;
|
||||
readonly evictionBatchSize: number;
|
||||
}
|
||||
|
||||
export function captureRetentionPolicy(maximumFrameCount: number, evictionBatchSize: number): CaptureRetentionPolicy {
|
||||
if (!(maximumFrameCount > 0)) throw new RangeError('Packet capture must keep at least one frame');
|
||||
if (!(evictionBatchSize > 0)) throw new RangeError('Packet capture eviction must remove at least one frame');
|
||||
if (evictionBatchSize > maximumFrameCount) throw new RangeError('Packet capture eviction cannot exceed the retention limit');
|
||||
return { maximumFrameCount, evictionBatchSize };
|
||||
}
|
||||
|
||||
export const PRODUCTION_CAPTURE_RETENTION = captureRetentionPolicy(10_000, 1_000);
|
||||
|
||||
class CaptureList {
|
||||
readonly frames: CapturedFrame[] = [];
|
||||
private readonly identities = new Set<number>();
|
||||
private count = 0;
|
||||
|
||||
/** `once`: a frame shows at most once (a card's list; a switch floods one frame onto several cables, each may drop it). */
|
||||
constructor(
|
||||
private readonly policy: CaptureRetentionPolicy,
|
||||
private readonly once: boolean,
|
||||
) {}
|
||||
|
||||
/** Adds the frame unless it shows once only and the list has it already; false then. */
|
||||
add(timeMilliseconds: number, frame: EthernetFrame): boolean {
|
||||
if (this.once && this.identities.has(frame.identity)) return false;
|
||||
if (this.frames.length >= this.policy.maximumFrameCount) {
|
||||
for (const evicted of this.frames.splice(0, this.policy.evictionBatchSize)) this.identities.delete(evicted.frame.identity);
|
||||
}
|
||||
this.count += 1;
|
||||
this.frames.push({ number: this.count, timeMilliseconds, frame });
|
||||
this.identities.add(frame.identity);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A new run numbers its frames from 1 again: the identities of the last run no longer tell a frame apart. */
|
||||
forgetIdentities(): void {
|
||||
this.identities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketCapture {
|
||||
private readonly listsByPortId = new Map<Id, CaptureList>();
|
||||
private dropped: CaptureList;
|
||||
|
||||
constructor(private readonly policy: CaptureRetentionPolicy = PRODUCTION_CAPTURE_RETENTION) {
|
||||
this.dropped = new CaptureList(policy, false);
|
||||
}
|
||||
|
||||
/** Logs the frame on the card at `portId`, unless it shows there already. */
|
||||
record(portId: Id, timeMilliseconds: number, frame: EthernetFrame): void {
|
||||
let list = this.listsByPortId.get(portId);
|
||||
if (list === undefined) {
|
||||
list = new CaptureList(this.policy, true);
|
||||
this.listsByPortId.set(portId, list);
|
||||
}
|
||||
list.add(timeMilliseconds, frame);
|
||||
}
|
||||
|
||||
/** Logs a frame the packet loss dropped on its cable (Lauscher.addDroppedDataUnit). */
|
||||
recordDropped(timeMilliseconds: number, frame: EthernetFrame): void {
|
||||
this.dropped.add(timeMilliseconds, frame);
|
||||
}
|
||||
|
||||
/** The card's frames, oldest first. */
|
||||
frames(portId: Id): readonly CapturedFrame[] {
|
||||
return this.listsByPortId.get(portId)?.frames ?? [];
|
||||
}
|
||||
|
||||
/** The cards that have frames. */
|
||||
portIds(): Id[] {
|
||||
return [...this.listsByPortId.keys()];
|
||||
}
|
||||
|
||||
droppedFrames(): readonly CapturedFrame[] {
|
||||
return this.dropped.frames;
|
||||
}
|
||||
|
||||
/** The simulation starts again: the lists stay, frames of the new run are new. */
|
||||
beginRun(): void {
|
||||
for (const list of this.listsByPortId.values()) list.forgetIdentities();
|
||||
}
|
||||
|
||||
/** Empties every list (Lauscher.reset). */
|
||||
clear(): void {
|
||||
this.listsByPortId.clear();
|
||||
this.dropped = new CaptureList(this.policy, false);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@
|
||||
// DNS after Java (filius.software.dns: DNSServer, DNSServerMitarbeiter,
|
||||
// Resolver, DNSQueryAgent), not Swift's typed format; a recursive server
|
||||
// asks on without asking itself (Java does, without end).
|
||||
// The packet capture after Java's Lauscher (capture.ts): a card logs a frame
|
||||
// when it leaves on the cable and when it arrives, a switch logs nothing.
|
||||
// Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht,
|
||||
// filius.software.vermittlungsschicht (ARP, IP, ICMP), system.SwitchFirmware,
|
||||
// filius.software.transportschicht (UDP, TCP), filius.software.dhcp.
|
||||
@@ -103,6 +105,7 @@ import {
|
||||
type DnsMessageRecord,
|
||||
type DnsQuestion,
|
||||
} from './dns.js';
|
||||
import { PacketCapture, PRODUCTION_CAPTURE_RETENTION, type CaptureRetentionPolicy } from './capture.js';
|
||||
import { JavaCompatibleRandom } from './random.js';
|
||||
import { routeTableRows, type RouteRow } from './routeTable.js';
|
||||
import { EMPTY_TOPOLOGY_SNAPSHOT, withSnapshot, type NodeSnapshot, type TopologySnapshot } from './snapshot.js';
|
||||
@@ -845,6 +848,7 @@ export class NetworkRuntimeEngine {
|
||||
private readonly cablesCarryOneFrameAtATime: boolean;
|
||||
private readonly swiftArp: boolean;
|
||||
private readonly swiftSends: boolean;
|
||||
private readonly packetCapture: PacketCapture;
|
||||
|
||||
/**
|
||||
* `swiftCables`: a cable carries all frames at once, as in the Swift
|
||||
@@ -855,12 +859,20 @@ export class NetworkRuntimeEngine {
|
||||
*
|
||||
* `swiftArp`, `swiftSends`: Swift's ARP and Swift's send that returns
|
||||
* when the network is quiet, likewise only for the comparison.
|
||||
*
|
||||
* `captureRetention`: how many frames each card's packet capture keeps.
|
||||
*/
|
||||
constructor(
|
||||
seed: bigint | number = DEFAULT_SEED,
|
||||
packetTraceRetentionPolicy: TraceRetentionPolicy = PRODUCTION_TRACE_RETENTION,
|
||||
options: { readonly swiftCables?: boolean; readonly swiftArp?: boolean; readonly swiftSends?: boolean } = {},
|
||||
options: {
|
||||
readonly swiftCables?: boolean;
|
||||
readonly swiftArp?: boolean;
|
||||
readonly swiftSends?: boolean;
|
||||
readonly captureRetention?: CaptureRetentionPolicy;
|
||||
} = {},
|
||||
) {
|
||||
this.packetCapture = new PacketCapture(options.captureRetention ?? PRODUCTION_CAPTURE_RETENTION);
|
||||
this.cablesCarryOneFrameAtATime = options.swiftCables !== true;
|
||||
this.swiftArp = options.swiftArp === true;
|
||||
this.swiftSends = options.swiftSends === true;
|
||||
@@ -876,6 +888,20 @@ export class NetworkRuntimeEngine {
|
||||
this.stateValue.simulationSpeedPercent = this.runtimeSpeed.percent;
|
||||
}
|
||||
|
||||
/**
|
||||
* The packet capture (Java's Lauscher): per card every frame it sent or
|
||||
* received. It outlives a stop and a new start, as in Java; clearCapture
|
||||
* empties it.
|
||||
*/
|
||||
get capture(): PacketCapture {
|
||||
return this.packetCapture;
|
||||
}
|
||||
|
||||
/** Empties the packet capture (Java: the data exchange window's reset, a new or loaded project). */
|
||||
clearCapture(): void {
|
||||
this.packetCapture.clear();
|
||||
}
|
||||
|
||||
/** The runtime state. Read it; change it only through the engine's methods. */
|
||||
get state(): RuntimeState {
|
||||
return this.stateValue;
|
||||
@@ -900,6 +926,7 @@ export class NetworkRuntimeEngine {
|
||||
if (this.stateValue.phase !== 'stopped') return [this.output({ type: 'startIgnoredAlreadyRunning' })];
|
||||
const seed = toSeed(input.seed);
|
||||
this.stateValue = initialState(seed);
|
||||
this.packetCapture.beginRun();
|
||||
this.stateValue.phase = 'running';
|
||||
this.stateValue.currentTimeMilliseconds = input.initialTimeMilliseconds;
|
||||
this.stateValue.simulationSpeedPercent = this.runtimeSpeed.percent;
|
||||
@@ -1143,6 +1170,7 @@ export class NetworkRuntimeEngine {
|
||||
layer: TraceLayer,
|
||||
): boolean {
|
||||
if (!this.stateValue.globalPacketLossEnabled) return false;
|
||||
this.packetCapture.recordDropped(this.stateValue.currentTimeMilliseconds, frame);
|
||||
this.recordTrace({
|
||||
frameIdentity: frame.identity,
|
||||
packetIdentity: packetIdentityInFrame(frame),
|
||||
@@ -3730,6 +3758,7 @@ export class NetworkRuntimeEngine {
|
||||
|
||||
/** The frame leaves on the cable (and shows in the packet log); it arrives one cable's delay later. */
|
||||
private departEthernetFrame(fromNodeId: Id, outgoingPortId: Id, endpoint: { nodeId: Id; portId: Id } | undefined, frame: EthernetFrame): void {
|
||||
this.captureOnCard(fromNodeId, outgoingPortId, frame);
|
||||
this.recordTrace({
|
||||
frameIdentity: frame.identity,
|
||||
packetIdentity: packetIdentityInFrame(frame),
|
||||
@@ -3804,7 +3833,14 @@ export class NetworkRuntimeEngine {
|
||||
return this.stateValue.topologySnapshot.nodes.find((node) => node.id === nodeId);
|
||||
}
|
||||
|
||||
/** Java logs a frame on a card; a switch has none. */
|
||||
private captureOnCard(nodeId: Id, portId: Id, frame: EthernetFrame): void {
|
||||
if (this.nodeSnapshot(nodeId)?.kind === 'networkSwitch') return;
|
||||
this.packetCapture.record(portId, this.stateValue.currentTimeMilliseconds, frame);
|
||||
}
|
||||
|
||||
private receiveEthernetFrame(frame: EthernetFrame, nodeId: Id, incomingPortId: Id): void {
|
||||
this.captureOnCard(nodeId, incomingPortId, frame);
|
||||
this.recordTrace({
|
||||
frameIdentity: frame.identity,
|
||||
packetIdentity: packetIdentityInFrame(frame),
|
||||
|
||||
Reference in New Issue
Block a user