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/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, 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/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
|
||||
| `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/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, 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 |
|
||||
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Ping über den Router im Kern |
|
||||
| `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, 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/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 |
|
||||
|
||||
+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));
|
||||
}
|
||||
|
||||
/** 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) ─────────
|
||||
|
||||
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);
|
||||
// - an address Java's setter rejected (null) becomes Swift's default, as
|
||||
// Swift does for an empty one; Swift would keep invalid text verbatim.
|
||||
// Not here yet: switch and wireless settings, routes, firewall, DHCP, remote
|
||||
// links and everything else under a node's system software.
|
||||
// - a router's default gateway (its first interface's, as in Java) — Swift
|
||||
// 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 {
|
||||
canonicalMacAddress,
|
||||
defaultSwitchConfiguration,
|
||||
deterministicDocumentationUuid,
|
||||
documentationColor,
|
||||
documentationItem,
|
||||
@@ -30,6 +35,13 @@ import {
|
||||
DOCUMENTATION_BLACK,
|
||||
DOCUMENTATION_PALE_YELLOW,
|
||||
type DeviceConfiguration,
|
||||
type FirewallAction,
|
||||
type FirewallConfiguration,
|
||||
type FirewallProtocol,
|
||||
type FirewallRule,
|
||||
type HostWirelessConfiguration,
|
||||
type ManualRoute,
|
||||
type SwitchConfiguration,
|
||||
type DocumentationItem,
|
||||
type HostLabelPolicy,
|
||||
type Id,
|
||||
@@ -41,6 +53,7 @@ import {
|
||||
type TopologyLink,
|
||||
type TopologyNode,
|
||||
} from '@mdg-filius/core';
|
||||
import type { FlsFirewall } from './systemSoftware.js';
|
||||
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
|
||||
|
||||
export interface ImportedProject {
|
||||
@@ -48,6 +61,12 @@ export interface ImportedProject {
|
||||
graph: TopologyGraph;
|
||||
deviceConfigurations: Map<Id, DeviceConfiguration>;
|
||||
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. */
|
||||
documentationItems: DocumentationItem[];
|
||||
/** 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 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 node = importNode(source, newId, warnings);
|
||||
registerConfigurations(source, node, deviceConfigurations, interfaceConfigurations);
|
||||
registerSystemConfigurations(source, node, system, warnings);
|
||||
return node;
|
||||
});
|
||||
|
||||
@@ -122,6 +149,7 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
|
||||
graph: { nodes, links },
|
||||
deviceConfigurations,
|
||||
interfaceConfigurations,
|
||||
...system,
|
||||
documentationItems: inDeterministicRenderOrder(documentationItems),
|
||||
warnings,
|
||||
};
|
||||
@@ -218,6 +246,21 @@ function registerConfigurations(
|
||||
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];
|
||||
if (node.kind === 'gateway' && wan !== undefined) {
|
||||
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
|
||||
* 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 {
|
||||
defaultDisplayName,
|
||||
defaultSwitchConfiguration,
|
||||
firewallConfiguration,
|
||||
FIREWALL_PROTOCOL,
|
||||
interfaceKey,
|
||||
deepEqual,
|
||||
NetworkRuntimeEngine,
|
||||
portSnapshot,
|
||||
topologySnapshot,
|
||||
@@ -54,6 +58,50 @@ function importFile(file: string): ImportedProject {
|
||||
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. */
|
||||
function asSwiftState(project: ImportedProject) {
|
||||
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 })),
|
||||
device: project.deviceConfigurations.get(node.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) => [
|
||||
nodeIndex.get(link.sourceNodeId),
|
||||
@@ -175,10 +224,8 @@ describe('corpus: every file imports completely', () => {
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
/** The core started on an imported project (the editor will derive this snapshot from its state). */
|
||||
function startedEngine(project: ImportedProject): NetworkRuntimeEngine {
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({
|
||||
type: 'start',
|
||||
@@ -191,15 +238,47 @@ describe('an imported file runs in the simulation core', () => {
|
||||
links: project.graph.links,
|
||||
deviceConfigurations: project.deviceConfigurations,
|
||||
interfaceConfigurations: project.interfaceConfigurations,
|
||||
manualRoutesByNodeId: project.manualRoutesByNodeId,
|
||||
ripEnabledByNodeId: project.ripEnabledByNodeId,
|
||||
firewallConfigurationsByNodeId: project.firewallConfigurationsByNodeId,
|
||||
switchConfigurationsByNodeId: project.switchConfigurations,
|
||||
}),
|
||||
seed: 14,
|
||||
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.11'), '141.99.2.10')).toMatchObject({ type: 'icmpError', kind: 'destinationNetworkUnreachable' });
|
||||
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 ─────────────────────────────────────
|
||||
@@ -295,6 +374,94 @@ describe('importProject rules', () => {
|
||||
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', () => {
|
||||
const project = importProject(
|
||||
topology({
|
||||
|
||||
@@ -323,6 +323,7 @@ func describeImportedState(_ state: TopologyEditorState) -> JSON {
|
||||
},
|
||||
"device": device,
|
||||
"interfaces": interfaces,
|
||||
"system": describeSystem(state, node),
|
||||
]
|
||||
}
|
||||
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]
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
struct PathCase: Decodable { let name: String; let first: String; let second: String }
|
||||
|
||||
Reference in New Issue
Block a user