From cf47cf8f9d4050f8aa00ef922f5803d6bebc8994 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Tue, 29 Sep 2026 21:09:08 +0000 Subject: [PATCH] feat(fls): import DHCP, port forwarding and modem settings importProject carries the DHCP client switch and server settings (hosts and home routers), a home router's port forwarding rows and a modem's pairing into the model; the core gains the types, ported from Swift. The Swift oracle writes them too, and they match on the 13 files Swift reads completely. Configuration text is now trimmed throughout, as Swift trims every value it reads (routes and firewall rules included). Outside those files Swift switches every DHCP server of the Internet examples off: their "aktiv" refers to a shared Boolean it cannot follow. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 4 +- corpus/corpus.swift.json | 142 +++++++++++++++++-- packages/core/src/runtime/config.ts | 59 ++++++++ packages/fls/src/importProject.ts | 121 ++++++++++++++-- packages/fls/test/importProject.test.ts | 87 ++++++++++++ tools/swift-oracle/Sources/Oracle/main.swift | 23 +++ 6 files changed, 406 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c6ce871..e9862cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) | | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | -| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, Firewall) | übertragen; Java-Beschriftungen der Oberfläche weggelassen | +| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Firewall) | übertragen; Java-Beschriftungen der Oberfläche weggelassen | | `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, TCP, DHCP, DNS, RIP, Remote Link, WLAN | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | @@ -33,7 +33,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen | | `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene | | `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen | -| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen; gegen Swifts importiertes Modell auf 13 Dateien geprüft | +| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt; gegen Swifts importiertes Modell auf 13 Dateien geprüft | | `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP, statische NAT, Modem; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen | diff --git a/corpus/corpus.swift.json b/corpus/corpus.swift.json index 7680c8e..97ed3cb 100644 --- a/corpus/corpus.swift.json +++ b/corpus/corpus.swift.json @@ -2046,7 +2046,17 @@ 96 ], "system" : { + "dhcpServer" : { + "dnsServerIPAddress" : "0.0.0.0", + "gatewayIPAddress" : "0.0.0.0", + "isActive" : true, + "lowerBoundIPAddress" : "192.168.0.100", + "staticAssignments" : [ + ], + "upperBoundIPAddress" : "192.168.0.150", + "useOwnSettings" : false + } } }, { @@ -2074,7 +2084,7 @@ 49 ], "system" : { - + "dhcpClient" : true } }, { @@ -2294,7 +2304,7 @@ 320 ], "system" : { - + "dhcpClient" : true } }, { @@ -2322,7 +2332,7 @@ 293 ], "system" : { - + "dhcpClient" : true } } ] @@ -3118,7 +3128,17 @@ 39 ], "system" : { + "dhcpServer" : { + "dnsServerIPAddress" : "0.0.0.0", + "gatewayIPAddress" : "0.0.0.0", + "isActive" : true, + "lowerBoundIPAddress" : "192.168.0.21", + "staticAssignments" : [ + ], + "upperBoundIPAddress" : "192.168.0.29", + "useOwnSettings" : false + } } }, { @@ -3310,7 +3330,7 @@ 512 ], "system" : { - + "dhcpClient" : true } }, { @@ -3338,7 +3358,7 @@ 513 ], "system" : { - + "dhcpClient" : true } }, { @@ -3366,7 +3386,7 @@ 510 ], "system" : { - + "dhcpClient" : true } }, { @@ -3394,7 +3414,7 @@ 509 ], "system" : { - + "dhcpClient" : true } }, { @@ -5206,7 +5226,24 @@ 268 ], "system" : { - + "dhcpServer" : { + "dnsServerIPAddress" : "10.0.0.1", + "gatewayIPAddress" : "10.0.0.1", + "isActive" : false, + "lowerBoundIPAddress" : "10.50.50.11", + "staticAssignments" : [ + [ + "A8:BD:04:A6:63:AD", + "10.10.20.11" + ], + [ + "3D:67:2A:01:2D:91", + "10.10.20.12" + ] + ], + "upperBoundIPAddress" : "10.50.50.254", + "useOwnSettings" : false + } } }, { @@ -5523,7 +5560,17 @@ 791 ], "system" : { + "dhcpServer" : { + "dnsServerIPAddress" : "0.0.0.0", + "gatewayIPAddress" : "0.0.0.0", + "isActive" : false, + "lowerBoundIPAddress" : "43.0.0.11", + "staticAssignments" : [ + ], + "upperBoundIPAddress" : "43.0.0.254", + "useOwnSettings" : false + } } }, { @@ -6641,7 +6688,24 @@ 268 ], "system" : { - + "dhcpServer" : { + "dnsServerIPAddress" : "10.0.0.1", + "gatewayIPAddress" : "10.0.0.1", + "isActive" : false, + "lowerBoundIPAddress" : "10.50.50.11", + "staticAssignments" : [ + [ + "A8:BD:04:A6:63:AD", + "10.10.20.11" + ], + [ + "3D:67:2A:01:2D:91", + "10.10.20.12" + ] + ], + "upperBoundIPAddress" : "10.50.50.254", + "useOwnSettings" : false + } } }, { @@ -6958,7 +7022,17 @@ 782 ], "system" : { + "dhcpServer" : { + "dnsServerIPAddress" : "0.0.0.0", + "gatewayIPAddress" : "0.0.0.0", + "isActive" : false, + "lowerBoundIPAddress" : "43.0.0.11", + "staticAssignments" : [ + ], + "upperBoundIPAddress" : "43.0.0.254", + "useOwnSettings" : false + } } }, { @@ -8757,7 +8831,11 @@ 212 ], "system" : { - + "remoteLink" : { + "isEnabled" : true, + "latencyMilliseconds" : 20, + "pairIdentifier" : "java-modem-port-12345" + } } }, { @@ -8780,7 +8858,11 @@ 79 ], "system" : { - + "remoteLink" : { + "isEnabled" : true, + "latencyMilliseconds" : 20, + "pairIdentifier" : "java-modem-port-12345" + } } } ] @@ -11162,7 +11244,17 @@ 234 ], "system" : { + "dhcpServer" : { + "dnsServerIPAddress" : "0.0.0.0", + "gatewayIPAddress" : "0.0.0.0", + "isActive" : false, + "lowerBoundIPAddress" : "141.99.2.100", + "staticAssignments" : [ + ], + "upperBoundIPAddress" : "141.99.2.250", + "useOwnSettings" : false + } } } ] @@ -11853,7 +11945,17 @@ 178 ], "system" : { + "dhcpServer" : { + "dnsServerIPAddress" : "0.0.0.0", + "gatewayIPAddress" : "0.0.0.0", + "isActive" : false, + "lowerBoundIPAddress" : "42.0.0.11", + "staticAssignments" : [ + ], + "upperBoundIPAddress" : "42.0.0.254", + "useOwnSettings" : false + } } }, { @@ -13619,7 +13721,17 @@ 39 ], "system" : { + "dhcpServer" : { + "dnsServerIPAddress" : "0.0.0.0", + "gatewayIPAddress" : "0.0.0.0", + "isActive" : true, + "lowerBoundIPAddress" : "192.168.0.21", + "staticAssignments" : [ + ], + "upperBoundIPAddress" : "192.168.0.29", + "useOwnSettings" : false + } } }, { @@ -13811,7 +13923,7 @@ 512 ], "system" : { - + "dhcpClient" : true } }, { @@ -13839,7 +13951,7 @@ 513 ], "system" : { - + "dhcpClient" : true } }, { @@ -13867,7 +13979,7 @@ 510 ], "system" : { - + "dhcpClient" : true } }, { @@ -13895,7 +14007,7 @@ 509 ], "system" : { - + "dhcpClient" : true } }, { diff --git a/packages/core/src/runtime/config.ts b/packages/core/src/runtime/config.ts index 8f491af..7546e46 100644 --- a/packages/core/src/runtime/config.ts +++ b/packages/core/src/runtime/config.ts @@ -90,6 +90,65 @@ export interface HostWirelessConfiguration { readonly ssid: string; } +// ── DHCP (Java filius.software.dhcp) ───────────────────────────────────────── + +export interface DhcpClientConfiguration { + readonly isEnabled: boolean; +} + +export interface DhcpStaticAssignment { + readonly id: Id; + readonly macAddress: string; + readonly ipAddress: string; +} + +export interface DhcpServerConfiguration { + readonly isActive: boolean; + readonly lowerBoundIPAddress: string; + readonly upperBoundIPAddress: string; + readonly gatewayIPAddress: string; + readonly dnsServerIPAddress: string; + /** Hand out the gateway and DNS server set here instead of the server's own. */ + readonly useOwnSettings: boolean; + readonly staticAssignments: readonly DhcpStaticAssignment[]; +} + +/** Java's DHCPServer as its constructor leaves it: off, all addresses 0.0.0.0. */ +export function dhcpServerConfiguration(fields: Partial = {}): DhcpServerConfiguration { + return { + isActive: fields.isActive ?? false, + lowerBoundIPAddress: fields.lowerBoundIPAddress ?? '0.0.0.0', + upperBoundIPAddress: fields.upperBoundIPAddress ?? '0.0.0.0', + gatewayIPAddress: fields.gatewayIPAddress ?? '0.0.0.0', + dnsServerIPAddress: fields.dnsServerIPAddress ?? '0.0.0.0', + useOwnSettings: fields.useOwnSettings ?? false, + staticAssignments: fields.staticAssignments ?? [], + }; +} + +// ── Gateway NAT and remote links ──────────────────────────────────────────── + +/** A row of a home router's port forwarding (Java GatewayFirmware.staticNAT), as text. */ +export interface GatewayPortForwardingRow { + readonly protocolValue: string; + readonly publicPortValue: string; + readonly lanIPAddress: string; + readonly lanPortValue: string; +} + +/** + * 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. + */ +export interface RemoteLinkConfiguration { + readonly pairIdentifier: string; + readonly latencyMilliseconds: number; + readonly isEnabled: boolean; +} + +export const DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS = 20; + // ── Firewall (Java filius.software.firewall.Firewall, FirewallRule) ───────── export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const; diff --git a/packages/fls/src/importProject.ts b/packages/fls/src/importProject.ts index f36b3c6..e82fe62 100644 --- a/packages/fls/src/importProject.ts +++ b/packages/fls/src/importProject.ts @@ -20,11 +20,18 @@ // - every router and home router has a firewall configuration, as every one // has a firewall in Java (Swift keeps one only when the file sets a field; // without one the core behaves as with an inactive one). -// Not here yet: DHCP, NAT, remote links, file systems, applications. +// - configuration text is trimmed, as Swift trims every value it reads; +// names stay as Java has them; +// - a Java modem becomes an in-project remote link pair keyed by its port, +// as in Swift — two FILIUS instances on different hosts are not joined. +// Not here yet: file systems and applications. import { canonicalMacAddress, + deepEqual, defaultSwitchConfiguration, + dhcpServerConfiguration, + DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, deterministicDocumentationUuid, documentationColor, documentationItem, @@ -35,6 +42,10 @@ import { DOCUMENTATION_BLACK, DOCUMENTATION_PALE_YELLOW, type DeviceConfiguration, + type DhcpClientConfiguration, + type DhcpServerConfiguration, + type GatewayPortForwardingRow, + type RemoteLinkConfiguration, type FirewallAction, type FirewallConfiguration, type FirewallProtocol, @@ -67,6 +78,11 @@ export interface ImportedProject { /** Only routers with RIP switched on. */ ripEnabledByNodeId: Map; firewallConfigurationsByNodeId: Map; + dhcpClientConfigurations: Map; + /** Only servers whose settings differ from Java's constructor. */ + dhcpServerConfigurations: Map; + portForwardingRowsByNodeId: Map; + remoteLinkConfigurations: Map; /** In render order: rectangles below text, then by order. */ documentationItems: DocumentationItem[]; /** The topology's warnings, then this step's. */ @@ -98,11 +114,16 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP manualRoutesByNodeId: new Map(), ripEnabledByNodeId: new Map(), firewallConfigurationsByNodeId: new Map(), + dhcpClientConfigurations: new Map(), + dhcpServerConfigurations: new Map(), + portForwardingRowsByNodeId: new Map(), + remoteLinkConfigurations: new Map(), }; const nodes = topology.nodes.map((source) => { const node = importNode(source, newId, warnings); registerConfigurations(source, node, deviceConfigurations, interfaceConfigurations); registerSystemConfigurations(source, node, system, warnings); + registerServiceConfigurations(source, node, system, newId, warnings); return node; }); @@ -280,8 +301,87 @@ type SystemConfigurations = Pick< | 'manualRoutesByNodeId' | 'ripEnabledByNodeId' | 'firewallConfigurationsByNodeId' + | 'dhcpClientConfigurations' + | 'dhcpServerConfigurations' + | 'portForwardingRowsByNodeId' + | 'remoteLinkConfigurations' >; +/** A row of text cells, trimmed, or undefined unless it has exactly four. */ +function fourCells(row: (string | null)[]): [string, string, string, string] | undefined { + if (row.length !== 4 || row.some((cell) => cell === null)) return undefined; + return row.map((cell) => cell!.trim()) as [string, string, string, string]; +} + +/** + * The DHCP, NAT and modem parts of Swift's finalizeCurrentNodeCandidate and + * registerRemoteLinkConfiguration. + */ +function registerServiceConfigurations( + source: FlsNode, + node: TopologyNode, + system: SystemConfigurations, + newId: () => Id, + warnings: string[], +): void { + const where = `node '${node.displayName}'`; + const dhcpCapable = node.kind === 'pc' || node.kind === 'notebook' || node.kind === 'gateway'; + if (source.system.dhcpClient === true) { + if (dhcpCapable) system.dhcpClientConfigurations.set(node.id, { isEnabled: true }); + else warnings.push(`${where}: DHCP client on a ${node.kind}, ignored`); + } + const server = source.system.dhcpServer; + if (server !== undefined) { + const configuration = dhcpServerConfiguration({ + isActive: server.isActive, + lowerBoundIPAddress: server.lowerBound?.trim() ?? '', + upperBoundIPAddress: server.upperBound?.trim() ?? '', + gatewayIPAddress: server.gateway?.trim() ?? '', + dnsServerIPAddress: server.dnsServer?.trim() ?? '', + useOwnSettings: server.useOwnSettings, + staticAssignments: server.staticAssignedAddresses.map((entry) => ({ + id: newId(), + macAddress: entry.mac, + ipAddress: entry.ip, + })), + }); + if (!deepEqual(configuration, dhcpServerConfiguration())) { + if (dhcpCapable) system.dhcpServerConfigurations.set(node.id, configuration); + else warnings.push(`${where}: DHCP server settings on a ${node.kind}, ignored`); + } + } + if (node.kind === 'gateway') { + const rows: GatewayPortForwardingRow[] = []; + for (const row of source.system.staticNAT ?? []) { + const cells = fourCells(row); + if (cells === undefined) { + warnings.push(`${where}: a port forwarding row without four entries, left out`); + continue; + } + const [protocolValue, publicPortValue, lanIPAddress, lanPortValue] = cells; + rows.push({ protocolValue, publicPortValue, lanIPAddress, lanPortValue }); + } + if (rows.length > 0) system.portForwardingRowsByNodeId.set(node.id, rows); + } + if (node.kind === 'remoteLink') { + const modem = source.system.modem; + let port = modem?.port ?? 12_345; + if (port < 1 || port > 65_535) { + warnings.push(`${where}: modem port ${port} is invalid, used 12345`); + port = 12_345; + } + if (modem !== undefined && modem.mode !== 1 && modem.mode !== 2) { + warnings.push(`${where}: modem mode ${modem.mode} unknown, treated as client`); + } + // Java pairs a server and a client modem over TCP on this port. + system.remoteLinkConfigurations.set(node.id, { + pairIdentifier: `java-modem-port-${port}`, + latencyMilliseconds: DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, + isEnabled: true, + }); + } +} + /** * Swift's registerSwitchAndWirelessConfigurations, registerManualRoutes and * the RIP and firewall parts of finalizeCurrentNodeCandidate. @@ -306,17 +406,12 @@ function registerSystemConfigurations( } const routes: ManualRoute[] = []; for (const row of source.system.manualRoutes ?? []) { - const [destinationNetwork, subnetMask, gateway, interfaceIPAddress] = row; - if ( - row.length !== 4 || - destinationNetwork == null || - subnetMask == null || - gateway == null || - interfaceIPAddress == null - ) { + const cells = fourCells(row); + if (cells === undefined) { warnings.push(`${where}: a manual route without four entries, left out`); continue; } + const [destinationNetwork, subnetMask, gateway, interfaceIPAddress] = cells; routes.push({ destinationNetwork, subnetMask, gateway, interfaceIPAddress }); } if (routes.length > 0) system.manualRoutesByNodeId.set(node.id, routes); @@ -340,10 +435,10 @@ function firewallConfigurationOf(firewall: FlsFirewall, where: string, warnings: const protocol = PROTOCOLS[rule.protocol]; if (protocol === undefined) warnings.push(`${where}: firewall protocol ${rule.protocol} unknown, used TCP`); return { - sourceIPAddress: rule.srcIP ?? '', - sourceSubnetMask: rule.srcMask ?? '', - destinationIPAddress: rule.destIP ?? '', - destinationSubnetMask: rule.destMask ?? '', + sourceIPAddress: rule.srcIP?.trim() ?? '', + sourceSubnetMask: rule.srcMask?.trim() ?? '', + destinationIPAddress: rule.destIP?.trim() ?? '', + destinationSubnetMask: rule.destMask?.trim() ?? '', port: rule.port, protocol: protocol ?? 'tcp', action: action(rule.action, 'action'), diff --git a/packages/fls/test/importProject.test.ts b/packages/fls/test/importProject.test.ts index c775cc0..7c78ab8 100644 --- a/packages/fls/test/importProject.test.ts +++ b/packages/fls/test/importProject.test.ts @@ -80,6 +80,21 @@ function systemOf(project: ImportedProject, nodeId: Id) { } const rip = project.ripEnabledByNodeId.get(nodeId); if (rip !== undefined) result.ripEnabled = rip; + const dhcpClient = project.dhcpClientConfigurations.get(nodeId); + if (dhcpClient !== undefined) result.dhcpClient = dhcpClient.isEnabled; + const dhcpServer = project.dhcpServerConfigurations.get(nodeId); + if (dhcpServer !== undefined) { + result.dhcpServer = { + ...dhcpServer, + staticAssignments: dhcpServer.staticAssignments.map((entry) => [entry.macAddress, entry.ipAddress]), + }; + } + const forwarding = project.portForwardingRowsByNodeId.get(nodeId); + if (forwarding !== undefined) { + result.portForwarding = forwarding.map((row) => [row.protocolValue, row.publicPortValue, row.lanIPAddress, row.lanPortValue]); + } + const remoteLink = project.remoteLinkConfigurations.get(nodeId); + if (remoteLink !== undefined) result.remoteLink = remoteLink; const firewall = project.firewallConfigurationsByNodeId.get(nodeId); if (firewall !== undefined && !deepEqual(firewall, firewallConfiguration())) { result.firewall = { @@ -462,6 +477,78 @@ describe('importProject rules', () => { expect(project.hostWirelessConfigurations.has(pc!)).toBe(false); }); + it('takes DHCP settings for hosts and home routers, trimmed, and only when they are not the defaults', () => { + const defaults = { isActive: false, lowerBound: '0.0.0.0', upperBound: '0.0.0.0', gateway: '0.0.0.0', dnsServer: '0.0.0.0', useOwnSettings: false, staticAssignedAddresses: [] }; + const project = importProject( + topology({ + nodes: [ + flsNode('Rechner', { + interfaces: [nic('10.0.0.1')], + system: { + dhcpClient: false, + dhcpServer: { + ...defaults, + isActive: true, + lowerBound: ' 10.0.0.100 ', + upperBound: '10.0.0.150', + staticAssignedAddresses: [{ mac: '0A:00:00:00:00:01', ip: '10.0.0.101' }], + }, + }, + }), + flsNode('Notebook', { interfaces: [nic('10.0.0.2')], system: { dhcpClient: true, dhcpServer: defaults } }), + flsNode('Vermittlungsrechner', { + interfaces: [nic('10.0.0.3')], + system: { dhcpClient: true, dhcpServer: { ...defaults, isActive: true } }, + }), + ], + }), + counter(), + ); + const [pc, notebook, router] = project.graph.nodes.map((node) => node.id); + + expect(project.dhcpServerConfigurations.get(pc!)).toMatchObject({ + isActive: true, + lowerBoundIPAddress: '10.0.0.100', + upperBoundIPAddress: '10.0.0.150', + staticAssignments: [{ macAddress: '0A:00:00:00:00:01', ipAddress: '10.0.0.101' }], + }); + expect([...project.dhcpServerConfigurations.keys()]).toEqual([pc]); + expect([...project.dhcpClientConfigurations]).toEqual([[notebook, { isEnabled: true }]]); + expect(project.warnings).toEqual([ + "node 'Vermittlungsrechner': DHCP client on a router, ignored", + "node 'Vermittlungsrechner': DHCP server settings on a router, ignored", + ]); + expect(router).toBeDefined(); + }); + + it('takes port forwarding rows of a home router and pairs modems by port', () => { + const project = importProject( + topology({ + nodes: [ + flsNode('Gateway', { + interfaces: [nic('42.0.0.10'), nic('192.168.0.1')], + system: { staticNAT: [[' TCP', '80', '192.168.0.10', '8080 '], ['UDP', null, '192.168.0.11', '53']] }, + }), + flsNode('Modem', { portCount: 1, system: { modem: { mode: 1, port: 4242, ipAddress: 'localhost' } } }), + flsNode('Modem', { portCount: 1, system: { modem: { mode: 7, port: 70_000, ipAddress: 'localhost' } } }), + ], + }), + counter(), + ); + const [gateway, server, broken] = project.graph.nodes.map((node) => node.id); + + expect(project.portForwardingRowsByNodeId.get(gateway!)).toEqual([ + { protocolValue: 'TCP', publicPortValue: '80', lanIPAddress: '192.168.0.10', lanPortValue: '8080' }, + ]); + expect(project.remoteLinkConfigurations.get(server!)).toEqual({ pairIdentifier: 'java-modem-port-4242', latencyMilliseconds: 20, isEnabled: true }); + expect(project.remoteLinkConfigurations.get(broken!)?.pairIdentifier).toBe('java-modem-port-12345'); + expect(project.warnings).toEqual([ + "node 'Gateway': a port forwarding row without four entries, left out", + "node 'Modem': modem port 70000 is invalid, used 12345", + "node 'Modem': modem mode 7 unknown, treated as client", + ]); + }); + it('leaves out a cable to itself and one to a port that already has a cable', () => { const project = importProject( topology({ diff --git a/tools/swift-oracle/Sources/Oracle/main.swift b/tools/swift-oracle/Sources/Oracle/main.swift index 4160629..46d3d96 100644 --- a/tools/swift-oracle/Sources/Oracle/main.swift +++ b/tools/swift-oracle/Sources/Oracle/main.swift @@ -366,6 +366,29 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON json["manualRoutes"] = routes.map { [$0.destinationNetwork, $0.subnetMask, $0.gateway, $0.interfaceIPAddress] } } if let rip = state.runtimeRIPEnabledByNodeID[node.id] { json["ripEnabled"] = rip } + if let client = state.runtimeDHCPClientConfigurationsByNodeID[node.id] { json["dhcpClient"] = client.isEnabled } + if let server = state.runtimeDHCPServerConfigurationsByNodeID[node.id] { + json["dhcpServer"] = [ + "isActive": server.isActive, + "lowerBoundIPAddress": server.lowerBoundIPAddress, + "upperBoundIPAddress": server.upperBoundIPAddress, + "gatewayIPAddress": server.gatewayIPAddress, + "dnsServerIPAddress": server.dnsServerIPAddress, + "useOwnSettings": server.useOwnSettings, + // The assignments' ids are random. + "staticAssignments": server.staticAssignments.map { [$0.macAddress, $0.ipAddress] }, + ] as JSON + } + if let rows = state.runtimePortForwardingRowsByNodeID[node.id] { + json["portForwarding"] = rows.map { [$0.protocolValue, $0.publicPortValue, $0.lanIPAddress, $0.lanPortValue] } + } + if let link = state.remoteLinkConfigurationsByNodeID[node.id] { + json["remoteLink"] = [ + "pairIdentifier": link.pairIdentifier, + "latencyMilliseconds": Int(link.latencyMilliseconds), + "isEnabled": link.isEnabled, + ] as JSON + } if let firewall = state.runtimeFirewallConfigurationsByNodeID[node.id] { json["firewall"] = [ "isActive": firewall.isActive,