// 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(); 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); }