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
+222
View File
@@ -0,0 +1,222 @@
// Ported from filius-ipad (GPLv2/GPLv3):
// State/TopologyJavaRouteTable.swift (row projection, validation, drafts)
// Java counterpart: filius.software.vermittlungsschicht.Weiterleitungstabelle
// (holeTabelle: the order of the generated rows).
// The Swift file also carries table geometry for SwiftUI; that belongs to the
// editor and is not ported here.
import { formatIPv4, maskAddress } from './ipv4.js';
import type { InterfaceConfiguration, ManualRoute } from './config.js';
export type RouteOrigin = 'localHost' | 'connected' | 'localhost' | 'manual' | 'defaultGateway' | 'rip';
export interface RouteRow {
readonly destinationNetwork: string;
readonly subnetMask: string;
readonly nextHop: string;
readonly interfaceIPAddress: string;
readonly origin: RouteOrigin;
readonly isEditable: boolean;
readonly metric?: number;
readonly expiresAtMilliseconds?: number;
}
export type RouteTableColumn = 'destination' | 'subnetMask' | 'nextGateway' | 'interface';
/**
* A Java table octet: digits only, leading zeros allowed, at most three
* significant digits, 0…255 ("001" is valid, " 1" and "+1" are not).
*/
function parseJavaOctet(segment: string): number | undefined {
if (segment.length === 0 || !/^[0-9]+$/.test(segment)) return undefined;
const significant = segment.replace(/^0+/, '');
if (significant.length === 0) return 0;
if (significant.length > 3) return undefined;
const value = Number(significant);
return value <= 255 ? value : undefined;
}
function parseJavaIPv4(value: string): number | undefined {
const segments = value.split('.');
if (segments.length !== 4) return undefined;
let address = 0;
for (const segment of segments) {
const octet = parseJavaOctet(segment);
if (octet === undefined) return undefined;
address = ((address << 8) | octet) >>> 0;
}
return address;
}
export function isValidJavaIPAddress(value: string): boolean {
return parseJavaIPv4(value) !== undefined;
}
/**
* FILIUS uses `musterSubNetz`, a dotted-decimal syntax check. It deliberately
* accepts non-contiguous masks in the route editor.
*/
export function isValidJavaSubnetMask(value: string): boolean {
return parseJavaIPv4(value) !== undefined;
}
export function isValidRouteCell(value: string, column: RouteTableColumn): boolean {
return column === 'subnetMask' ? isValidJavaSubnetMask(value) : isValidJavaIPAddress(value);
}
function sameNetwork(lhs: string, rhs: string, mask: string): boolean {
const left = parseJavaIPv4(lhs);
const right = parseJavaIPv4(rhs);
const netmask = parseJavaIPv4(mask);
if (left === undefined || right === undefined || netmask === undefined) return false;
return maskAddress(left, netmask) === maskAddress(right, netmask);
}
function networkAddress(ipAddress: string, subnetMask: string): string | undefined {
const address = parseJavaIPv4(ipAddress);
const mask = parseJavaIPv4(subnetMask);
if (address === undefined || mask === undefined) return undefined;
return formatIPv4(maskAddress(address, mask));
}
/**
* The static table as Java shows it: the interfaces' own addresses (newest
* first), their networks, loopback, the manual rows, then the default route.
*/
export function routeTableRows(
interfaceConfigurations: readonly InterfaceConfiguration[],
manualRoutes: readonly ManualRoute[],
defaultGateway: string | undefined,
): RouteRow[] {
const validInterfaces = interfaceConfigurations.filter(
(configuration) =>
parseJavaIPv4(configuration.ipAddress) !== undefined &&
parseJavaIPv4(configuration.subnetMask) !== undefined,
);
const result: RouteRow[] = manualRoutes.map((route) => ({
destinationNetwork: route.destinationNetwork,
subnetMask: route.subnetMask,
nextHop: route.gateway,
interfaceIPAddress: route.interfaceIPAddress,
origin: 'manual',
isEditable: true,
}));
result.unshift({
destinationNetwork: '127.0.0.0',
subnetMask: '255.0.0.0',
nextHop: '127.0.0.1',
interfaceIPAddress: '127.0.0.1',
origin: 'localhost',
isEditable: false,
});
for (const configuration of validInterfaces) {
const network = networkAddress(configuration.ipAddress, configuration.subnetMask);
if (network === undefined) continue;
result.unshift({
destinationNetwork: network,
subnetMask: configuration.subnetMask,
nextHop: configuration.ipAddress,
interfaceIPAddress: configuration.ipAddress,
origin: 'connected',
isEditable: false,
});
}
for (const configuration of validInterfaces) {
result.unshift({
destinationNetwork: configuration.ipAddress,
subnetMask: '255.255.255.255',
nextHop: '127.0.0.1',
interfaceIPAddress: '127.0.0.1',
origin: 'localHost',
isEditable: false,
});
}
const trimmedGateway = defaultGateway?.trim() ?? '';
const primaryInterface = validInterfaces[0];
if (parseJavaIPv4(trimmedGateway) !== undefined && primaryInterface !== undefined) {
let selectedInterface = primaryInterface.ipAddress;
for (const configuration of validInterfaces) {
if (sameNetwork(trimmedGateway, configuration.ipAddress, configuration.subnetMask)) {
selectedInterface = configuration.ipAddress;
}
}
result.push({
destinationNetwork: '0.0.0.0',
subnetMask: '0.0.0.0',
nextHop: trimmedGateway,
interfaceIPAddress: selectedInterface,
origin: 'defaultGateway',
isEditable: false,
});
}
return result;
}
export function visibleRouteRows(rows: readonly RouteRow[], showAllEntries: boolean): RouteRow[] {
return showAllEntries ? [...rows] : rows.filter((row) => row.isEditable);
}
/** One row being edited in the route editor. */
export interface RouteDraft {
readonly id: string;
destinationNetwork: string;
subnetMask: string;
nextGateway: string;
interfaceIPAddress: string;
isPendingInsertion: boolean;
}
export function blankRouteDraft(id: string): RouteDraft {
return {
id,
destinationNetwork: '',
subnetMask: '',
nextGateway: '',
interfaceIPAddress: '',
isPendingInsertion: true,
};
}
export function routeDraftFromRoute(id: string, route: ManualRoute): RouteDraft {
return {
id,
destinationNetwork: route.destinationNetwork,
subnetMask: route.subnetMask,
nextGateway: route.gateway,
interfaceIPAddress: route.interfaceIPAddress,
isPendingInsertion: false,
};
}
/** The route a draft stands for — only once all four cells are valid. */
export function manualRouteFromDraft(draft: RouteDraft): ManualRoute | undefined {
if (
!isValidRouteCell(draft.destinationNetwork, 'destination') ||
!isValidRouteCell(draft.subnetMask, 'subnetMask') ||
!isValidRouteCell(draft.nextGateway, 'nextGateway') ||
!isValidRouteCell(draft.interfaceIPAddress, 'interface')
) {
return undefined;
}
return {
destinationNetwork: draft.destinationNetwork,
subnetMask: draft.subnetMask,
gateway: draft.nextGateway,
interfaceIPAddress: draft.interfaceIPAddress,
};
}
/** At most one blank row waits at the end of the editor. */
export function appendingBlankDraftIfNeeded(
drafts: readonly RouteDraft[],
newId: () => string,
): RouteDraft[] {
if (drafts.some((draft) => draft.isPendingInsertion)) return [...drafts];
return [...drafts, blankRouteDraft(newId())];
}