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:
@@ -26,6 +26,7 @@ import {
|
||||
JavaCompatibleRandom,
|
||||
ripSeed,
|
||||
NetworkRuntimeEngine,
|
||||
captureRetentionPolicy,
|
||||
portSnapshot,
|
||||
stableMacAddress,
|
||||
switchConfiguration,
|
||||
@@ -2385,3 +2386,121 @@ describe('Home router NAT', () => {
|
||||
expect(engine.state.natTablesByNodeId.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("packet capture (Java's Lauscher)", () => {
|
||||
const hostA = uuid(0x2e01);
|
||||
const portA = uuid(0x2e11);
|
||||
const hostB = uuid(0x2e02);
|
||||
const portB = uuid(0x2e12);
|
||||
const switchId = uuid(0x2e04);
|
||||
const switchPorts = [1, 2].map((index) => portSnapshot(uuid(0x2e20 + index), `sw${index}`));
|
||||
const network = {
|
||||
nodes: [pc(hostA, portA), pc(hostB, portB), { id: switchId, kind: 'networkSwitch' as const, ports: switchPorts }],
|
||||
links: [link(hostA, portA, switchId, switchPorts[0]!.id), link(hostB, portB, switchId, switchPorts[1]!.id)],
|
||||
deviceConfigurations: new Map([
|
||||
[hostA, device('10.0.0.1')],
|
||||
[hostB, device('10.0.0.2')],
|
||||
]),
|
||||
};
|
||||
const kinds = (engine: NetworkRuntimeEngine, portId: string) =>
|
||||
engine.capture.frames(portId).map(({ number, frame }) => {
|
||||
const what = frame.payload.kind === 'arp' ? `arp ${frame.payload.packet.operation}` : frame.payload.packet.payload.kind;
|
||||
return `${number} ${frame.sourceMacAddress === stableMacAddress(portId) ? 'sent' : 'received'} ${what}`;
|
||||
});
|
||||
const arpRequest = (engine: NetworkRuntimeEngine): EthernetFrame => ({
|
||||
identity: engine.allocateFrameIdentity(),
|
||||
sourceMacAddress: stableMacAddress(portA),
|
||||
destinationMacAddress: ETHERNET_BROADCAST_MAC_ADDRESS,
|
||||
payload: {
|
||||
kind: 'arp',
|
||||
packet: { operation: 'request', senderMacAddress: stableMacAddress(portA), senderIPAddress: '10.0.0.1', targetMacAddress: UNSPECIFIED_MAC_ADDRESS, targetIPAddress: '10.0.0.9' },
|
||||
},
|
||||
});
|
||||
|
||||
it('logs every frame a card sends or receives, numbered, and nothing on a switch', () => {
|
||||
const engine = startedEngine(network);
|
||||
engine.sendIcmpEcho(hostA, '10.0.0.2');
|
||||
letFramesArrive(engine);
|
||||
|
||||
expect(kinds(engine, portA)).toEqual(['1 sent arp request', '2 received arp reply', '3 sent icmp', '4 received icmp']);
|
||||
expect(kinds(engine, portB)).toEqual(['1 received arp request', '2 sent arp reply', '3 received icmp', '4 sent icmp']);
|
||||
expect(switchPorts.map((port) => engine.capture.frames(port.id))).toEqual([[], []]);
|
||||
// When the card sent it, when the other card received it: two cables later.
|
||||
const [sent] = engine.capture.frames(portA);
|
||||
const [received] = engine.capture.frames(portB);
|
||||
expect(received!.timeMilliseconds - sent!.timeMilliseconds).toBe(2 * engine.simulationSpeed.linkTransmissionDelayMilliseconds);
|
||||
});
|
||||
|
||||
it('logs a frame on a card once, also a frame for another card', () => {
|
||||
const engine = startedEngine(network);
|
||||
const frame = { ...arpRequest(engine), destinationMacAddress: '02:00:00:00:00:FE' };
|
||||
engine.sendEthernetFrame(hostA, portA, frame);
|
||||
engine.sendEthernetFrame(hostA, portA, frame);
|
||||
letFramesArrive(engine);
|
||||
|
||||
expect(kinds(engine, portA)).toEqual(['1 sent arp request']);
|
||||
// The switch floods the unknown destination; B's card logs what is not addressed to it.
|
||||
expect(kinds(engine, portB)).toEqual(['1 received arp request']);
|
||||
});
|
||||
|
||||
it('keeps the frames the packet loss dropped apart; the sender logged them', () => {
|
||||
const engine = startedEngine(network);
|
||||
engine.setGlobalPacketLossEnabled(true);
|
||||
engine.sendEthernetFrame(hostA, portA, arpRequest(engine));
|
||||
letFramesArrive(engine);
|
||||
|
||||
expect(kinds(engine, portA)).toEqual(['1 sent arp request']);
|
||||
expect(engine.capture.frames(portB)).toEqual([]);
|
||||
expect(engine.capture.droppedFrames().map((each) => each.timeMilliseconds)).toEqual([engine.simulationSpeed.linkTransmissionDelayMilliseconds]);
|
||||
});
|
||||
|
||||
it('logs a frame flooded onto two cables once for each cable that drops it', () => {
|
||||
const hostC = uuid(0x2e03);
|
||||
const portC = uuid(0x2e13);
|
||||
const ports = [1, 2, 3].map((index) => portSnapshot(uuid(0x2e30 + index), `sw${index}`));
|
||||
const engine = startedEngine({
|
||||
nodes: [pc(hostA, portA), pc(hostB, portB), pc(hostC, portC), { id: switchId, kind: 'networkSwitch', ports }],
|
||||
links: [link(hostA, portA, switchId, ports[0]!.id), link(hostB, portB, switchId, ports[1]!.id), link(hostC, portC, switchId, ports[2]!.id)],
|
||||
});
|
||||
engine.sendEthernetFrame(hostA, portA, arpRequest(engine));
|
||||
// The switch has it and floods it; then the loss begins.
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.simulationSpeed.linkTransmissionDelayMilliseconds });
|
||||
engine.setGlobalPacketLossEnabled(true);
|
||||
letFramesArrive(engine);
|
||||
|
||||
expect(engine.capture.droppedFrames()).toHaveLength(2);
|
||||
expect([portB, portC].map((port) => engine.capture.frames(port))).toEqual([[], []]);
|
||||
});
|
||||
|
||||
it('outlives a stop and a new start, as in Java, until it is cleared', () => {
|
||||
const engine = startedEngine(network);
|
||||
engine.sendEthernetFrame(hostA, portA, arpRequest(engine));
|
||||
letFramesArrive(engine);
|
||||
engine.handle({ type: 'stop' });
|
||||
expect(kinds(engine, portA)).toEqual(['1 sent arp request']);
|
||||
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot(network), seed: 14, initialTimeMilliseconds: 1_000 });
|
||||
engine.setGlobalPacketLossEnabled(true);
|
||||
engine.sendEthernetFrame(hostA, portA, arpRequest(engine));
|
||||
letFramesArrive(engine);
|
||||
expect(engine.capture.frames(portA).map((each) => [each.number, each.timeMilliseconds])).toEqual([
|
||||
[1, 0],
|
||||
[2, 1_000],
|
||||
]);
|
||||
expect(engine.capture.droppedFrames()).toHaveLength(1);
|
||||
|
||||
engine.clearCapture();
|
||||
expect(engine.capture.portIds()).toEqual([]);
|
||||
expect(engine.capture.droppedFrames()).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps at most the retention, dropping the oldest at once; a frame keeps its number', () => {
|
||||
const engine = new NetworkRuntimeEngine(14, undefined, { captureRetention: captureRetentionPolicy(3, 2) });
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot(network), seed: 14, initialTimeMilliseconds: 0 });
|
||||
for (let count = 0; count < 4; count += 1) engine.sendEthernetFrame(hostA, portA, arpRequest(engine));
|
||||
letFramesArrive(engine);
|
||||
|
||||
expect(engine.capture.frames(portA).map((each) => each.number)).toEqual([3, 4]);
|
||||
expect(() => captureRetentionPolicy(2, 3)).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user