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,