Files
mdg-filius/packages/fls/src/systemSoftware.ts
T
julian.kiedaischandClaude Opus 5.5 4dcc7e09fc feat(fls): DHCP, static NAT and modem settings as Java loads them
readSystemSoftware adds the DHCP client switch and server settings of
every IP stack, a home router's static NAT table (port forwarding) and a
modem's mode, port and address. All 23 corpus files match Java.

Static DHCP assignments follow setStaticAssignedAddresses: each entry is
split at Java whitespace (the same 25 characters as Character.isWhitespace,
measured over the BMP), and an entry without MAC and IP ends the list, as
Java throws there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:06:08 +00:00

295 lines
12 KiB
TypeScript

// The settings of a node's system software that the topology needs, read as
// Java FILIUS holds them after loading: a switch's SSID and learning time, a
// host's wireless SSID, a router's RIP switch, the manual routes, and the
// firewall of a router or home router.
// Java counterparts: filius.software.system.SwitchFirmware, Betriebssystem,
// VermittlungsrechnerBetriebssystem, GatewayFirmware, InternetKnotenBetriebssystem,
// filius.software.vermittlungsschicht.Weiterleitungstabelle,
// filius.software.firewall.Firewall and FirewallRule.
//
// Also DHCP (client switch and server settings), a home router's static NAT
// table (port forwarding) and a modem's connection settings.
// Java counterparts as well: filius.software.dhcp.DHCPServer, ModemFirmware.
//
// Checked against Java (corpus.java.json, `system`). File systems and
// applications are later layers.
//
// A router's firewall is one object its constructor makes and hangs in two
// places: the installed applications and the configuration plug-ins of its
// web server. XMLEncoder writes its settings wherever it meets it first —
// in the corpus, under the web server's `konfig` plug-in. The decoder does
// not know Java's constructors and sees a placeholder per path; they are
// merged here.
import { isJavaObject, isUnresolved, listItems, mapEntries, type JavaObject, type JavaValue } from './javabeans.js';
import type { FlsNodeClass } from './topology.js';
export interface FlsFirewallRule {
srcIP: string | null;
srcMask: string | null;
destIP: string | null;
destMask: string | null;
/** A port, or -1 for all. */
port: number;
/** -1 all, 1 ICMP, 6 TCP, 17 UDP. */
protocol: number;
/** 0 drop, 1 accept. */
action: number;
}
export interface FlsFirewall {
activated: boolean;
/** 0 drop, 1 accept. */
defaultPolicy: number;
dropICMP: boolean;
filterSYNSegmentsOnly: boolean;
filterUdp: boolean;
rules: FlsFirewallRule[];
}
/** The settings of Java's DHCPServer under English names (Java's own: javaDhcpServer in the topology test). */
export interface FlsDhcpServer {
isActive: boolean;
lowerBound: string | null;
upperBound: string | null;
gateway: string | null;
dnsServer: string | null;
useOwnSettings: boolean;
/** As Java keeps them after setStaticAssignedAddresses: MAC and IP of each entry. */
staticAssignedAddresses: { mac: string; ip: string }[];
}
export interface FlsModem {
/** 1 server, 2 client. */
mode: number;
port: number;
/** Java's ipAdresse: the host a client modem connects to. */
ipAddress: string | null;
}
export interface FlsSystemSoftware {
/**
* Switch: its SSID, undefined when the file has none (Java rolls one).
* Rechner, Notebook: the wireless network it joins, null for none.
*/
ssid?: string | null | undefined;
/** Switch: how long it keeps a learned MAC address, in milliseconds. */
retentionTime?: number;
/** Vermittlungsrechner. */
ripEnabled?: boolean;
/** Every node with an IP stack: rows of destination, mask, gateway, interface address — as the file has them. */
manualRoutes?: (string | null)[][];
/** Vermittlungsrechner, Gateway. */
firewall?: FlsFirewall;
/** Every node with an IP stack: whether it asks a DHCP server for its address. */
dhcpClient?: boolean;
/** Every node with an IP stack. */
dhcpServer?: FlsDhcpServer;
/** Gateway: rows of protocol, public port, LAN address, LAN port — as the file has them. */
staticNAT?: (string | null)[][];
/** Modem. */
modem?: FlsModem;
}
type Warn = (message: string) => void;
const FIREWALL_CLASS = 'filius.software.firewall.Firewall';
const WEB_SERVER_CLASS = 'filius.software.www.WebServer';
export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass, warn: Warn): FlsSystemSoftware {
const system = device.properties.get('systemSoftware');
const software = isJavaObject(system) ? system : undefined;
const value = (property: string) => software?.properties.get(property);
const result: FlsSystemSoftware = {};
switch (javaClass) {
case 'Switch':
result.ssid = optionalString(value('SSID'), 'SSID', warn);
result.retentionTime = integer(value('retentionTime'), 300_000, 'retentionTime', warn);
break;
case 'Rechner':
case 'Notebook':
result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null;
break;
case 'Vermittlungsrechner':
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn);
break;
case 'Gateway':
case 'Modem':
break;
}
if (javaClass !== 'Switch' && javaClass !== 'Modem') {
result.manualRoutes = manualRoutes(value('weiterleitungstabelle'), warn);
result.dhcpClient = boolean(value('DHCPKonfiguration'), false, 'DHCPKonfiguration', warn);
result.dhcpServer = dhcpServer(value('DHCPServer'), warn);
}
if (javaClass === 'Gateway') result.staticNAT = rowsOfStrings(value('staticNAT'));
if (javaClass === 'Modem') {
result.modem = {
mode: integer(value('mode'), 2, 'modem mode', warn),
port: integer(value('port'), 12_345, 'modem port', warn),
ipAddress: modemAddress(optionalString(value('ipAdresse'), 'modem ipAdresse', warn)),
};
}
if (javaClass === 'Vermittlungsrechner' || javaClass === 'Gateway') {
result.firewall = firewall(software, warn);
}
return result;
}
function manualRoutes(table: JavaValue | undefined, warn: Warn): (string | null)[][] {
if (table === undefined || table === null) return [];
if (!isJavaObject(table)) {
warn(`weiterleitungstabelle is ${describe(table)}, ignored`);
return [];
}
return rowsOfStrings(table.properties.get('manuelleTabelle'));
}
function modemAddress(value: string | null | undefined): string | null {
return value === undefined ? 'localhost' : value;
}
/** A list of String arrays (routes, static NAT); anything but a string is null. */
function rowsOfStrings(list: JavaValue | undefined): (string | null)[][] {
return listItems(list).map((row) => listItems(row).map((cell) => (typeof cell === 'string' ? cell : null)));
}
/** The DHCP server every IP stack's constructor makes: off, all addresses 0.0.0.0. */
function dhcpServer(server: JavaValue | undefined, warn: Warn): FlsDhcpServer {
const at = (property: string) => (isJavaObject(server) ? server.properties.get(property) : undefined);
const address = (property: string) => {
const value = optionalString(at(property), `DHCPServer ${property}`, warn);
return value === undefined ? '0.0.0.0' : value;
};
return {
isActive: boolean(at('aktiv'), false, 'DHCPServer aktiv', warn),
lowerBound: address('untergrenze'),
upperBound: address('obergrenze'),
gateway: address('gatewayip'),
dnsServer: address('dnsserverip'),
useOwnSettings: boolean(at('ownSettings'), false, 'DHCPServer ownSettings', warn),
staticAssignedAddresses: staticAssignments(at('staticAssignedAddresses'), warn),
};
}
/**
* setStaticAssignedAddresses: each entry split at whitespace (StringUtils.split),
* MAC and IP from the first two parts. An entry with fewer throws inside the
* loop — XMLDecoder swallows it, and the entries after it are lost.
*/
function staticAssignments(list: JavaValue | undefined, warn: Warn): { mac: string; ip: string }[] {
const result: { mac: string; ip: string }[] = [];
for (const entry of listItems(list)) {
const parts = typeof entry === 'string' ? javaWhitespaceSplit(entry) : [];
if (parts.length < 2) {
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP; Java stops reading here`);
break;
}
result.push({ mac: parts[0]!, ip: parts[1]! });
}
return result;
}
/**
* Character.isWhitespace: space, line and paragraph separators except the
* no-break ones (U+00A0, U+2007, U+202F), and the controls TAB, LF, VT, FF,
* CR, FS, GS, RS, US.
*/
const JAVA_WHITESPACE = /(?:(?![\u00A0\u2007\u202F])[\p{Zs}\p{Zl}\p{Zp}]|[\t\n\u000B\f\r\u001C-\u001F])+/u;
function javaWhitespaceSplit(text: string): string[] {
return text.split(JAVA_WHITESPACE).filter((part) => part !== '');
}
/** Every object the file reaches the router's one firewall by. */
function firewallObjects(software: JavaObject | undefined): JavaObject[] {
const found: JavaObject[] = [];
const applications = new Map(mapEntries(software?.properties.get('installierteAnwendungen')));
const installed = applications.get(FIREWALL_CLASS);
if (isJavaObject(installed)) found.push(installed);
const webServer = applications.get(WEB_SERVER_CLASS);
if (isJavaObject(webServer)) {
for (const [, plugin] of mapEntries(webServer.properties.get('plugins'))) {
const pluginFirewall = isJavaObject(plugin) ? plugin.properties.get('firewall') : undefined;
if (isJavaObject(pluginFirewall) && !found.includes(pluginFirewall)) found.push(pluginFirewall);
}
}
return found;
}
/** The firewall as the router's constructor leaves it (off, drop), then the file's settings. */
function firewall(software: JavaObject | undefined, warn: Warn): FlsFirewall {
const properties = new Map<string, JavaValue>();
for (const object of firewallObjects(software)) {
for (const [property, value] of object.properties) {
if (properties.has(property) && properties.get(property) !== value) {
warn(`firewall: ${property} is set in two places, the later one is used`);
}
properties.set(property, value);
}
}
const at = (property: string) => properties.get(property);
// setAllowRelatedPackets is the old name of setFilterSYNSegmentsOnly.
const filterSyn = at('filterSYNSegmentsOnly') ?? at('allowRelatedPackets');
return {
activated: boolean(at('activated'), false, 'firewall activated', warn),
defaultPolicy: integer(at('defaultPolicy'), 0, 'firewall defaultPolicy', warn),
dropICMP: boolean(at('dropICMP'), false, 'firewall dropICMP', warn),
filterSYNSegmentsOnly: boolean(filterSyn, true, 'firewall filterSYNSegmentsOnly', warn),
filterUdp: boolean(at('filterUdp'), true, 'firewall filterUdp', warn),
rules: listItems(at('ruleset')).flatMap((rule) => (isJavaObject(rule) ? [firewallRule(rule, warn)] : [])),
};
}
/**
* FirewallRule: public fields set one by one (`getField(name).set`), or the
* seven-argument constructor. Defaults: all addresses empty, all ports, TCP,
* drop.
*/
function firewallRule(rule: JavaObject, warn: Warn): FlsFirewallRule {
const [srcIP, srcMask, destIP, destMask, port, protocol, action] = rule.args.length === 7 ? rule.args : [];
const field = (name: string, fromConstructor: JavaValue | undefined) => rule.properties.get(name) ?? fromConstructor;
const text = (name: string, fromConstructor: JavaValue | undefined) => {
const value = field(name, fromConstructor) ?? '';
if (typeof value === 'string' || value === null) return value;
warn(`firewall rule: ${name} is ${describe(value)}, used ""`);
return '';
};
return {
srcIP: text('srcIP', srcIP),
srcMask: text('srcMask', srcMask),
destIP: text('destIP', destIP),
destMask: text('destMask', destMask),
port: integer(field('port', port), -1, 'firewall rule port', warn),
protocol: integer(field('protocol', protocol), 6, 'firewall rule protocol', warn),
action: integer(field('action', action), 0, 'firewall rule action', warn),
};
}
function optionalString(value: JavaValue | undefined, name: string, warn: Warn): string | null | undefined {
if (value === undefined || value === null || typeof value === 'string') return value;
warn(`${name} is ${describe(value)}, ignored`);
return undefined;
}
function boolean(value: JavaValue | undefined, fallback: boolean, name: string, warn: Warn): boolean {
if (value === undefined) return fallback;
if (typeof value === 'boolean') return value;
warn(`${name} is ${describe(value)}, used ${fallback}`);
return fallback;
}
function integer(value: JavaValue | undefined, fallback: number, name: string, warn: Warn): number {
if (value === undefined) return fallback;
if (typeof value === 'number' && Number.isInteger(value)) return value;
warn(`${name} is ${describe(value)}, used ${fallback}`);
return fallback;
}
function describe(value: JavaValue): string {
if (isUnresolved(value)) return `unresolved (${value.reason})`;
if (isJavaObject(value)) return `an object of ${value.className}`;
return typeof value === 'bigint' ? `${value}` : JSON.stringify(value);
}