Files
mdg-filius/packages/core/src/runtime/config.ts
T
julian.kiedaischandClaude Opus 5.5 95daa64276 feat(fls): export the editor model's programs
exportProject.ts, step X4a: each installed program of a PC or notebook as
the FILIUS application of its class, keyed by that class name as Java keys
them, with its settings: servers switched on as the model has them, a web
server's port and virtual hosts, the DNS server's recursion on port 53, the
echo server's port, the generic client's target (none as null, as its
constructor leaves it), the peer count, the personal firewall. E-mail
contents are the next step.

The model lost a web server's virtual hosts switch (useVHost, on for 3 web
servers in the corpus); Swift sets it off on import. The core now has
WebServerConfiguration.virtualHosts (Swift's name), the import carries
Java's value, and the Swift comparison names Swift's error.

Checked by Java: every corpus file, imported, exported and written, loads
with the programs of the original, with no new departure. In the synthetic
model the PC has every program with settings away from the defaults, the
notebook programs without settings (Java's defaults, 3 peers), where Java
makes /peer2peer itself. 12 mutation probes, each loaded by Java, were all
caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:17:30 +00:00

363 lines
12 KiB
TypeScript

// Ported from filius-ipad (GPLv2/GPLv3):
// State/TopologyEditorState.swift (runtime configuration value types)
import type { Id } from '../ids.js';
import { maskAddress, parseIPv4 } from './ipv4.js';
/** The IP settings of a PC or notebook (its single interface) — and a router's gateway. */
export interface DeviceConfiguration {
readonly ipAddress: string;
readonly subnetMask: string;
readonly defaultGateway: string;
readonly dnsServer: string;
}
export function deviceConfiguration(
ipAddress: string,
subnetMask: string,
defaultGateway = '',
dnsServer = '',
): DeviceConfiguration {
return { ipAddress, subnetMask, defaultGateway, dnsServer };
}
/** One interface of a router or gateway. */
export interface InterfaceConfiguration {
readonly ipAddress: string;
readonly subnetMask: string;
}
/** Map key for an interface: node and port. */
export type InterfaceKey = string;
export function interfaceKey(nodeId: Id, portId: Id): InterfaceKey {
return `${nodeId}/${portId}`;
}
/** A row of the manual routing table (Java `Weiterleitungstabelle`). */
export interface ManualRoute {
readonly destinationNetwork: string;
readonly subnetMask: string;
readonly gateway: string;
readonly interfaceIPAddress: string;
}
/** Longest numeric mask wins; on a tie the first row stays. */
export function bestMatchingManualRoute(
targetIPAddress: string,
routes: readonly ManualRoute[],
): ManualRoute | undefined {
const target = parseIPv4(targetIPAddress);
if (target === undefined) return undefined;
let best: ManualRoute | undefined;
let bestMask: number | undefined;
for (const route of routes) {
const destination = parseIPv4(route.destinationNetwork);
const mask = parseIPv4(route.subnetMask);
if (destination === undefined || mask === undefined) continue;
if (destination !== maskAddress(target, mask)) continue;
if (bestMask !== undefined && mask <= bestMask) continue;
bestMask = mask;
best = route;
}
return best;
}
export interface SwitchConfiguration {
readonly ssid: string;
readonly retentionTimeMilliseconds: number;
}
/** Java SwitchFirmware keeps a learned MAC address for five minutes. */
export const DEFAULT_SWITCH_RETENTION_MILLISECONDS = 300_000;
export function switchConfiguration(
ssid: string,
retentionTimeMilliseconds = DEFAULT_SWITCH_RETENTION_MILLISECONDS,
): SwitchConfiguration {
return { ssid, retentionTimeMilliseconds };
}
/** The first six hex digits of the node's UUID, lower case. */
export function defaultSwitchConfiguration(nodeId: Id): SwitchConfiguration {
const compact = nodeId.replaceAll('-', '').toLowerCase();
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;
}
// ── 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> = {}): 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;
// ── Installed programs (Java installierteAnwendungen) ──────────────────────
/** Swift's TopologyRuntimeInstallableProgram. */
export type InstallableProgram =
| 'commandPrompt'
| 'fileExplorer'
| 'imageViewer'
| 'textEditor'
| 'webServer'
| 'webBrowser'
| 'echoServer'
| 'simpleClient'
| 'dnsServer'
| 'dhcpServer'
| 'firewall'
| 'emailClient'
| 'emailServer'
| 'gnutella';
export interface WebServerConfiguration {
readonly port: number;
/** Where the served files are: Java keeps them in /webserver, Swift moves them to /www. */
readonly documentRoot: string;
/** Serve by host name, from the hosts named in www.conf/vhosts (Java's useVHost). */
readonly virtualHosts: boolean;
}
export const DEFAULT_WEB_SERVER_PORT = 80;
export interface GnutellaConfiguration {
readonly maximumKnownPeers: number;
}
export type DnsRecordType = 'A' | 'MX' | 'NS';
/**
* One record a DNS server answers with (Swift's TopologyDNSResourceRecord):
* `name` in lower case without the final dot (the root is "."), `target` an
* IPv4 address for A, an absolute name ("mail.filius.de.") for MX and NS.
*/
export interface DnsResourceRecord {
readonly name: string;
readonly type: DnsRecordType;
readonly ttlSeconds: number;
readonly target: string;
}
export interface DnsServerConfiguration {
readonly recursiveResolutionEnabled: boolean;
readonly forwardingServerIPAddress?: string;
/** In the order of Java's /dns/hosts, which stays in the file system as it is. */
readonly records: readonly DnsResourceRecord[];
}
/**
* An e-mail address as Java's AddressEntry holds it: a display name, an
* address, or both (Swift's TopologyRuntimeEmailAddress wants the address).
*/
export interface EmailAddress {
readonly name: string | null;
readonly mailAddress: string | null;
}
/** An e-mail (Swift's TopologyRuntimeEmailMessage without its run-time delivery identity). */
export interface EmailMessage {
/** Numbered from 1 per program, in folder order, as Swift numbers them. */
readonly id: number;
readonly from: EmailAddress;
readonly to: readonly EmailAddress[];
readonly cc: readonly EmailAddress[];
readonly bcc: readonly EmailAddress[];
readonly subject: string;
readonly body: string;
/** Only when the file gives one; Java never sets it. */
readonly receivedAtMilliseconds?: number;
readonly isNew: boolean;
readonly isMarkedForDeletion: boolean;
/** A message of the sent folder. */
readonly isSent: boolean;
}
/** The account an e-mail client uses. First and last name stay apart, as Java keeps them. */
export interface EmailClientAccount {
readonly pop3Host: string;
readonly pop3Port: number;
readonly smtpHost: string;
readonly smtpPort: number;
readonly username: string;
readonly password: string;
readonly firstName: string;
readonly lastName: string;
readonly email: string;
}
/** Java's Kontakt: an address book entry. */
export interface EmailContact {
readonly name: string;
readonly firstName: string;
readonly street: string;
readonly houseNumber: number;
readonly postalCode: number;
readonly city: string;
readonly email: string;
readonly phone: string;
}
export interface EmailClientConfiguration {
/** Null when the client has none (Java opens it without one). */
readonly account: EmailClientAccount | null;
readonly inbox: readonly EmailMessage[];
readonly sent: readonly EmailMessage[];
readonly drafts: readonly EmailMessage[];
readonly addressBook: readonly EmailContact[];
readonly nextMessageID: number;
}
export interface EmailServerAccount {
readonly username: string;
readonly password: string;
readonly firstName: string;
readonly lastName: string;
readonly mailbox: readonly EmailMessage[];
}
export interface EmailServerConfiguration {
readonly domain: string;
readonly pop3Port: number;
readonly accounts: readonly EmailServerAccount[];
readonly nextMessageID: number;
}
export const DEFAULT_POP3_PORT = 110;
export const DEFAULT_SMTP_PORT = 25;
/**
* Java's echo server (ServerBaustein) and generic client (ClientBaustein)
* keep their port and target in the file; Swift asks for them at run time.
*/
export interface EchoServerConfiguration {
readonly port: number;
}
export interface SimpleClientConfiguration {
readonly targetAddress: string;
readonly targetPort: number;
}
// ── Firewall (Java filius.software.firewall.Firewall, FirewallRule) ─────────
export const FIREWALL_PROTOCOL = { all: -1, icmp: 1, tcp: 6, udp: 17 } as const;
export type FirewallProtocol = keyof typeof FIREWALL_PROTOCOL;
export type FirewallAction = 'drop' | 'accept';
/** Java writes this as the source of a rule that means "any directly connected network". */
export const DIRECTLY_CONNECTED_SOURCE_MARKER = '999.999.999.999';
export const ALL_PORTS = -1;
export interface FirewallRule {
readonly sourceIPAddress: string;
readonly sourceSubnetMask: string;
readonly destinationIPAddress: string;
readonly destinationSubnetMask: string;
/** A port number or ALL_PORTS. */
readonly port: number;
readonly protocol: FirewallProtocol;
readonly action: FirewallAction;
}
export function firewallRule(fields: Partial<FirewallRule> = {}): FirewallRule {
return {
sourceIPAddress: fields.sourceIPAddress ?? '',
sourceSubnetMask: fields.sourceSubnetMask ?? '',
destinationIPAddress: fields.destinationIPAddress ?? '',
destinationSubnetMask: fields.destinationSubnetMask ?? '',
port: fields.port ?? ALL_PORTS,
protocol: fields.protocol ?? 'tcp',
action: fields.action ?? 'drop',
};
}
export interface FirewallConfiguration {
readonly isActive: boolean;
readonly defaultPolicy: FirewallAction;
readonly dropIcmp: boolean;
readonly filterSynSegmentsOnly: boolean;
readonly filterUdp: boolean;
readonly rules: readonly FirewallRule[];
}
/** What a router or gateway has without a firewall configured: inactive. */
export function firewallConfiguration(fields: Partial<FirewallConfiguration> = {}): FirewallConfiguration {
return {
isActive: fields.isActive ?? false,
defaultPolicy: fields.defaultPolicy ?? 'drop',
dropIcmp: fields.dropIcmp ?? false,
filterSynSegmentsOnly: fields.filterSynSegmentsOnly ?? true,
filterUdp: fields.filterUdp ?? true,
rules: fields.rules ?? [],
};
}
/** What Java installs as the personal firewall of a PC: active, drop by default. */
export const JAVA_PERSONAL_FIREWALL_DEFAULTS: FirewallConfiguration = firewallConfiguration({
isActive: true,
defaultPolicy: 'drop',
dropIcmp: false,
filterSynSegmentsOnly: true,
filterUdp: true,
rules: [],
});