feat(core): the packet capture's table after Java's Lauscher, measured against Java's own tables

captureRows.ts builds Java's rows per frame (Lauscher.datenVorbereiten):
Ethernet; ARP, ICMP or IP; TCP or UDP; the data with Java's protocol
recognition; with addresses and ports or ports only. The comment stays
structured; captureCommentText says it with given words and numbers.

ScenarioNetwork writes Java's finished tables per card (Lauscher.getDaten,
German locale); all runtime runs are recorded again, NatRun has ttlOut.
expectSameCaptures compares both views for every frame. Fixed where the
engine differed: an ARP request's target MAC is the broadcast address,
an ICMP error's identifier and sequence number are 0 (RFC 792), and host
unreachable from a router carries the packet as forwarded. Not taken:
Java's lower-case broadcast address in a reply to 0.0.0.0, and the typo
'Exeeded'. RIP keeps its recordings: Java's staggered router start now
gives a different startup on this machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:11:05 +00:00
co-authored by Claude Opus 5.5
parent 0a34f3a4c4
commit 21a404693c
55 changed files with 37423 additions and 1321 deletions
+15 -23
View File
@@ -3716,13 +3716,13 @@ export class NetworkRuntimeEngine {
return this.stateValue.arpCachesByNodeId.get(nodeId)?.get(ipAddress)?.macAddress;
}
/** One ARP request for `targetIPAddress`, broadcast from the interface. */
/** One ARP request for `targetIPAddress`, broadcast from the interface; its target MAC the broadcast address, as Java's (ARP.sendeARPBroadcast; Swift, and swiftArp: zeros). */
private sendArpRequest(nodeId: Id, networkInterface: NetworkInterface, targetIPAddress: string): void {
const request: ArpPacket = {
operation: 'request',
senderMacAddress: networkInterface.macAddress,
senderIPAddress: networkInterface.ipAddress,
targetMacAddress: UNSPECIFIED_MAC_ADDRESS,
targetMacAddress: this.swiftArp ? UNSPECIFIED_MAC_ADDRESS : ETHERNET_BROADCAST_MAC_ADDRESS,
targetIPAddress,
};
const frame: EthernetFrame = {
@@ -4210,7 +4210,7 @@ export class NetworkRuntimeEngine {
const answered = () =>
(this.stateValue.icmpObservationsByNodeId.get(fromNodeId) ?? [])
.slice(observationCount)
.some((observation) => observation.message.identifier === identifier && observation.message.sequenceNumber === sequenceNumber);
.some((observation) => answersEcho(observation.message, identifier, sequenceNumber));
let sent: IPv4DeliveryResult | undefined;
let sentAt = this.stateValue.currentTimeMilliseconds;
const handedOn = this.sendIPv4PacketWithOptions(fromNodeId, packet, sourceInterface.portId, true, 'sent', {
@@ -4229,9 +4229,7 @@ export class NetworkRuntimeEngine {
}
const result = sent ?? handedOn;
const newObservations = (this.stateValue.icmpObservationsByNodeId.get(fromNodeId) ?? []).slice(observationCount);
const observation = newObservations.findLast(
(candidate) => candidate.message.identifier === identifier && candidate.message.sequenceNumber === sequenceNumber,
);
const observation = newObservations.findLast((candidate) => answersEcho(candidate.message, identifier, sequenceNumber));
if (observation !== undefined) {
switch (observation.message.kind) {
case 'echoReply':
@@ -4533,11 +4531,12 @@ export class NetworkRuntimeEngine {
detail: 'TTL decremented',
});
// Java: a router whose ARP finds no one answers host unreachable; for
// ICMP only an echo request (ICMP.sendUnicastToNextHop).
// ICMP only an echo request (ICMP.sendUnicastToNextHop). The error carries
// the packet as the router forwarded it, its TTL lowered (measured).
const echoOrNotIcmp = packet.payload.kind !== 'icmp' || packet.payload.message.kind === 'echoRequest';
const result = this.sendIPv4PacketWithOptions(nodeId, forwarded, undefined, false, 'forwarded', {
onUnresolved: () => {
if (echoOrNotIcmp) this.emitIcmpError(nodeId, networkInterface, 'destinationHostUnreachable', packet);
if (echoOrNotIcmp) this.emitIcmpError(nodeId, networkInterface, 'destinationHostUnreachable', forwarded);
},
onFinished: finished,
});
@@ -4646,11 +4645,8 @@ export class NetworkRuntimeEngine {
protocol: 'icmp',
payload: {
kind: 'icmp',
message: icmpMessage(kind, {
identifier: icmpIdentifier(originalPacket),
sequenceNumber: icmpSequenceNumber(originalPacket),
embeddedOriginalPacket: embeddedOriginal(originalPacket),
}),
// Identifier and sequence number unused, 0 (RFC 792, Java's sendeICMP for errors).
message: icmpMessage(kind, { embeddedOriginalPacket: embeddedOriginal(originalPacket) }),
},
};
this.recordTrace({
@@ -4672,11 +4668,7 @@ export class NetworkRuntimeEngine {
kind: IcmpMessageKind,
originalPacket: IPv4Packet,
): void {
const message = icmpMessage(kind, {
identifier: icmpIdentifier(originalPacket),
sequenceNumber: icmpSequenceNumber(originalPacket),
embeddedOriginalPacket: embeddedOriginal(originalPacket),
});
const message = icmpMessage(kind, { embeddedOriginalPacket: embeddedOriginal(originalPacket) });
const packet: IPv4Packet = {
identity: this.allocatePacketIdentity(),
senderIPAddress: this.networkInterfaces(nodeId)[0]?.ipAddress ?? UNSPECIFIED_IP_ADDRESS,
@@ -5010,13 +5002,13 @@ function embeddedOriginal(packet: IPv4Packet): IPv4Packet {
return packet;
}
function icmpIdentifier(packet: IPv4Packet): number {
return packet.payload.kind === 'icmp' ? packet.payload.message.identifier : 0;
/** The echo reply to an echo request, or an error about it (it carries the request). */
function answersEcho(message: IcmpMessage, identifier: number, sequenceNumber: number): boolean {
if (message.kind === 'echoReply') return message.identifier === identifier && message.sequenceNumber === sequenceNumber;
const carried = message.embeddedOriginalPacket?.payload;
return carried?.kind === 'icmp' && carried.message.kind === 'echoRequest' && carried.message.identifier === identifier && carried.message.sequenceNumber === sequenceNumber;
}
function icmpSequenceNumber(packet: IPv4Packet): number {
return packet.payload.kind === 'icmp' ? packet.payload.message.sequenceNumber : 0;
}
export function ethernetHeaders(frame: EthernetFrame): HeaderField[] {
return [