feat(core): modems run — the pair in the project, and a modem of another project hands its frames on (K0–K2)

Measured first: tools/java-oracle's ModemRun joins two networks by a pair
of Java modems over TCP on localhost. The modem has no card and adds no
delay of its own — a frame takes the cables on both sides, nothing more.

The core pairs the two enabled modems on a port (Swift's inProject, with
its conditions and cycle suppression) on Java's timing: no 20 ms. The
modem example of FILIUS now pings through instead of failing the core.
A modem of another project (relay) gives its frames to whoever runs the
engine and puts on its cable what comes back, with this engine's frame
and packet numbers; a frame dies after MAXIMUM_RELAY_HOPS trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:12:27 +00:00
co-authored by Claude Opus 5.5
parent 6f583616f6
commit c85b0e45da
19 changed files with 1743 additions and 44 deletions
+14 -3
View File
@@ -139,14 +139,25 @@ export interface GatewayPortForwardingRow {
}
/**
* The in-project pairing of two remote links (Java Modem). Swift's LAN
* transport settings belong to its Network-framework controller and are not
* part of the port.
* Where a modem's partner is when it is in another project (server F's
* relay): this modem offers a connection under a code derived from its
* project and itself, or joins the modem that offers `code`. Java's modem
* names the other FILIUS by IP address and port; a code takes their place.
*/
export type RemoteLinkRelay = { readonly role: 'offer' } | { readonly role: 'join'; readonly code: string };
/**
* A modem (Java Modem, Swift remote link): paired in the project with the one
* other modem on its port (`pairIdentifier`), or, with `relay`, with a modem
* of another project. Swift's LAN transport settings belong to its
* Network-framework controller and are not part of the port.
*/
export interface RemoteLinkConfiguration {
readonly pairIdentifier: string;
readonly latencyMilliseconds: number;
readonly isEnabled: boolean;
/** A modem of another project; none: the pair in this project. */
readonly relay?: RemoteLinkRelay;
}
export const DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS = 20;
+181 -25
View File
@@ -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);
+2
View File
@@ -8,6 +8,8 @@ export interface EthernetFrame {
readonly sourceMacAddress: string;
readonly destinationMacAddress: string;
readonly payload: EthernetPayload;
/** How often the frame went through a modem into another project (server F's relay); none: never. */
readonly relayHops?: number;
}
export type EthernetPayload =
+5
View File
@@ -19,6 +19,7 @@ import type {
InterfaceKey,
ManualRoute,
SimpleClientConfiguration,
RemoteLinkConfiguration,
SwitchConfiguration,
WebServerConfiguration,
EmailServerAccount,
@@ -95,6 +96,8 @@ export interface TopologySnapshot {
/** A home router's port forwarding (Java GatewayFirmware.staticNAT), by node. */
readonly portForwardingRowsByNodeId: ReadonlyMap<Id, readonly GatewayPortForwardingRow[]>;
readonly switchConfigurationsByNodeId: ReadonlyMap<Id, SwitchConfiguration>;
/** The modems' pairs and relays (Java Modem), by node. */
readonly remoteLinkConfigurationsByNodeId: ReadonlyMap<Id, RemoteLinkConfiguration>;
/** Nodes whose IP forwarding differs from their kind's (forwardsIPv4ByDefault): a host that forwards. */
readonly ipForwardingByNodeId: ReadonlyMap<Id, boolean>;
/** The installed echo servers (ServerBaustein), by node. */
@@ -132,6 +135,7 @@ export interface TopologySnapshotInput {
firewallConfigurationsByNodeId?: ReadonlyMap<Id, FirewallConfiguration>;
portForwardingRowsByNodeId?: ReadonlyMap<Id, readonly GatewayPortForwardingRow[]>;
switchConfigurationsByNodeId?: ReadonlyMap<Id, SwitchConfiguration>;
remoteLinkConfigurationsByNodeId?: ReadonlyMap<Id, RemoteLinkConfiguration>;
ipForwardingByNodeId?: ReadonlyMap<Id, boolean>;
echoServersByNodeId?: ReadonlyMap<Id, EchoServerSnapshot>;
simpleClientsByNodeId?: ReadonlyMap<Id, SimpleClientConfiguration>;
@@ -159,6 +163,7 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot
firewallConfigurationsByNodeId: input.firewallConfigurationsByNodeId ?? new Map(),
portForwardingRowsByNodeId: input.portForwardingRowsByNodeId ?? new Map(),
switchConfigurationsByNodeId: input.switchConfigurationsByNodeId ?? new Map(),
remoteLinkConfigurationsByNodeId: input.remoteLinkConfigurationsByNodeId ?? new Map(),
ipForwardingByNodeId: input.ipForwardingByNodeId ?? new Map(),
echoServersByNodeId: input.echoServersByNodeId ?? new Map(),
simpleClientsByNodeId: input.simpleClientsByNodeId ?? new Map(),