feat(fls): import switch, wireless, routes, RIP and firewall settings
importProject carries the system software settings into the model the core runs: switch SSID and learning time (an SSID Java would roll comes from the node id, as in Swift), a host's wireless network, manual routes, RIP and the firewall of every router and home router. The Swift oracle writes these settings too; they match on the 13 files Swift reads completely. A router's default gateway — its first interface's, which Java routes to (Weiterleitungstabelle.holeTabelle) — is imported into the router's device configuration. Swift's model drops it; in internetwork_komplett the way back from 141.99.5.10 depends on it. A test pings across three routers in the core, with and without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -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/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/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/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, Firewall) | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
|
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, 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/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/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
|
||||||
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
|
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
|
||||||
@@ -33,8 +33,8 @@ 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/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/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/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, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard; 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, 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 |
|
||||||
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Ping über den Router im Kern |
|
| `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` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht); gegen Java geprüft |
|
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht); gegen Java geprüft |
|
||||||
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
||||||
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter |
|
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter |
|
||||||
|
|||||||
+917
-183
File diff suppressed because it is too large
Load Diff
@@ -84,6 +84,12 @@ export function defaultSwitchConfiguration(nodeId: Id): SwitchConfiguration {
|
|||||||
return switchConfiguration(compact.slice(0, 6));
|
return switchConfiguration(compact.slice(0, 6));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A PC or notebook joining a switch's wireless network (Java Betriebssystem.ssid). */
|
||||||
|
export interface HostWirelessConfiguration {
|
||||||
|
readonly isEnabled: boolean;
|
||||||
|
readonly ssid: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Firewall (Java filius.software.firewall.Firewall, FirewallRule) ─────────
|
// ── Firewall (Java filius.software.firewall.Firewall, FirewallRule) ─────────
|
||||||
|
|
||||||
export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const;
|
export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const;
|
||||||
|
|||||||
@@ -15,11 +15,16 @@
|
|||||||
// and put a default in place of an empty one);
|
// and put a default in place of an empty one);
|
||||||
// - an address Java's setter rejected (null) becomes Swift's default, as
|
// - an address Java's setter rejected (null) becomes Swift's default, as
|
||||||
// Swift does for an empty one; Swift would keep invalid text verbatim.
|
// Swift does for an empty one; Swift would keep invalid text verbatim.
|
||||||
// Not here yet: switch and wireless settings, routes, firewall, DHCP, remote
|
// - a router's default gateway (its first interface's, as in Java) — Swift
|
||||||
// links and everything else under a node's system software.
|
// drops it, and routed examples lose their way back;
|
||||||
|
// - 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.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
canonicalMacAddress,
|
canonicalMacAddress,
|
||||||
|
defaultSwitchConfiguration,
|
||||||
deterministicDocumentationUuid,
|
deterministicDocumentationUuid,
|
||||||
documentationColor,
|
documentationColor,
|
||||||
documentationItem,
|
documentationItem,
|
||||||
@@ -30,6 +35,13 @@ import {
|
|||||||
DOCUMENTATION_BLACK,
|
DOCUMENTATION_BLACK,
|
||||||
DOCUMENTATION_PALE_YELLOW,
|
DOCUMENTATION_PALE_YELLOW,
|
||||||
type DeviceConfiguration,
|
type DeviceConfiguration,
|
||||||
|
type FirewallAction,
|
||||||
|
type FirewallConfiguration,
|
||||||
|
type FirewallProtocol,
|
||||||
|
type FirewallRule,
|
||||||
|
type HostWirelessConfiguration,
|
||||||
|
type ManualRoute,
|
||||||
|
type SwitchConfiguration,
|
||||||
type DocumentationItem,
|
type DocumentationItem,
|
||||||
type HostLabelPolicy,
|
type HostLabelPolicy,
|
||||||
type Id,
|
type Id,
|
||||||
@@ -41,6 +53,7 @@ import {
|
|||||||
type TopologyLink,
|
type TopologyLink,
|
||||||
type TopologyNode,
|
type TopologyNode,
|
||||||
} from '@mdg-filius/core';
|
} from '@mdg-filius/core';
|
||||||
|
import type { FlsFirewall } from './systemSoftware.js';
|
||||||
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
|
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
|
||||||
|
|
||||||
export interface ImportedProject {
|
export interface ImportedProject {
|
||||||
@@ -48,6 +61,12 @@ export interface ImportedProject {
|
|||||||
graph: TopologyGraph;
|
graph: TopologyGraph;
|
||||||
deviceConfigurations: Map<Id, DeviceConfiguration>;
|
deviceConfigurations: Map<Id, DeviceConfiguration>;
|
||||||
interfaceConfigurations: Map<InterfaceKey, InterfaceConfiguration>;
|
interfaceConfigurations: Map<InterfaceKey, InterfaceConfiguration>;
|
||||||
|
switchConfigurations: Map<Id, SwitchConfiguration>;
|
||||||
|
hostWirelessConfigurations: Map<Id, HostWirelessConfiguration>;
|
||||||
|
manualRoutesByNodeId: Map<Id, ManualRoute[]>;
|
||||||
|
/** Only routers with RIP switched on. */
|
||||||
|
ripEnabledByNodeId: Map<Id, boolean>;
|
||||||
|
firewallConfigurationsByNodeId: Map<Id, FirewallConfiguration>;
|
||||||
/** In render order: rectangles below text, then by order. */
|
/** In render order: rectangles below text, then by order. */
|
||||||
documentationItems: DocumentationItem[];
|
documentationItems: DocumentationItem[];
|
||||||
/** The topology's warnings, then this step's. */
|
/** The topology's warnings, then this step's. */
|
||||||
@@ -73,9 +92,17 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
|
|||||||
const deviceConfigurations = new Map<Id, DeviceConfiguration>();
|
const deviceConfigurations = new Map<Id, DeviceConfiguration>();
|
||||||
const interfaceConfigurations = new Map<InterfaceKey, InterfaceConfiguration>();
|
const interfaceConfigurations = new Map<InterfaceKey, InterfaceConfiguration>();
|
||||||
|
|
||||||
|
const system: SystemConfigurations = {
|
||||||
|
switchConfigurations: new Map(),
|
||||||
|
hostWirelessConfigurations: new Map(),
|
||||||
|
manualRoutesByNodeId: new Map(),
|
||||||
|
ripEnabledByNodeId: new Map(),
|
||||||
|
firewallConfigurationsByNodeId: new Map(),
|
||||||
|
};
|
||||||
const nodes = topology.nodes.map((source) => {
|
const nodes = topology.nodes.map((source) => {
|
||||||
const node = importNode(source, newId, warnings);
|
const node = importNode(source, newId, warnings);
|
||||||
registerConfigurations(source, node, deviceConfigurations, interfaceConfigurations);
|
registerConfigurations(source, node, deviceConfigurations, interfaceConfigurations);
|
||||||
|
registerSystemConfigurations(source, node, system, warnings);
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,6 +149,7 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
|
|||||||
graph: { nodes, links },
|
graph: { nodes, links },
|
||||||
deviceConfigurations,
|
deviceConfigurations,
|
||||||
interfaceConfigurations,
|
interfaceConfigurations,
|
||||||
|
...system,
|
||||||
documentationItems: inDeterministicRenderOrder(documentationItems),
|
documentationItems: inDeterministicRenderOrder(documentationItems),
|
||||||
warnings,
|
warnings,
|
||||||
};
|
};
|
||||||
@@ -218,6 +246,21 @@ function registerConfigurations(
|
|||||||
subnetMask: orDefault(nic?.mask ?? null, wan ? '255.0.0.0' : '255.255.255.0'),
|
subnetMask: orDefault(nic?.mask ?? null, wan ? '255.0.0.0' : '255.255.255.0'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Java routes a router's traffic without a better route to the gateway of
|
||||||
|
// its first interface (Weiterleitungstabelle.holeTabelle, getStandardGateway).
|
||||||
|
// Swift's model has no router gateway at all; the core reads it from the
|
||||||
|
// router's device configuration, which is set only when there is one.
|
||||||
|
const first = source.interfaces[0];
|
||||||
|
const routerGateway = first?.gateway?.trim() ?? '';
|
||||||
|
if (node.kind === 'router' && first !== undefined && routerGateway !== '') {
|
||||||
|
const firstConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id));
|
||||||
|
deviceConfigurations.set(node.id, {
|
||||||
|
ipAddress: firstConfiguration?.ipAddress ?? '',
|
||||||
|
subnetMask: firstConfiguration?.subnetMask ?? '',
|
||||||
|
defaultGateway: routerGateway,
|
||||||
|
dnsServer: first.dns?.trim() ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
const wan = source.interfaces[0];
|
const wan = source.interfaces[0];
|
||||||
if (node.kind === 'gateway' && wan !== undefined) {
|
if (node.kind === 'gateway' && wan !== undefined) {
|
||||||
const wanConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id));
|
const wanConfiguration = interfaceConfigurations.get(interfaceKey(node.id, node.ports[0]!.id));
|
||||||
@@ -230,6 +273,92 @@ function registerConfigurations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SystemConfigurations = Pick<
|
||||||
|
ImportedProject,
|
||||||
|
| 'switchConfigurations'
|
||||||
|
| 'hostWirelessConfigurations'
|
||||||
|
| 'manualRoutesByNodeId'
|
||||||
|
| 'ripEnabledByNodeId'
|
||||||
|
| 'firewallConfigurationsByNodeId'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swift's registerSwitchAndWirelessConfigurations, registerManualRoutes and
|
||||||
|
* the RIP and firewall parts of finalizeCurrentNodeCandidate.
|
||||||
|
*/
|
||||||
|
function registerSystemConfigurations(
|
||||||
|
source: FlsNode,
|
||||||
|
node: TopologyNode,
|
||||||
|
system: SystemConfigurations,
|
||||||
|
warnings: string[],
|
||||||
|
): void {
|
||||||
|
const where = `node '${node.displayName}'`;
|
||||||
|
const ssid = source.system.ssid?.trim() ?? '';
|
||||||
|
if (node.kind === 'networkSwitch') {
|
||||||
|
// Without an SSID in the file, Java rolls one; Swift derives it from the node id.
|
||||||
|
system.switchConfigurations.set(node.id, {
|
||||||
|
ssid: ssid === '' ? defaultSwitchConfiguration(node.id).ssid : ssid,
|
||||||
|
retentionTimeMilliseconds: source.system.retentionTime ?? 300_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ((node.kind === 'pc' || node.kind === 'notebook') && ssid !== '') {
|
||||||
|
system.hostWirelessConfigurations.set(node.id, { isEnabled: true, ssid });
|
||||||
|
}
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
warnings.push(`${where}: a manual route without four entries, left out`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
routes.push({ destinationNetwork, subnetMask, gateway, interfaceIPAddress });
|
||||||
|
}
|
||||||
|
if (routes.length > 0) system.manualRoutesByNodeId.set(node.id, routes);
|
||||||
|
if (node.kind === 'router' && source.system.ripEnabled === true) system.ripEnabledByNodeId.set(node.id, true);
|
||||||
|
if ((node.kind === 'router' || node.kind === 'gateway') && source.system.firewall !== undefined) {
|
||||||
|
system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(source.system.firewall, where, warnings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROTOCOLS: Readonly<Record<number, FirewallProtocol>> = { [-1]: 'all', 1: 'icmp', 6: 'tcp', 17: 'udp' };
|
||||||
|
const ACTIONS: Readonly<Record<number, FirewallAction>> = { 0: 'drop', 1: 'accept' };
|
||||||
|
|
||||||
|
/** Java's numbers as the core's names; an unknown number keeps Swift's default (TCP, drop). */
|
||||||
|
function firewallConfigurationOf(firewall: FlsFirewall, where: string, warnings: string[]): FirewallConfiguration {
|
||||||
|
const action = (value: number, what: string): FirewallAction => {
|
||||||
|
const known = ACTIONS[value];
|
||||||
|
if (known === undefined) warnings.push(`${where}: firewall ${what} ${value} unknown, used drop`);
|
||||||
|
return known ?? 'drop';
|
||||||
|
};
|
||||||
|
const rules: FirewallRule[] = firewall.rules.map((rule) => {
|
||||||
|
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 ?? '',
|
||||||
|
port: rule.port,
|
||||||
|
protocol: protocol ?? 'tcp',
|
||||||
|
action: action(rule.action, 'action'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
isActive: firewall.activated,
|
||||||
|
defaultPolicy: action(firewall.defaultPolicy, 'default policy'),
|
||||||
|
dropIcmp: firewall.dropICMP,
|
||||||
|
filterSynSegmentsOnly: firewall.filterSYNSegmentsOnly,
|
||||||
|
filterUdp: firewall.filterUdp,
|
||||||
|
rules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swift's finalizeCurrentDocumentationCandidate: RECT = 1, TEXT = 2; a frame
|
* Swift's finalizeCurrentDocumentationCandidate: RECT = 1, TEXT = 2; a frame
|
||||||
* inside the editor's world; the id from the item's values and its order.
|
* inside the editor's world; the id from the item's values and its order.
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
defaultDisplayName,
|
defaultDisplayName,
|
||||||
|
defaultSwitchConfiguration,
|
||||||
|
firewallConfiguration,
|
||||||
|
FIREWALL_PROTOCOL,
|
||||||
interfaceKey,
|
interfaceKey,
|
||||||
|
deepEqual,
|
||||||
NetworkRuntimeEngine,
|
NetworkRuntimeEngine,
|
||||||
portSnapshot,
|
portSnapshot,
|
||||||
topologySnapshot,
|
topologySnapshot,
|
||||||
@@ -54,6 +58,50 @@ function importFile(file: string): ImportedProject {
|
|||||||
return importProject(readTopology(values), counter());
|
return importProject(readTopology(values), counter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* describeSystem's shape. A firewall as the core has it without one
|
||||||
|
* (inactive, drop, no rules) is left out: Swift keeps a configuration only
|
||||||
|
* when the file sets a field, and the core treats both alike.
|
||||||
|
*/
|
||||||
|
function systemOf(project: ImportedProject, nodeId: Id) {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
const switchConfiguration = project.switchConfigurations.get(nodeId);
|
||||||
|
if (switchConfiguration !== undefined) {
|
||||||
|
result.switch = {
|
||||||
|
ssid: switchConfiguration.ssid === defaultSwitchConfiguration(nodeId).ssid ? '<from node id>' : switchConfiguration.ssid,
|
||||||
|
retentionTimeMilliseconds: switchConfiguration.retentionTimeMilliseconds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const wireless = project.hostWirelessConfigurations.get(nodeId);
|
||||||
|
if (wireless !== undefined) result.hostWireless = wireless;
|
||||||
|
const routes = project.manualRoutesByNodeId.get(nodeId);
|
||||||
|
if (routes !== undefined) {
|
||||||
|
result.manualRoutes = routes.map((route) => [route.destinationNetwork, route.subnetMask, route.gateway, route.interfaceIPAddress]);
|
||||||
|
}
|
||||||
|
const rip = project.ripEnabledByNodeId.get(nodeId);
|
||||||
|
if (rip !== undefined) result.ripEnabled = rip;
|
||||||
|
const firewall = project.firewallConfigurationsByNodeId.get(nodeId);
|
||||||
|
if (firewall !== undefined && !deepEqual(firewall, firewallConfiguration())) {
|
||||||
|
result.firewall = {
|
||||||
|
isActive: firewall.isActive,
|
||||||
|
defaultPolicy: firewall.defaultPolicy === 'accept' ? 1 : 0,
|
||||||
|
dropICMP: firewall.dropIcmp,
|
||||||
|
filterSYNSegmentsOnly: firewall.filterSynSegmentsOnly,
|
||||||
|
filterUDP: firewall.filterUdp,
|
||||||
|
rules: firewall.rules.map((rule) => [
|
||||||
|
rule.sourceIPAddress,
|
||||||
|
rule.sourceSubnetMask,
|
||||||
|
rule.destinationIPAddress,
|
||||||
|
rule.destinationSubnetMask,
|
||||||
|
rule.port,
|
||||||
|
FIREWALL_PROTOCOL[rule.protocol],
|
||||||
|
rule.action === 'accept' ? 1 : 0,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/** The shape describeImportedState (tools/swift-oracle) writes: indices instead of random ids. */
|
/** The shape describeImportedState (tools/swift-oracle) writes: indices instead of random ids. */
|
||||||
function asSwiftState(project: ImportedProject) {
|
function asSwiftState(project: ImportedProject) {
|
||||||
const { nodes, links } = project.graph;
|
const { nodes, links } = project.graph;
|
||||||
@@ -68,6 +116,7 @@ function asSwiftState(project: ImportedProject) {
|
|||||||
ports: node.ports.map((port) => ({ label: port.label, mac: port.importedMacAddress ?? null, occupied: port.isOccupied })),
|
ports: node.ports.map((port) => ({ label: port.label, mac: port.importedMacAddress ?? null, occupied: port.isOccupied })),
|
||||||
device: project.deviceConfigurations.get(node.id) ?? null,
|
device: project.deviceConfigurations.get(node.id) ?? null,
|
||||||
interfaces: node.ports.map((port) => project.interfaceConfigurations.get(interfaceKey(node.id, port.id)) ?? null),
|
interfaces: node.ports.map((port) => project.interfaceConfigurations.get(interfaceKey(node.id, port.id)) ?? null),
|
||||||
|
system: systemOf(project, node.id),
|
||||||
})),
|
})),
|
||||||
links: links.map((link) => [
|
links: links.map((link) => [
|
||||||
nodeIndex.get(link.sourceNodeId),
|
nodeIndex.get(link.sourceNodeId),
|
||||||
@@ -175,10 +224,8 @@ describe('corpus: every file imports completely', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('an imported file runs in the simulation core', () => {
|
/** The core started on an imported project (the editor will derive this snapshot from its state). */
|
||||||
it('pings across the router from the notebook with a gateway, not from the one without', () => {
|
function startedEngine(project: ImportedProject): NetworkRuntimeEngine {
|
||||||
const project = importFile(join(corpusRoot, 'java-examples/zwei_rechnernetze_komplett.fls'));
|
|
||||||
const byName = (displayName: string) => project.graph.nodes.find((node) => node.displayName === displayName)!.id;
|
|
||||||
const engine = new NetworkRuntimeEngine(14);
|
const engine = new NetworkRuntimeEngine(14);
|
||||||
engine.handle({
|
engine.handle({
|
||||||
type: 'start',
|
type: 'start',
|
||||||
@@ -191,15 +238,47 @@ describe('an imported file runs in the simulation core', () => {
|
|||||||
links: project.graph.links,
|
links: project.graph.links,
|
||||||
deviceConfigurations: project.deviceConfigurations,
|
deviceConfigurations: project.deviceConfigurations,
|
||||||
interfaceConfigurations: project.interfaceConfigurations,
|
interfaceConfigurations: project.interfaceConfigurations,
|
||||||
|
manualRoutesByNodeId: project.manualRoutesByNodeId,
|
||||||
|
ripEnabledByNodeId: project.ripEnabledByNodeId,
|
||||||
|
firewallConfigurationsByNodeId: project.firewallConfigurationsByNodeId,
|
||||||
|
switchConfigurationsByNodeId: project.switchConfigurations,
|
||||||
}),
|
}),
|
||||||
seed: 14,
|
seed: 14,
|
||||||
initialTimeMilliseconds: 0,
|
initialTimeMilliseconds: 0,
|
||||||
});
|
});
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('an imported file runs in the simulation core', () => {
|
||||||
|
it('pings across the router from the notebook with a gateway, not from the one without', () => {
|
||||||
|
const project = importFile(join(corpusRoot, 'java-examples/zwei_rechnernetze_komplett.fls'));
|
||||||
|
const byName = (displayName: string) => project.graph.nodes.find((node) => node.displayName === displayName)!.id;
|
||||||
|
const engine = startedEngine(project);
|
||||||
|
|
||||||
expect(engine.sendIcmpEcho(byName('Notebook 1.10'), '141.99.2.10').type).toBe('delivered');
|
expect(engine.sendIcmpEcho(byName('Notebook 1.10'), '141.99.2.10').type).toBe('delivered');
|
||||||
expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.2.10')).toMatchObject({ type: 'icmpError', kind: 'destinationNetworkUnreachable' });
|
expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.2.10')).toMatchObject({ type: 'icmpError', kind: 'destinationNetworkUnreachable' });
|
||||||
expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.1.10').type).toBe('delivered');
|
expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.1.10').type).toBe('delivered');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('pings across three routers on manual routes and the routers\' default gateways', () => {
|
||||||
|
// Router 1 (141.99.1/2) sends everything else to 2.2; router 2 has manual
|
||||||
|
// routes to 1.0 and 5.0; router 3 (141.99.4/5) sends everything else to
|
||||||
|
// 4.1. The way back needs router 3's default gateway — which Swift drops.
|
||||||
|
const project = importFile(join(corpusRoot, 'java-examples/internetwork_komplett.fls'));
|
||||||
|
const notebook = project.graph.nodes.find((node) => node.displayName === 'Notebook 1.10')!.id;
|
||||||
|
const routers = project.graph.nodes.filter((node) => node.kind === 'router').map((node) => node.id);
|
||||||
|
|
||||||
|
expect(routers.map((router) => project.deviceConfigurations.get(router)?.defaultGateway)).toEqual([
|
||||||
|
'141.99.2.2',
|
||||||
|
'141.99.4.2',
|
||||||
|
'141.99.4.1',
|
||||||
|
]);
|
||||||
|
expect(startedEngine(project).sendIcmpEcho(notebook, '141.99.5.10').type).toBe('delivered');
|
||||||
|
|
||||||
|
const withoutRouterGateways = { ...project, deviceConfigurations: new Map(project.deviceConfigurations) };
|
||||||
|
for (const router of routers) withoutRouterGateways.deviceConfigurations.delete(router);
|
||||||
|
expect(startedEngine(withoutRouterGateways).sendIcmpEcho(notebook, '141.99.5.10').type).not.toBe('delivered');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Rules the corpus does not exercise ─────────────────────────────────────
|
// ── Rules the corpus does not exercise ─────────────────────────────────────
|
||||||
@@ -295,6 +374,94 @@ describe('importProject rules', () => {
|
|||||||
expect(project.documentationItems[0]).toMatchObject({ isBold: true, fontSize: 18, color: { alpha: 0.2 } });
|
expect(project.documentationItems[0]).toMatchObject({ isBold: true, fontSize: 18, color: { alpha: 0.2 } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps a router's firewall, RIP and routes, and says what it cannot map", () => {
|
||||||
|
const project = importProject(
|
||||||
|
topology({
|
||||||
|
nodes: [
|
||||||
|
flsNode('Vermittlungsrechner', {
|
||||||
|
interfaces: [nic('10.0.0.1')],
|
||||||
|
system: {
|
||||||
|
ripEnabled: true,
|
||||||
|
manualRoutes: [
|
||||||
|
['10.1.0.0', '255.255.0.0', '10.0.0.2', '10.0.0.1'],
|
||||||
|
['10.2.0.0', '255.255.0.0', null, '10.0.0.1'],
|
||||||
|
],
|
||||||
|
firewall: {
|
||||||
|
activated: true,
|
||||||
|
defaultPolicy: 1,
|
||||||
|
dropICMP: true,
|
||||||
|
filterSYNSegmentsOnly: false,
|
||||||
|
filterUdp: false,
|
||||||
|
rules: [
|
||||||
|
{ srcIP: '10.0.0.0', srcMask: '255.0.0.0', destIP: null, destMask: '', port: 80, protocol: -1, action: 1 },
|
||||||
|
{ srcIP: '', srcMask: '', destIP: '', destMask: '', port: -1, protocol: 99, action: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
counter(),
|
||||||
|
);
|
||||||
|
const router = project.graph.nodes[0]!.id;
|
||||||
|
|
||||||
|
expect(project.ripEnabledByNodeId.get(router)).toBe(true);
|
||||||
|
expect(project.manualRoutesByNodeId.get(router)).toEqual([
|
||||||
|
{ destinationNetwork: '10.1.0.0', subnetMask: '255.255.0.0', gateway: '10.0.0.2', interfaceIPAddress: '10.0.0.1' },
|
||||||
|
]);
|
||||||
|
expect(project.firewallConfigurationsByNodeId.get(router)).toEqual({
|
||||||
|
isActive: true,
|
||||||
|
defaultPolicy: 'accept',
|
||||||
|
dropIcmp: true,
|
||||||
|
filterSynSegmentsOnly: false,
|
||||||
|
filterUdp: false,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
sourceIPAddress: '10.0.0.0',
|
||||||
|
sourceSubnetMask: '255.0.0.0',
|
||||||
|
destinationIPAddress: '',
|
||||||
|
destinationSubnetMask: '',
|
||||||
|
port: 80,
|
||||||
|
protocol: 'all',
|
||||||
|
action: 'accept',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourceIPAddress: '',
|
||||||
|
sourceSubnetMask: '',
|
||||||
|
destinationIPAddress: '',
|
||||||
|
destinationSubnetMask: '',
|
||||||
|
port: -1,
|
||||||
|
protocol: 'tcp',
|
||||||
|
action: 'drop',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(project.warnings).toEqual([
|
||||||
|
"node 'Vermittlungsrechner': a manual route without four entries, left out",
|
||||||
|
"node 'Vermittlungsrechner': firewall protocol 99 unknown, used TCP",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives a switch without SSID one from its id, and joins a host to a wireless network only with an SSID", () => {
|
||||||
|
const project = importProject(
|
||||||
|
topology({
|
||||||
|
nodes: [
|
||||||
|
flsNode('Switch', { portCount: 24, system: { ssid: undefined, retentionTime: 2000 } }),
|
||||||
|
flsNode('Switch', { portCount: 24, system: { ssid: ' MyWiFi ', retentionTime: 300_000 } }),
|
||||||
|
flsNode('Notebook', { interfaces: [nic('10.0.0.1')], system: { ssid: 'MyWiFi', manualRoutes: [] } }),
|
||||||
|
flsNode('Rechner', { interfaces: [nic('10.0.0.2')], system: { ssid: ' ', manualRoutes: [] } }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
counter(),
|
||||||
|
);
|
||||||
|
const [first, second, notebook, pc] = project.graph.nodes.map((node) => node.id);
|
||||||
|
|
||||||
|
expect(project.switchConfigurations.get(first!)).toEqual({ ssid: defaultSwitchConfiguration(first!).ssid, retentionTimeMilliseconds: 2000 });
|
||||||
|
expect(project.switchConfigurations.get(second!)).toEqual({ ssid: 'MyWiFi', retentionTimeMilliseconds: 300_000 });
|
||||||
|
expect(project.hostWirelessConfigurations.get(notebook!)).toEqual({ isEnabled: true, ssid: 'MyWiFi' });
|
||||||
|
expect(project.hostWirelessConfigurations.has(pc!)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('leaves out a cable to itself and one to a port that already has a cable', () => {
|
it('leaves out a cable to itself and one to a port that already has a cable', () => {
|
||||||
const project = importProject(
|
const project = importProject(
|
||||||
topology({
|
topology({
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ func describeImportedState(_ state: TopologyEditorState) -> JSON {
|
|||||||
},
|
},
|
||||||
"device": device,
|
"device": device,
|
||||||
"interfaces": interfaces,
|
"interfaces": interfaces,
|
||||||
|
"system": describeSystem(state, node),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
let links: [[Int]] = state.graph.links.map { link in
|
let links: [[Int]] = state.graph.links.map { link in
|
||||||
@@ -347,6 +348,40 @@ func describeImportedState(_ state: TopologyEditorState) -> JSON {
|
|||||||
return ["nodes": nodes, "links": links, "documentation": documentation]
|
return ["nodes": nodes, "links": links, "documentation": documentation]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The system software settings of a node in the imported state. A switch
|
||||||
|
/// SSID Swift derives from the (random) node id is written as a marker.
|
||||||
|
func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON {
|
||||||
|
var json: JSON = [:]
|
||||||
|
if let configuration = state.switchConfigurationsByNodeID[node.id] {
|
||||||
|
let derived = TopologySwitchConfiguration.defaultConfiguration(nodeID: node.id).ssid
|
||||||
|
json["switch"] = [
|
||||||
|
"ssid": configuration.ssid == derived ? "<from node id>" : configuration.ssid,
|
||||||
|
"retentionTimeMilliseconds": Int(configuration.retentionTimeMilliseconds),
|
||||||
|
] as JSON
|
||||||
|
}
|
||||||
|
if let wireless = state.hostWirelessConfigurationsByNodeID[node.id] {
|
||||||
|
json["hostWireless"] = ["isEnabled": wireless.isEnabled, "ssid": wireless.ssid] as JSON
|
||||||
|
}
|
||||||
|
if let routes = state.runtimeManualRoutesByNodeID[node.id] {
|
||||||
|
json["manualRoutes"] = routes.map { [$0.destinationNetwork, $0.subnetMask, $0.gateway, $0.interfaceIPAddress] }
|
||||||
|
}
|
||||||
|
if let rip = state.runtimeRIPEnabledByNodeID[node.id] { json["ripEnabled"] = rip }
|
||||||
|
if let firewall = state.runtimeFirewallConfigurationsByNodeID[node.id] {
|
||||||
|
json["firewall"] = [
|
||||||
|
"isActive": firewall.isActive,
|
||||||
|
"defaultPolicy": firewall.defaultPolicy.rawValue,
|
||||||
|
"dropICMP": firewall.dropICMP,
|
||||||
|
"filterSYNSegmentsOnly": firewall.filterSYNSegmentsOnly,
|
||||||
|
"filterUDP": firewall.filterUDP,
|
||||||
|
"rules": firewall.rules.map { rule -> [Any] in
|
||||||
|
[rule.sourceIPAddress, rule.sourceSubnetMask, rule.destinationIPAddress, rule.destinationSubnetMask,
|
||||||
|
rule.port, rule.protocolType.rawValue, rule.action.rawValue]
|
||||||
|
},
|
||||||
|
] as JSON
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Archive path folding
|
// MARK: - Archive path folding
|
||||||
|
|
||||||
struct PathCase: Decodable { let name: String; let first: String; let second: String }
|
struct PathCase: Decodable { let name: String; let first: String; let second: String }
|
||||||
|
|||||||
Reference in New Issue
Block a user