// A core scenario (test/scenarios/.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; interfaces?: { node: string; port: string; ip: string; mask: string }[]; routes?: Record; switches?: Record; /** Hosts with a wireless card and the SSID they join (Java only: the editor lays the cable, the simulation does not ask). */ wireless?: Record; 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; /** Routers with RIP switched on. */ rip?: string[]; /** Records as `name TYPE target` (Java's hinzuRecord: TTL 3600). */ dnsServers?: Record; 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(); 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, })), 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, }); }