Routers and home routers start a web server on port 80 with Java's index, log and routes pages in the project's language (routerPages.ts); the firewall keeps a log per node that outlives a new start; RIP routes carry the telling router's public address. TCP counts sequence numbers and segments in Java's characters, not bytes. RouterWebRun records six stories, with firewalls now built by ScenarioNetwork. Not taken from Java: its router server sometimes never listening (a race in setAktiv), and </titel>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
225 lines
7.4 KiB
TypeScript
225 lines
7.4 KiB
TypeScript
// 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;
|
|
/** A RIP route: the router that told it, by its first address (Java's hopPublicIp, a hint for the administrator). */
|
|
readonly hopPublicIPAddress?: string;
|
|
}
|
|
|
|
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())];
|
|
}
|