feat(core): ARP after Java, measured with Java FILIUS

An ARP lookup asks, waits a round trip, asks once more and gives up after
another round trip; then host unreachable, observed at the sender itself or
sent back by the router (for ICMP only for an echo request). A waiting
sender wakes after what else happens at that time. A ping then waits a
round trip for its answer, counted from when it left. Swift asks twice at
once while events run and lets the packet wait without end when the
address exists anywhere in the network; the option swiftArp keeps that for
the Swift comparison, and three scenarios are compared with Swift until the
trace where ARP fails or a ping goes unanswered.

arpJava.test.ts compares ArpRun: both pings match; the two UDP stories wait
for Java's transport thread (next step). DNS without a server now matches
Java too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:11:23 +00:00
co-authored by Claude Opus 5.5
parent 67fb608412
commit dbbc1fd70d
8 changed files with 414 additions and 152 deletions
+231 -135
View File
@@ -17,6 +17,9 @@
// processScheduledRuntimeEvent, dropForGlobalPacketLoss (lines ~4753–4850)
// A cable carries one frame after the other, as Java's SimplexVerbindung
// (Swift: all at once; the option swiftCables keeps that for the comparison).
// ARP after Java's holeARPTabellenEintrag: a lookup asks twice, a round trip
// each, then host unreachable (Swift: all at once, or without end when the
// address exists anywhere; the option swiftArp keeps that for the comparison).
// DNS after Java (filius.software.dns: DNSServer, DNSServerMitarbeiter,
// Resolver, DNSQueryAgent), not Swift's typed format; recursion not yet.
// Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht,
@@ -52,7 +55,6 @@ import {
firewallConfiguration,
interfaceKey,
} from './config.js';
import { deepEqual } from '../util/equal.js';
import { maskAddress, parseIPv4, sameSubnet } from './ipv4.js';
import {
forwardingClone,
@@ -120,6 +122,10 @@ export type ScheduledEventKind =
| { readonly kind: 'ripBeacon'; readonly nodeId: Id }
| { readonly kind: 'dhcpClientStart'; readonly nodeId: Id }
| { readonly kind: 'dhcpTimeout'; readonly nodeId: Id }
/** An ARP lookup waited a round trip for its answer. */
| { readonly kind: 'arpTimeout'; readonly lookupId: number }
/** The lookups waiting for an address wake up, after what else happens at that time. */
| { readonly kind: 'arpAnswered'; readonly nodeId: Id; readonly ipAddress: string; readonly macAddress: string }
| { readonly kind: 'tcpTimeout'; readonly sessionId: Id }
| {
/** A frame that waited in the port's output buffer leaves: the cable was busy. */
@@ -522,10 +528,26 @@ type PacketRoute =
| { readonly type: 'egress'; readonly interface: NetworkInterface; readonly nextHop: string }
| { readonly type: 'unavailable' };
interface PendingIPv4Transmission {
readonly packet: IPv4Packet;
readonly interface: NetworkInterface;
readonly operation: TraceOperation;
/** Java's IP asks ARP this many times, a round trip each (holeARPTabellenEintrag(ziel, 2, …)). */
export const ARP_ATTEMPTS = 2;
interface ArpLookup {
readonly id: number;
readonly nodeId: Id;
readonly ipAddress: string;
readonly networkInterface: NetworkInterface;
attemptsLeft: number;
/** Swift, for the comparison: a lookup made while events run waits in a list emptied by the answer itself. */
readonly swiftQueued: boolean;
readonly done: (macAddress: string | undefined) => void;
}
/** What a sender learns when its packet has left, or when it could not. */
interface IPv4SendCallbacks {
/** The packet left (or failed) — at once, or when ARP has answered or given up. */
readonly onFinished?: (result: IPv4DeliveryResult) => void;
/** ARP found no one for the next hop (Java: the sender sends host unreachable). */
readonly onUnresolved?: () => void;
}
function toSeed(seed: bigint | number): bigint {
@@ -534,12 +556,14 @@ function toSeed(seed: bigint | number): bigint {
/** A departure waits for the delivery of the frame before it, so draining the deliveries reaches it. */
function isNetworkDeliveryEvent(kind: ScheduledEventKind): boolean {
return kind.kind === 'ethernetLinkDeparture' || kind.kind === 'ethernetLinkDelivery' || kind.kind === 'remoteLinkDelivery';
return (
kind.kind === 'ethernetLinkDeparture' ||
kind.kind === 'ethernetLinkDelivery' ||
kind.kind === 'remoteLinkDelivery' ||
kind.kind === 'arpAnswered'
);
}
function isRemoteLinkWaitEvent(kind: ScheduledEventKind): boolean {
return isNetworkDeliveryEvent(kind) || kind.kind === 'lanRemoteLinkTransmission';
}
function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V {
let value = map.get(key);
@@ -562,10 +586,13 @@ export class NetworkRuntimeEngine {
private runtimeSpeed: RuntimeSpeed;
private readonly packetTraceRetentionPolicy: TraceRetentionPolicy;
private isProcessingScheduledEvents = false;
private pendingIPv4TransmissionsByArpKey = new Map<string, PendingIPv4Transmission[]>();
/** ARP lookups waiting for their answer (Java: a thread in holeARPTabellenEintrag). */
private arpLookups: ArpLookup[] = [];
private nextArpLookupId = 1;
/** Per cable end (node and port): when the last frame sent there arrives. */
private cableBusyUntilByPort = new Map<string, number>();
private readonly cablesCarryOneFrameAtATime: boolean;
private readonly swiftArp: boolean;
/**
* `swiftCables`: a cable carries all frames at once, as in the Swift
@@ -577,9 +604,10 @@ export class NetworkRuntimeEngine {
constructor(
seed: bigint | number = DEFAULT_SEED,
packetTraceRetentionPolicy: TraceRetentionPolicy = PRODUCTION_TRACE_RETENTION,
options: { readonly swiftCables?: boolean } = {},
options: { readonly swiftCables?: boolean; readonly swiftArp?: boolean } = {},
) {
this.cablesCarryOneFrameAtATime = options.swiftCables !== true;
this.swiftArp = options.swiftArp === true;
const normalizedSeed = toSeed(seed);
this.stateValue = initialState(normalizedSeed);
this.random = new JavaCompatibleRandom(normalizedSeed);
@@ -626,7 +654,6 @@ export class NetworkRuntimeEngine {
this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n);
this.dnsIdRandom = new JavaCompatibleRandom(seed ^ 0x444e_5349n);
this.isProcessingScheduledEvents = false;
this.pendingIPv4TransmissionsByArpKey.clear();
this.cableBusyUntilByPort.clear();
this.initializeRipState();
this.initializeDhcpState();
@@ -641,7 +668,7 @@ export class NetworkRuntimeEngine {
state.phase = 'stopped';
state.globalPacketLossEnabled = false;
this.isProcessingScheduledEvents = false;
this.pendingIPv4TransmissionsByArpKey.clear();
this.arpLookups = [];
state.pendingEvents = [];
state.arpCachesByNodeId.clear();
state.switchForwardingTablesByNodeId.clear();
@@ -834,6 +861,12 @@ export class NetworkRuntimeEngine {
case 'dhcpTimeout':
this.handleDhcpClientTimeout(kind.nodeId);
return;
case 'arpTimeout':
this.handleArpTimeout(kind.lookupId);
return;
case 'arpAnswered':
this.completeArpLookups(kind.nodeId, kind.ipAddress, kind.macAddress);
return;
case 'tcpTimeout':
this.handleTcpTimeout(kind.sessionId);
return;
@@ -2651,15 +2684,15 @@ export class NetworkRuntimeEngine {
/**
* The MAC address for `targetIPAddress`, asking by ARP broadcast when it is
* not cached. Blocks like Java: the frames travel (virtual time passes)
* before the call returns.
* not cached. Blocks like Java: each request waits a round trip for its
* answer (virtual time passes) before the next, or before the call gives up.
*/
resolveMacAddress(
nodeId: Id,
targetIPAddress: string,
options: { preferredInterfaceId?: Id | undefined; maxRetries?: number; selectInterfaceByAddress?: boolean } = {},
): string | undefined {
const maxRetries = options.maxRetries ?? 2;
const maxRetries = options.maxRetries ?? ARP_ATTEMPTS;
const selectInterfaceByAddress = options.selectInterfaceByAddress ?? true;
const interfaces = this.networkInterfaces(nodeId);
if (interfaces.length === 0) return undefined;
@@ -2676,13 +2709,76 @@ export class NetworkRuntimeEngine {
}
if (selectedInterface === undefined) return undefined;
for (let attempt = 0; attempt < Math.max(0, maxRetries); attempt += 1) {
this.sendArpRequest(nodeId, selectedInterface, targetIPAddress);
this.advanceRemoteLinkEventsUntilArpCacheResolves(nodeId, targetIPAddress);
const resolved = this.stateValue.arpCachesByNodeId.get(nodeId)?.get(targetIPAddress);
if (resolved !== undefined) return resolved.macAddress;
let finished = false;
let resolved: string | undefined;
this.lookUpMacAddress(nodeId, targetIPAddress, selectedInterface, maxRetries, (macAddress) => {
finished = true;
resolved = macAddress;
});
if (!finished && !this.isProcessingScheduledEvents) {
this.advanceUntil(() => finished, this.stateValue.currentTimeMilliseconds + (maxRetries + 1) * this.runtimeSpeed.roundTripTimeMilliseconds);
}
return undefined;
return resolved;
}
/**
* Java's holeARPTabellenEintrag as a lookup of its own: a known address at
* once; otherwise an ARP request and a round trip's wait for the answer,
* `attempts` times, then undefined. Java wakes on any change of the ARP
* table and gives up when it finds nothing; the lookup waits for its
* address (as the DHCP client's check does).
*/
private lookUpMacAddress(
nodeId: Id,
ipAddress: string,
networkInterface: NetworkInterface,
attempts: number,
done: (macAddress: string | undefined) => void,
): void {
const known = this.knownMacAddress(nodeId, ipAddress);
if (known !== undefined || attempts <= 0) {
done(known);
return;
}
const swiftQueued = this.swiftArp && this.isProcessingScheduledEvents;
const lookup: ArpLookup = { id: this.nextArpLookupId++, nodeId, ipAddress, networkInterface, attemptsLeft: attempts - 1, swiftQueued, done };
this.arpLookups.push(lookup);
if (swiftQueued) {
// Swift, only for the comparison: every request at once, then the packet waits without end.
for (let attempt = 0; attempt < attempts; attempt += 1) this.sendArpRequest(nodeId, networkInterface, ipAddress);
lookup.attemptsLeft = 0;
return;
}
this.schedule(this.stateValue.currentTimeMilliseconds + this.runtimeSpeed.roundTripTimeMilliseconds, { kind: 'arpTimeout', lookupId: lookup.id });
this.sendArpRequest(nodeId, networkInterface, ipAddress);
}
/** The node learned `ipAddress`: every lookup waiting for it has its answer. */
private completeArpLookups(nodeId: Id, ipAddress: string, macAddress: string, onlySwiftQueued = false): void {
const answered = this.arpLookups.filter(
(lookup) => lookup.nodeId === nodeId && lookup.ipAddress === ipAddress && (!onlySwiftQueued || lookup.swiftQueued),
);
if (answered.length === 0) return;
const ids = new Set(answered.map((lookup) => lookup.id));
this.arpLookups = this.arpLookups.filter((lookup) => !ids.has(lookup.id));
this.stateValue.pendingEvents = this.stateValue.pendingEvents.filter(
(event) => !(event.kind.kind === 'arpTimeout' && ids.has(event.kind.lookupId)),
);
for (const lookup of answered) lookup.done(macAddress);
}
/** No answer within the round trip: ask again, or give up. */
private handleArpTimeout(lookupId: number): void {
const lookup = this.arpLookups.find((each) => each.id === lookupId);
if (lookup === undefined) return;
if (lookup.attemptsLeft > 0) {
lookup.attemptsLeft -= 1;
this.schedule(this.stateValue.currentTimeMilliseconds + this.runtimeSpeed.roundTripTimeMilliseconds, { kind: 'arpTimeout', lookupId });
this.sendArpRequest(lookup.nodeId, lookup.networkInterface, lookup.ipAddress);
return;
}
this.arpLookups = this.arpLookups.filter((each) => each.id !== lookupId);
lookup.done(undefined);
}
/** The MAC address for `ipAddress` without asking: localhost, one of the node's own, or cached. */
@@ -2724,39 +2820,6 @@ export class NetworkRuntimeEngine {
this.sendEthernetFrame(nodeId, networkInterface.portId, frame);
}
private advanceRemoteLinkEventsUntilArpCacheResolves(nodeId: Id, targetIPAddress: string): void {
if (this.isProcessingScheduledEvents) return;
this.advancePendingRemoteLinkEvents(
() => this.stateValue.arpCachesByNodeId.get(nodeId)?.get(targetIPAddress) !== undefined,
);
}
private advanceRemoteLinkEventsUntilIcmpObservation(
nodeId: Id,
observationCount: number,
identifier: number,
sequenceNumber: number,
): void {
if (this.isProcessingScheduledEvents) return;
this.advancePendingRemoteLinkEvents(() => {
const newObservations = (this.stateValue.icmpObservationsByNodeId.get(nodeId) ?? []).slice(observationCount);
return newObservations.some(
(observation) =>
observation.message.identifier === identifier && observation.message.sequenceNumber === sequenceNumber,
);
});
}
private advancePendingRemoteLinkEvents(condition: () => boolean): void {
const maximumEvents = 1_024;
let processedEventCount = 0;
while (!condition() && processedEventCount < maximumEvents) {
const nextDeadline = this.earliestDeadline(isRemoteLinkWaitEvent);
if (nextDeadline === undefined) return;
this.advance(nextDeadline);
processedEventCount += 1;
}
}
private earliestDeadline(filter: (kind: ScheduledEventKind) => boolean): number | undefined {
let earliest: number | undefined;
@@ -3086,7 +3149,16 @@ export class NetworkRuntimeEngine {
macAddress: packet.senderMacAddress,
updatedAtMilliseconds: this.stateValue.currentTimeMilliseconds,
});
this.flushIPv4TransmissionsAwaitingArp(nodeId, packet.senderIPAddress, packet.senderMacAddress);
// Java's waiting thread wakes when the ARP thread is done (Swift, for the comparison: at once).
this.completeArpLookups(nodeId, packet.senderIPAddress, packet.senderMacAddress, true);
if (this.arpLookups.some((lookup) => lookup.nodeId === nodeId && lookup.ipAddress === packet.senderIPAddress)) {
this.schedule(this.stateValue.currentTimeMilliseconds, {
kind: 'arpAnswered',
nodeId,
ipAddress: packet.senderIPAddress,
macAddress: packet.senderMacAddress,
});
}
this.dhcpClientLearnedAddress(nodeId, packet.senderIPAddress);
}
this.recordTrace({
@@ -3191,8 +3263,27 @@ export class NetworkRuntimeEngine {
detail: 'ICMP echo request',
});
const observationCount = this.stateValue.icmpObservationsByNodeId.get(fromNodeId)?.length ?? 0;
const result = this.sendIPv4Packet(fromNodeId, packet, sourceInterface.portId);
this.advanceRemoteLinkEventsUntilIcmpObservation(fromNodeId, observationCount, identifier, sequenceNumber);
const answered = () =>
(this.stateValue.icmpObservationsByNodeId.get(fromNodeId) ?? [])
.slice(observationCount)
.some((observation) => observation.message.identifier === identifier && observation.message.sequenceNumber === sequenceNumber);
let sent: IPv4DeliveryResult | undefined;
let sentAt = this.stateValue.currentTimeMilliseconds;
const handedOn = this.sendIPv4PacketWithOptions(fromNodeId, packet, sourceInterface.portId, true, 'sent', {
onFinished: (outcome) => {
sent = outcome;
sentAt = this.stateValue.currentTimeMilliseconds;
},
});
if (!this.isProcessingScheduledEvents) {
// Java's sendProbe: the caller waits for ARP (up to two round trips),
// then a round trip for the answer (ICMPThread.waitForIcmpResponse),
// counted from when the packet left — the engine may have run past it.
const rtt = this.runtimeSpeed.roundTripTimeMilliseconds;
this.advanceUntil(() => sent !== undefined, this.stateValue.currentTimeMilliseconds + (ARP_ATTEMPTS + 1) * rtt);
if (sentAt + rtt > this.stateValue.currentTimeMilliseconds) this.advanceUntil(answered, sentAt + rtt);
}
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,
@@ -3254,8 +3345,78 @@ export class NetworkRuntimeEngine {
preferredInterfaceId: Id | undefined,
allowIcmpErrorGeneration: boolean,
operation: TraceOperation,
callbacks: IPv4SendCallbacks = {},
): IPv4DeliveryResult {
const route = this.routeForPacket(fromNodeId, packet.receiverIPAddress, preferredInterfaceId);
if (route.type === 'egress') return this.sendIPv4ToNextHop(fromNodeId, packet, route, allowIcmpErrorGeneration, operation, callbacks);
const result = this.sendIPv4Directly(fromNodeId, packet, route, preferredInterfaceId, allowIcmpErrorGeneration, operation);
callbacks.onFinished?.(result);
return result;
}
/**
* To the next hop, once ARP knows it; until then the packet waits for its
* lookup and the result says it was handed on. When ARP finds no one: host
* unreachable — observed at the sender itself, or sent back by the router
* (onUnresolved).
*/
private sendIPv4ToNextHop(
fromNodeId: Id,
packet: IPv4Packet,
route: { readonly interface: NetworkInterface; readonly nextHop: string },
allowIcmpErrorGeneration: boolean,
operation: TraceOperation,
callbacks: IPv4SendCallbacks,
): IPv4DeliveryResult {
let finished: IPv4DeliveryResult | undefined;
this.lookUpMacAddress(fromNodeId, route.nextHop, route.interface, ARP_ATTEMPTS, (macAddress) => {
if (macAddress !== undefined) {
this.transmitIPv4Packet(fromNodeId, packet, route.interface, macAddress, operation);
finished = { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
} else {
this.recordTrace({
packetIdentity: packet.identity,
nodeId: fromNodeId,
interfaceId: route.interface.portId,
direction: 'outbound',
layer: 'network',
operation: 'dropped',
beforeHeaders: ipv4Headers(packet),
detail: `no ARP answer for ${route.nextHop}`,
});
if (allowIcmpErrorGeneration) {
this.observeLocalIcmpError(fromNodeId, route.interface.portId, 'destinationHostUnreachable', packet);
finished = { type: 'icmpError', packetIdentity: packet.identity, nodeId: fromNodeId, kind: 'destinationHostUnreachable' };
} else {
callbacks.onUnresolved?.();
finished = { type: 'dropped', packetIdentity: packet.identity, nodeId: fromNodeId };
}
}
callbacks.onFinished?.(finished);
});
if (finished === undefined && this.swiftArp && this.isProcessingScheduledEvents) {
this.recordTrace({
packetIdentity: packet.identity,
nodeId: fromNodeId,
interfaceId: route.interface.portId,
direction: 'outbound',
layer: 'network',
operation: 'created',
beforeHeaders: ipv4Headers(packet),
detail: `waiting for ARP resolution of ${route.nextHop}`,
});
}
return finished ?? { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
}
private sendIPv4Directly(
fromNodeId: Id,
packet: IPv4Packet,
route: PacketRoute,
preferredInterfaceId: Id | undefined,
allowIcmpErrorGeneration: boolean,
operation: TraceOperation,
): IPv4DeliveryResult {
switch (route.type) {
case 'local': {
const networkInterface = route.interface ?? this.networkInterfaces(fromNodeId)[0];
@@ -3281,30 +3442,8 @@ export class NetworkRuntimeEngine {
return { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
}
case 'egress': {
const destinationMacAddress = this.resolveMacAddress(fromNodeId, route.nextHop, {
preferredInterfaceId: route.interface.portId,
maxRetries: 2,
});
if (destinationMacAddress === undefined) {
if (this.isProcessingScheduledEvents && this.hasConfiguredInterface(route.nextHop)) {
this.enqueueIPv4TransmissionAwaitingArp(fromNodeId, route.nextHop, packet, route.interface, operation);
return { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
}
if (allowIcmpErrorGeneration) {
this.observeLocalIcmpError(fromNodeId, route.interface.portId, 'destinationHostUnreachable', packet);
return {
type: 'icmpError',
packetIdentity: packet.identity,
nodeId: fromNodeId,
kind: 'destinationHostUnreachable',
};
}
return { type: 'dropped', packetIdentity: packet.identity, nodeId: fromNodeId };
}
this.transmitIPv4Packet(fromNodeId, packet, route.interface, destinationMacAddress, operation);
return { type: 'delivered', packetIdentity: packet.identity, nodeId: fromNodeId };
}
case 'egress':
return this.sendIPv4ToNextHop(fromNodeId, packet, route, allowIcmpErrorGeneration, operation, {});
case 'unavailable':
if (allowIcmpErrorGeneration) {
@@ -3320,46 +3459,6 @@ export class NetworkRuntimeEngine {
}
}
private hasConfiguredInterface(ipAddress: string): boolean {
return this.stateValue.topologySnapshot.nodes.some((node) =>
this.networkInterfaces(node.id).some((candidate) => candidate.ipAddress === ipAddress),
);
}
private enqueueIPv4TransmissionAwaitingArp(
fromNodeId: Id,
nextHop: string,
packet: IPv4Packet,
networkInterface: NetworkInterface,
operation: TraceOperation,
): void {
const key = arpKey(fromNodeId, nextHop);
const transmission: PendingIPv4Transmission = { packet, interface: networkInterface, operation };
const pending = this.pendingIPv4TransmissionsByArpKey.get(key);
if (pending?.some((candidate) => deepEqual(candidate, transmission)) === true) return;
getOrCreate(this.pendingIPv4TransmissionsByArpKey, key, () => []).push(transmission);
this.recordTrace({
packetIdentity: packet.identity,
nodeId: fromNodeId,
interfaceId: networkInterface.portId,
direction: 'outbound',
layer: 'network',
operation: 'created',
beforeHeaders: ipv4Headers(packet),
detail: `waiting for ARP resolution of ${nextHop}`,
});
}
private flushIPv4TransmissionsAwaitingArp(nodeId: Id, ipAddress: string, macAddress: string): void {
const key = arpKey(nodeId, ipAddress);
const transmissions = this.pendingIPv4TransmissionsByArpKey.get(key);
if (transmissions === undefined) return;
this.pendingIPv4TransmissionsByArpKey.delete(key);
for (const transmission of transmissions) {
this.transmitIPv4Packet(nodeId, transmission.packet, transmission.interface, macAddress, transmission.operation);
}
}
private transmitIPv4Packet(
fromNodeId: Id,
packet: IPv4Packet,
@@ -3451,15 +3550,16 @@ export class NetworkRuntimeEngine {
afterHeaders: ipv4Headers(forwarded),
detail: 'TTL decremented',
});
const result = this.sendIPv4PacketWithOptions(nodeId, forwarded, undefined, false, 'forwarded');
if (result.type === 'dropped') {
const route = this.routeForPacket(nodeId, packetForProcessing.receiverIPAddress);
this.emitIcmpError(
nodeId,
networkInterface,
route.type === 'unavailable' ? 'destinationNetworkUnreachable' : 'destinationHostUnreachable',
packet,
);
// Java: a router whose ARP finds no one answers host unreachable; for
// ICMP only an echo request (ICMP.sendUnicastToNextHop).
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 (result.type === 'dropped' && this.routeForPacket(nodeId, packetForProcessing.receiverIPAddress).type === 'unavailable') {
this.emitIcmpError(nodeId, networkInterface, 'destinationNetworkUnreachable', packet);
}
}
@@ -3900,10 +4000,6 @@ function portKey(nodeId: Id, portId: Id): string {
return `${nodeId}|${portId}`;
}
function arpKey(nodeId: Id, ipAddress: string): string {
return `${nodeId}|${ipAddress}`;
}
function bestInterface(targetIPAddress: string, interfaces: readonly NetworkInterface[]): NetworkInterface | undefined {
let selected: NetworkInterface | undefined;
let selectedMask: number | undefined;