|
|
|
@@ -52,8 +52,12 @@
|
|
|
|
|
// Calls that block in Java (resolving an address, waiting for an echo reply)
|
|
|
|
|
// advance the virtual clock inside the call, exactly as the Swift port does.
|
|
|
|
|
//
|
|
|
|
|
// Not ported yet — each place throws NotPortedError instead of guessing:
|
|
|
|
|
// remote links.
|
|
|
|
|
// Modems (Java Modem, Swift remote links) after Java's timing: a modem has no
|
|
|
|
|
// card and no delay of its own; a frame that reaches it leaves its partner at
|
|
|
|
|
// once (Java's TCP between the two FILIUS). The pair after Swift's inProject:
|
|
|
|
|
// the two enabled modems on a port. A modem of another project (`relay`)
|
|
|
|
|
// hands its frames to whoever runs the engine (server F's relay) and puts on
|
|
|
|
|
// its cable what comes from there.
|
|
|
|
|
|
|
|
|
|
import { compareIds, type Id } from '../ids.js';
|
|
|
|
|
import { forwardsIPv4ByDefault, isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js';
|
|
|
|
@@ -67,6 +71,7 @@ import type {
|
|
|
|
|
InterfaceConfiguration,
|
|
|
|
|
InterfaceKey,
|
|
|
|
|
ManualRoute,
|
|
|
|
|
RemoteLinkConfiguration,
|
|
|
|
|
} from './config.js';
|
|
|
|
|
import {
|
|
|
|
|
ALL_PORTS,
|
|
|
|
@@ -88,6 +93,7 @@ import {
|
|
|
|
|
TCP_FLAG,
|
|
|
|
|
type ArpPacket,
|
|
|
|
|
type EthernetFrame,
|
|
|
|
|
type EthernetPayload,
|
|
|
|
|
type IcmpMessage,
|
|
|
|
|
type IcmpMessageKind,
|
|
|
|
|
type IPv4Packet,
|
|
|
|
@@ -148,6 +154,28 @@ export const LOCALHOST_IP_ADDRESS = '127.0.0.1';
|
|
|
|
|
/** The seed a simulation uses unless told otherwise. */
|
|
|
|
|
export const DEFAULT_SEED = 0xf11a5n;
|
|
|
|
|
|
|
|
|
|
/** How often a frame may go through modems into other projects before it is dropped (two pairs between the same networks make a circle). */
|
|
|
|
|
export const MAXIMUM_RELAY_HOPS = 16;
|
|
|
|
|
|
|
|
|
|
/** How a modem stands (Swift TopologyRemoteLinkRuntimeCondition, without its LAN transport). */
|
|
|
|
|
export type RemoteLinkCondition =
|
|
|
|
|
| { readonly kind: 'stopped' }
|
|
|
|
|
| { readonly kind: 'missingConfiguration' }
|
|
|
|
|
| { readonly kind: 'disabled' }
|
|
|
|
|
| { readonly kind: 'unpaired' }
|
|
|
|
|
| { readonly kind: 'ambiguous'; readonly enabledNodeCount: number }
|
|
|
|
|
/** Its own cable or its partner's missing. */
|
|
|
|
|
| { readonly kind: 'detached'; readonly partnerNodeId?: Id }
|
|
|
|
|
| { readonly kind: 'active'; readonly partnerNodeId: Id }
|
|
|
|
|
/** A modem of another project, on its cable: whether it has a partner, the relay knows. */
|
|
|
|
|
| { readonly kind: 'relay' };
|
|
|
|
|
|
|
|
|
|
/** A modem's pair in the project, trimmed; undefined when it names none. */
|
|
|
|
|
function pairOf(configuration: RemoteLinkConfiguration): string | undefined {
|
|
|
|
|
const pair = configuration.pairIdentifier.trim();
|
|
|
|
|
return pair === '' ? undefined : pair;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** A part of the Swift engine that has not been carried over yet. */
|
|
|
|
|
export class NotPortedError extends Error {
|
|
|
|
|
constructor(feature: string) {
|
|
|
|
@@ -193,22 +221,7 @@ export type ScheduledEventKind =
|
|
|
|
|
readonly latencyMilliseconds: number;
|
|
|
|
|
readonly frame: EthernetFrame;
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
readonly kind: 'remoteLinkDelivery';
|
|
|
|
|
readonly sourceNodeId: Id;
|
|
|
|
|
readonly partnerNodeId: Id;
|
|
|
|
|
readonly partnerPortId: Id;
|
|
|
|
|
readonly pairIdentifier: string;
|
|
|
|
|
readonly latencyMilliseconds: number;
|
|
|
|
|
readonly frame: EthernetFrame;
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
readonly kind: 'lanRemoteLinkTransmission';
|
|
|
|
|
readonly nodeId: Id;
|
|
|
|
|
readonly pairIdentifier: string;
|
|
|
|
|
readonly latencyMilliseconds: number;
|
|
|
|
|
readonly frame: EthernetFrame;
|
|
|
|
|
};
|
|
|
|
|
;
|
|
|
|
|
|
|
|
|
|
export interface ScheduledEvent {
|
|
|
|
|
readonly id: number;
|
|
|
|
@@ -729,6 +742,8 @@ export interface RuntimeState {
|
|
|
|
|
switchForwardingTablesByNodeId: Map<Id, Map<string, Id>>;
|
|
|
|
|
switchForwardingUpdatedAtMillisecondsByNodeId: Map<Id, Map<string, number>>;
|
|
|
|
|
switchSeenFrameIdentitiesByNodeId: Map<Id, Set<number>>;
|
|
|
|
|
/** The frames each modem passed on (Swift's remote link cycle suppression). */
|
|
|
|
|
remoteLinkSeenFrameIdentitiesByNodeId: Map<Id, Set<number>>;
|
|
|
|
|
deliveredIPv4PacketsByNodeId: Map<Id, DeliveredIPv4Packet[]>;
|
|
|
|
|
icmpObservationsByNodeId: Map<Id, IcmpObservation[]>;
|
|
|
|
|
firewallDecisions: FirewallDecision[];
|
|
|
|
@@ -772,6 +787,7 @@ function initialState(seed: bigint): RuntimeState {
|
|
|
|
|
switchForwardingTablesByNodeId: new Map(),
|
|
|
|
|
switchForwardingUpdatedAtMillisecondsByNodeId: new Map(),
|
|
|
|
|
switchSeenFrameIdentitiesByNodeId: new Map(),
|
|
|
|
|
remoteLinkSeenFrameIdentitiesByNodeId: new Map(),
|
|
|
|
|
deliveredIPv4PacketsByNodeId: new Map(),
|
|
|
|
|
icmpObservationsByNodeId: new Map(),
|
|
|
|
|
firewallDecisions: [],
|
|
|
|
@@ -867,7 +883,6 @@ function isNetworkDeliveryEvent(kind: ScheduledEventKind): boolean {
|
|
|
|
|
return (
|
|
|
|
|
kind.kind === 'ethernetLinkDeparture' ||
|
|
|
|
|
kind.kind === 'ethernetLinkDelivery' ||
|
|
|
|
|
kind.kind === 'remoteLinkDelivery' ||
|
|
|
|
|
kind.kind === 'arpAnswered'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
@@ -951,6 +966,8 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
private readonly gnutellaPeersByNodeId = new Map<Id, GnutellaPeer>();
|
|
|
|
|
/** The time of day a program writes into a mail (Java's new Date()). */
|
|
|
|
|
private readonly wallClock: () => number;
|
|
|
|
|
/** Where a modem of another project's frames go (the constructor's `relay`). */
|
|
|
|
|
private readonly relay: ((nodeId: Id, frame: EthernetFrame) => void) | undefined;
|
|
|
|
|
/** Each node's firewall log; it outlives a stop and a new start, as Java's log page does. */
|
|
|
|
|
private readonly firewallLogsByNodeId = new Map<Id, LogLine<FirewallLogEntry>[]>();
|
|
|
|
|
/** Accepted connections a program has taken (acceptTcpSteps). */
|
|
|
|
@@ -973,6 +990,9 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
*
|
|
|
|
|
* `wallClock`: the time of day in milliseconds, which a program writes
|
|
|
|
|
* into a mail (the e-mail server's notice); the host's clock by default.
|
|
|
|
|
*
|
|
|
|
|
* `relay`: takes the frames a modem of another project passes on (server
|
|
|
|
|
* F's relay); without it they are lost, as at a modem that is not connected.
|
|
|
|
|
*/
|
|
|
|
|
constructor(
|
|
|
|
|
seed: bigint | number = DEFAULT_SEED,
|
|
|
|
@@ -984,9 +1004,11 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
readonly swiftRouters?: boolean;
|
|
|
|
|
readonly captureRetention?: CaptureRetentionPolicy;
|
|
|
|
|
readonly wallClock?: () => number;
|
|
|
|
|
readonly relay?: (nodeId: Id, frame: EthernetFrame) => void;
|
|
|
|
|
} = {},
|
|
|
|
|
) {
|
|
|
|
|
this.wallClock = options.wallClock ?? Date.now;
|
|
|
|
|
this.relay = options.relay;
|
|
|
|
|
this.packetCapture = new PacketCapture(options.captureRetention ?? PRODUCTION_CAPTURE_RETENTION);
|
|
|
|
|
this.cablesCarryOneFrameAtATime = options.swiftCables !== true;
|
|
|
|
|
this.swiftArp = options.swiftArp === true;
|
|
|
|
@@ -1096,6 +1118,7 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
state.switchForwardingTablesByNodeId.clear();
|
|
|
|
|
state.switchForwardingUpdatedAtMillisecondsByNodeId.clear();
|
|
|
|
|
state.switchSeenFrameIdentitiesByNodeId.clear();
|
|
|
|
|
state.remoteLinkSeenFrameIdentitiesByNodeId.clear();
|
|
|
|
|
state.deliveredIPv4PacketsByNodeId.clear();
|
|
|
|
|
state.icmpObservationsByNodeId.clear();
|
|
|
|
|
state.socketsById.clear();
|
|
|
|
@@ -1698,9 +1721,6 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
wake?.();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
case 'remoteLinkDelivery':
|
|
|
|
|
case 'lanRemoteLinkTransmission':
|
|
|
|
|
throw new NotPortedError('remote links');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4568,9 +4588,10 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
return this.stateValue.topologySnapshot.nodes.find((node) => node.id === nodeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Java logs a frame on a card; a switch has none. */
|
|
|
|
|
/** Java logs a frame on a card; a switch and a modem have none. */
|
|
|
|
|
private captureOnCard(nodeId: Id, portId: Id, frame: EthernetFrame): void {
|
|
|
|
|
if (this.nodeSnapshot(nodeId)?.kind === 'networkSwitch') return;
|
|
|
|
|
const kind = this.nodeSnapshot(nodeId)?.kind;
|
|
|
|
|
if (kind === 'networkSwitch' || kind === 'remoteLink') return;
|
|
|
|
|
this.packetCapture.record(portId, this.stateValue.currentTimeMilliseconds, frame);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4592,7 +4613,10 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
this.forwardFrameThroughSwitch(frame, node, incomingPortId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (node.kind === 'remoteLink') throw new NotPortedError('remote links');
|
|
|
|
|
if (node.kind === 'remoteLink') {
|
|
|
|
|
this.forwardFrameThroughRemoteLink(frame, node, incomingPortId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.recordTrace({
|
|
|
|
|
frameIdentity: frame.identity,
|
|
|
|
@@ -4633,6 +4657,138 @@ export class NetworkRuntimeEngine {
|
|
|
|
|
|
|
|
|
|
// ── Switch ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// ── Modems ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* How a modem stands (Swift's remoteLinkCondition): its partner in the
|
|
|
|
|
* project, or why it has none; `relay` for a modem of another project,
|
|
|
|
|
* whose partner only the relay knows. Undefined for anything but a modem.
|
|
|
|
|
*/
|
|
|
|
|
remoteLinkCondition(nodeId: Id): RemoteLinkCondition | undefined {
|
|
|
|
|
const node = this.nodeSnapshot(nodeId);
|
|
|
|
|
if (node === undefined || node.kind !== 'remoteLink') return undefined;
|
|
|
|
|
if (this.stateValue.phase !== 'running') return { kind: 'stopped' };
|
|
|
|
|
const configuration = this.stateValue.topologySnapshot.remoteLinkConfigurationsByNodeId.get(nodeId);
|
|
|
|
|
const pairIdentifier = configuration === undefined ? undefined : pairOf(configuration);
|
|
|
|
|
if (configuration === undefined || (pairIdentifier === undefined && configuration.relay === undefined)) return { kind: 'missingConfiguration' };
|
|
|
|
|
if (!configuration.isEnabled) return { kind: 'disabled' };
|
|
|
|
|
const attached = (id: Id) => {
|
|
|
|
|
const port = this.nodeSnapshot(id)?.ports[0];
|
|
|
|
|
return port !== undefined && this.attachedEndpoint(id, port.id) !== undefined;
|
|
|
|
|
};
|
|
|
|
|
if (configuration.relay !== undefined) return attached(nodeId) ? { kind: 'relay' } : { kind: 'detached' };
|
|
|
|
|
const matches = this.stateValue.topologySnapshot.nodes
|
|
|
|
|
.filter((candidate) => {
|
|
|
|
|
if (candidate.kind !== 'remoteLink') return false;
|
|
|
|
|
const other = this.stateValue.topologySnapshot.remoteLinkConfigurationsByNodeId.get(candidate.id);
|
|
|
|
|
return other !== undefined && other.isEnabled && other.relay === undefined && pairOf(other) === pairIdentifier;
|
|
|
|
|
})
|
|
|
|
|
.map((candidate) => candidate.id);
|
|
|
|
|
if (matches.length > 2) return { kind: 'ambiguous', enabledNodeCount: matches.length };
|
|
|
|
|
const partnerNodeId = matches.length === 2 ? matches.find((id) => id !== nodeId) : undefined;
|
|
|
|
|
if (partnerNodeId === undefined) return { kind: 'unpaired' };
|
|
|
|
|
return attached(nodeId) && attached(partnerNodeId) ? { kind: 'active', partnerNodeId } : { kind: 'detached', partnerNodeId };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A frame reached a modem on its cable: it leaves the partner modem at once
|
|
|
|
|
* (Java's modems pass it over TCP; no delay of their own), or goes to the
|
|
|
|
|
* relay for a modem of another project. Lost when the modem has no partner,
|
|
|
|
|
* as at Java's modem that is not connected.
|
|
|
|
|
*/
|
|
|
|
|
private forwardFrameThroughRemoteLink(frame: EthernetFrame, node: NodeSnapshot, incomingPortId: Id): void {
|
|
|
|
|
const drop = (detail: string) =>
|
|
|
|
|
this.recordTrace({
|
|
|
|
|
frameIdentity: frame.identity,
|
|
|
|
|
packetIdentity: packetIdentityInFrame(frame),
|
|
|
|
|
nodeId: node.id,
|
|
|
|
|
interfaceId: incomingPortId,
|
|
|
|
|
direction: 'inbound',
|
|
|
|
|
layer: 'dataLink',
|
|
|
|
|
operation: 'dropped',
|
|
|
|
|
beforeHeaders: ethernetHeaders(frame),
|
|
|
|
|
detail,
|
|
|
|
|
});
|
|
|
|
|
const seen = getOrCreate(this.stateValue.remoteLinkSeenFrameIdentitiesByNodeId, node.id, () => new Set());
|
|
|
|
|
if (seen.has(frame.identity)) {
|
|
|
|
|
drop('remote link cycle suppression');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
seen.add(frame.identity);
|
|
|
|
|
const condition = this.remoteLinkCondition(node.id);
|
|
|
|
|
if (condition?.kind === 'relay') {
|
|
|
|
|
const hops = frame.relayHops ?? 0;
|
|
|
|
|
if (hops >= MAXIMUM_RELAY_HOPS) {
|
|
|
|
|
drop('remote link relay hop limit');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.relay === undefined) {
|
|
|
|
|
drop('remote link relay unavailable');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.recordTrace({
|
|
|
|
|
frameIdentity: frame.identity,
|
|
|
|
|
packetIdentity: packetIdentityInFrame(frame),
|
|
|
|
|
nodeId: node.id,
|
|
|
|
|
interfaceId: incomingPortId,
|
|
|
|
|
direction: 'outbound',
|
|
|
|
|
layer: 'dataLink',
|
|
|
|
|
operation: 'forwarded',
|
|
|
|
|
beforeHeaders: ethernetHeaders(frame),
|
|
|
|
|
detail: 'remote link relayed',
|
|
|
|
|
});
|
|
|
|
|
this.relay(node.id, { ...frame, relayHops: hops + 1 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (condition?.kind !== 'active') {
|
|
|
|
|
drop(`remote link ${condition?.kind ?? 'missingConfiguration'}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const partnerPortId = this.nodeSnapshot(condition.partnerNodeId)!.ports[0]!.id;
|
|
|
|
|
getOrCreate(this.stateValue.remoteLinkSeenFrameIdentitiesByNodeId, condition.partnerNodeId, () => new Set()).add(frame.identity);
|
|
|
|
|
this.recordTrace({
|
|
|
|
|
frameIdentity: frame.identity,
|
|
|
|
|
packetIdentity: packetIdentityInFrame(frame),
|
|
|
|
|
nodeId: condition.partnerNodeId,
|
|
|
|
|
interfaceId: partnerPortId,
|
|
|
|
|
direction: 'outbound',
|
|
|
|
|
layer: 'dataLink',
|
|
|
|
|
operation: 'forwarded',
|
|
|
|
|
beforeHeaders: ethernetHeaders(frame),
|
|
|
|
|
detail: `remote link delivered pair=${pairOf(this.stateValue.topologySnapshot.remoteLinkConfigurationsByNodeId.get(node.id)!)}`,
|
|
|
|
|
});
|
|
|
|
|
this.sendEthernetFrame(condition.partnerNodeId, partnerPortId, frame);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A frame that came from the partner of a modem of another project (server
|
|
|
|
|
* F's relay): it leaves the modem on its cable, with this engine's numbers
|
|
|
|
|
* for the frame and its packet — the other engine counts its own, and a
|
|
|
|
|
* switch would take a number it saw before for the same frame. False when
|
|
|
|
|
* the simulation does not run or the modem cannot take it.
|
|
|
|
|
*/
|
|
|
|
|
receiveRelayedFrame(nodeId: Id, frame: EthernetFrame): boolean {
|
|
|
|
|
if (this.remoteLinkCondition(nodeId)?.kind !== 'relay') return false;
|
|
|
|
|
const portId = this.nodeSnapshot(nodeId)!.ports[0]!.id;
|
|
|
|
|
const payload: EthernetPayload =
|
|
|
|
|
frame.payload.kind === 'ipv4' ? { kind: 'ipv4', packet: { ...frame.payload.packet, identity: this.allocatePacketIdentity() } } : frame.payload;
|
|
|
|
|
const local: EthernetFrame = { ...frame, identity: this.allocateFrameIdentity(), payload };
|
|
|
|
|
getOrCreate(this.stateValue.remoteLinkSeenFrameIdentitiesByNodeId, nodeId, () => new Set()).add(local.identity);
|
|
|
|
|
this.recordTrace({
|
|
|
|
|
frameIdentity: local.identity,
|
|
|
|
|
packetIdentity: packetIdentityInFrame(local),
|
|
|
|
|
nodeId,
|
|
|
|
|
interfaceId: portId,
|
|
|
|
|
direction: 'inbound',
|
|
|
|
|
layer: 'dataLink',
|
|
|
|
|
operation: 'received',
|
|
|
|
|
afterHeaders: ethernetHeaders(local),
|
|
|
|
|
detail: 'remote link relayed',
|
|
|
|
|
});
|
|
|
|
|
this.sendEthernetFrame(nodeId, portId, local);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private forwardFrameThroughSwitch(frame: EthernetFrame, switchNode: NodeSnapshot, incomingPortId: Id): void {
|
|
|
|
|
const state = this.stateValue;
|
|
|
|
|
const seen = state.switchSeenFrameIdentitiesByNodeId.get(switchNode.id);
|
|
|
|
|