feat(core): event queue, Ethernet, switch, ARP, IPv4, ICMP, routing and firewall

First slice of the TypeScript port of the filius-ipad runtime engine,
with 29 tests ported from the Swift suite. Parts not ported yet throw
NotPortedError instead of guessing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:22:57 +00:00
co-authored by Claude Opus 5.5
parent 7caee33b3d
commit 696334aa5a
29 changed files with 6199 additions and 11 deletions
+150
View File
@@ -0,0 +1,150 @@
// 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));
}
// ── 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: [],
});