Files
julian.kiedaischandClaude Opus 5.5 c85b0e45da 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>
2026-10-11 18:12:27 +00:00

213 lines
7.8 KiB
TypeScript

// A core scenario (test/scenarios/<name>.scenario.json): a network, its
// settings and the steps to run on it. Read by the Swift comparison
// (oracle.test.ts) and the Java comparison (dhcpJava.test.ts).
import {
deviceConfiguration,
dhcpServerConfiguration,
firewallConfiguration,
firewallRule,
interfaceKey,
portSnapshot,
switchConfiguration,
topologySnapshot,
type FirewallConfiguration,
type ManualRoute,
type NodeKind,
type TopologySnapshot,
type DnsRecordType,
} from '../src/index.js';
export interface Scenario {
seed: number;
speedPercent?: number;
nodes: { id: string; kind: NodeKind; ports: { id: string; label: string; mac?: string }[] }[];
/** `wireless`: a wireless cable (Java only: Kabel.wireless; a cable like any other at run time). */
links: { id: string; sourceNode: string; sourcePort: string; targetNode: string; targetPort: string; wireless?: boolean }[];
devices?: Record<string, { ip: string; mask: string; gateway?: string; dns?: string }>;
interfaces?: { node: string; port: string; ip: string; mask: string }[];
routes?: Record<string, { destination: string; mask: string; gateway: string; interface: string }[]>;
switches?: Record<string, { ssid: string; retention?: number }>;
/** Hosts with a wireless card and the SSID they join (Java only: the editor lays the cable, the simulation does not ask). */
wireless?: Record<string, { ssid: string }>;
firewalls?: Record<
string,
{
active?: boolean;
defaultPolicy?: 'accept' | 'drop';
dropIcmp?: boolean;
synOnly?: boolean;
filterUdp?: boolean;
rules?: {
source?: string;
sourceMask?: string;
destination?: string;
destinationMask?: string;
port?: number;
protocol?: 'all' | 'icmp' | 'tcp' | 'udp';
action?: 'accept' | 'drop';
}[];
}
>;
dhcpServers?: Record<
string,
{ active?: boolean; lower?: string; upper?: string; gateway?: string; dns?: string; own?: boolean; staticAssignments?: [string, string][] }
>;
dhcpClients?: string[];
/** Home routers' port forwarding: rows of protocol, public port, LAN address, LAN port. */
portForwarding?: Record<string, [string, string, string, string][]>;
/** Routers with RIP switched on. */
rip?: string[];
/**
* Modems: Java pairs a server and a client on a port over TCP; the core
* pairs the two modems on a port (the mode is Java's only).
*/
modems?: Record<string, { mode: 'server' | 'client'; port: number }>;
/** Records as `name TYPE target` (Java's hinzuRecord: TTL 3600). */
dnsServers?: Record<string, { records: string[]; recursive?: boolean }>;
steps: {
op: string;
from?: string;
node?: string;
target?: string;
ttl?: number;
identifier?: number;
sequence?: number;
maxTtl?: number;
to?: number;
enabled?: boolean;
percent?: number;
// UDP: a socket is named by the id bindUdp returned.
port?: number;
localIp?: string;
remoteIp?: string;
remotePort?: number;
socket?: string;
payload?: string;
timeout?: number;
// TCP: the payload repeated this many times, for messages longer than a segment.
repeatCount?: number;
localPort?: number;
}[];
}
export function snapshotOf(scenario: Scenario): TopologySnapshot {
const firewalls = new Map<string, FirewallConfiguration>();
for (const [node, config] of Object.entries(scenario.firewalls ?? {})) {
firewalls.set(
node,
firewallConfiguration({
isActive: config.active ?? false,
defaultPolicy: config.defaultPolicy ?? 'drop',
dropIcmp: config.dropIcmp ?? false,
filterSynSegmentsOnly: config.synOnly ?? true,
filterUdp: config.filterUdp ?? true,
rules: (config.rules ?? []).map((rule) =>
firewallRule({
sourceIPAddress: rule.source ?? '',
sourceSubnetMask: rule.sourceMask ?? '',
destinationIPAddress: rule.destination ?? '',
destinationSubnetMask: rule.destinationMask ?? '',
...(rule.port === undefined ? {} : { port: rule.port }),
protocol: rule.protocol ?? 'tcp',
action: rule.action ?? 'drop',
}),
),
}),
);
}
return topologySnapshot({
nodes: scenario.nodes.map((node) => ({
id: node.id,
kind: node.kind,
ports: node.ports.map((port) => portSnapshot(port.id, port.label, port.mac)),
})),
links: scenario.links.map((link) => ({
id: link.id,
sourceNodeId: link.sourceNode,
sourcePortId: link.sourcePort,
targetNodeId: link.targetNode,
targetPortId: link.targetPort,
})),
remoteLinkConfigurationsByNodeId: new Map(
Object.entries(scenario.modems ?? {}).map(([node, modem]) => [node, { pairIdentifier: `java-modem-port-${modem.port}`, latencyMilliseconds: 20, isEnabled: true }]),
),
deviceConfigurations: new Map(
Object.entries(scenario.devices ?? {}).map(([node, device]) => [
node,
deviceConfiguration(device.ip, device.mask, device.gateway ?? '', device.dns ?? ''),
]),
),
interfaceConfigurations: new Map(
(scenario.interfaces ?? []).map((item) => [
interfaceKey(item.node, item.port),
{ ipAddress: item.ip, subnetMask: item.mask },
]),
),
manualRoutesByNodeId: new Map(
Object.entries(scenario.routes ?? {}).map(([node, rows]) => [
node,
rows.map(
(row): ManualRoute => ({
destinationNetwork: row.destination,
subnetMask: row.mask,
gateway: row.gateway,
interfaceIPAddress: row.interface,
}),
),
]),
),
dhcpServerConfigurationsByNodeId: new Map(
Object.entries(scenario.dhcpServers ?? {}).map(([node, config]) => [
node,
dhcpServerConfiguration({
isActive: config.active ?? false,
lowerBoundIPAddress: config.lower ?? '0.0.0.0',
upperBoundIPAddress: config.upper ?? '0.0.0.0',
gatewayIPAddress: config.gateway ?? '0.0.0.0',
dnsServerIPAddress: config.dns ?? '0.0.0.0',
useOwnSettings: config.own ?? false,
staticAssignments: (config.staticAssignments ?? []).map(([macAddress, ipAddress], index) => ({
id: `static-${index}`,
macAddress,
ipAddress,
})),
}),
]),
),
dhcpClientConfigurationsByNodeId: new Map((scenario.dhcpClients ?? []).map((node) => [node, { isEnabled: true }])),
ripEnabledByNodeId: new Map((scenario.rip ?? []).map((node) => [node, true])),
portForwardingRowsByNodeId: new Map(
Object.entries(scenario.portForwarding ?? {}).map(([node, rows]) => [
node,
rows.map(([protocolValue, publicPortValue, lanIPAddress, lanPortValue]) => ({ protocolValue, publicPortValue, lanIPAddress, lanPortValue })),
]),
),
switchConfigurationsByNodeId: new Map(
Object.entries(scenario.switches ?? {}).map(([node, config]) => [
node,
config.retention === undefined ? switchConfiguration(config.ssid) : switchConfiguration(config.ssid, config.retention),
]),
),
dnsServerConfigurationsByNodeId: new Map(
Object.entries(scenario.dnsServers ?? {}).map(([node, config]) => [
node,
{
recursiveResolutionEnabled: config.recursive ?? false,
records: config.records.map((line) => {
const [name = '', type = 'A', target = ''] = line.split(' ');
const nameServerOrMail = type === 'NS' || type === 'MX';
return {
name: name.replace(/\.$/, '').toLowerCase() || '.',
type: type as DnsRecordType,
ttlSeconds: 3600,
target: nameServerOrMail && !target.endsWith('.') ? `${target}.` : target,
};
}),
},
]),
),
firewallConfigurationsByNodeId: firewalls,
});
}