feat(core): the firewall settings page of routers after Java, measured against Java

The page konfig changes the firewall at run time (into the snapshot, back
into the project through projectWithSimulationChanges). KonfigCases feeds
Java's plug-in directly (25 cases, de and en); RouterWebRun plays two
stories over the network. Ten Java bugs named, not adopted. The web server
answers a method not written GET or POST with 501, as Java does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 18:13:31 +00:00
co-authored by Claude Opus 5.5
parent 7dac57cbbe
commit 8438b42018
23 changed files with 13229 additions and 24 deletions
+10 -8
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -23,6 +23,7 @@ export * from './runtime/apps/server.js';
export * from './runtime/apps/echo.js';
export * from './runtime/apps/web.js';
export * from './runtime/apps/routerPages.js';
export * from './runtime/apps/firewallSettings.js';
export * from './runtime/http.js';
export * from './util/equal.js';
export * from './util/javaBase64.js';
File diff suppressed because one or more lines are too long
@@ -81,6 +81,11 @@ const TEXTS: Record<PageLanguage, PageTexts> = {
},
};
/** jfirewalldialog_msg33, msg34: a rule's action as the pages name it. */
export function firewallActionText(action: 'accept' | 'drop', language: PageLanguage): string {
return action === 'accept' ? TEXTS[language].accept : TEXTS[language].drop;
}
/** The index page a router or home router keeps in /webserver (WebServer.erzeugeIndexDatei). */
export function routerIndexPage(kind: 'router' | 'gateway', language: PageLanguage): string {
return TEMPLATES[kind === 'router' ? 'routerIndex' : 'gatewayIndex'][language];
@@ -114,7 +119,7 @@ export function firewallLogText(entry: FirewallLogEntry, language: PageLanguage)
if (entry.kind === 'icmpDropped') {
return `${texts.icmpDropped} ${entry.senderIPAddress} -> ${entry.receiverIPAddress} (code: ${entry.code}, type: ${entry.type})`;
}
return `${texts.ruleMatched} #${entry.ruleNumber} (${firewallRuleText(entry.rule, entry.firstInterface)}) -> ${entry.accepted ? texts.accept : texts.drop}`;
return `${texts.ruleMatched} #${entry.ruleNumber} (${firewallRuleText(entry.rule, entry.firstInterface)}) -> ${firewallActionText(entry.accepted ? 'accept' : 'drop', language)}`;
}
/** FirewallWebLog.holeHtmlSeite: the log since the engine was made. */
+3 -1
View File
@@ -129,6 +129,8 @@ export class WebServer extends TcpServerProgram {
});
const parsed = parseHttpRequest(message);
if (parsed.type === 'invalid') return response(parsed.statusCode);
// HTTPNachricht reads the method in any case; the server answers only GET and POST as written.
if (parsed.request.method !== HTTP_GET && parsed.request.method !== HTTP_POST) return response(501);
const path = httpPath(parsed.request.path);
let file = path === '/' ? 'index.html' : path.startsWith('/') ? path.slice(1) : path;
let folder = this.virtualHostFolder(parsed.request.host);
@@ -189,7 +191,7 @@ function javaSplit(text: string, separator: string): string[] {
/** EingabenUeberpruefung.musterDomain and musterIpAdresse. */
const DOMAIN_PATTERN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9]){0,1}(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]){0,1})*){0,1}\.{0,1}$/;
const IP_ADDRESS_PATTERN =
export const IP_ADDRESS_PATTERN =
/^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/;
function isHostName(text: string): boolean {
+18 -3
View File
@@ -116,6 +116,7 @@ import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.
import { EchoServer, SimpleClient } from './apps/echo.js';
import { WebBrowser, WebServer, type WebServerPage } from './apps/web.js';
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
import { VirtualFileSystem } from '../model/virtualFileSystem.js';
import {
PRODUCTION_TRACE_RETENTION,
@@ -1152,7 +1153,17 @@ export class NetworkRuntimeEngine {
const files = new VirtualFileSystem();
files.createDirectory('/webserver', true);
files.writeTextFile('/webserver/index.html', routerIndexPage(kind, language));
const pages = new Map<string, WebServerPage>([['log', () => firewallLogPage(this.firewallLog(nodeId), language)]]);
const pages = new Map<string, WebServerPage>([
['log', () => firewallLogPage(this.firewallLog(nodeId), language)],
[
'konfig',
(postData) => {
const result = applyFirewallSettings(this.firewallConfiguration(nodeId), postData);
if (result.configuration !== this.firewallConfiguration(nodeId)) this.setFirewallConfiguration(nodeId, result.configuration);
return firewallSettingsPage(result.configuration, language, result.rejected);
},
],
]);
if (kind === 'router') {
pages.set('routes', () => {
const rip = this.ripTable(nodeId);
@@ -5096,6 +5107,11 @@ export class NetworkRuntimeEngine {
// ── Firewall ─────────────────────────────────────────────────────────────
/** The node's firewall as it is now: changed at run time (the page konfig), it is in the snapshot, and so in the project saved from it. */
firewallConfiguration(nodeId: Id): FirewallConfiguration {
return this.stateValue.topologySnapshot.firewallConfigurationsByNodeId.get(nodeId) ?? firewallConfiguration();
}
setFirewallConfiguration(nodeId: Id, configuration: FirewallConfiguration): void {
const configurations = new Map(this.stateValue.topologySnapshot.firewallConfigurationsByNodeId);
configurations.set(nodeId, configuration);
@@ -5111,8 +5127,7 @@ export class NetworkRuntimeEngine {
* recorded, including "firewall inactive".
*/
evaluateFirewall(packet: IPv4Packet, nodeId: Id, incomingInterfaceId?: Id, frameIdentity?: number): FirewallDecision {
const configuration =
this.stateValue.topologySnapshot.firewallConfigurationsByNodeId.get(nodeId) ?? firewallConfiguration();
const configuration = this.firewallConfiguration(nodeId);
const decide = (accepted: boolean, ruleIndex: number | undefined, detail: string): FirewallDecision =>
this.recordFirewallDecision(packet, frameIdentity, nodeId, incomingInterfaceId, accepted, ruleIndex, detail);
+35 -1
View File
@@ -41,7 +41,7 @@ function words(line: string): string[] {
}
/** Java's String.trim. */
function javaTrim(text: string): string {
export function javaTrim(text: string): string {
let start = 0;
let end = text.length;
while (start < end && text.charCodeAt(start) <= 0x20) start += 1;
@@ -211,3 +211,37 @@ export function decodeUriPath(path: string): string | undefined {
}
return new TextDecoder().decode(new Uint8Array(bytes));
}
/**
* URLDecoder.decode(text, "UTF-8"), as a form's data is read: "+" a space,
* each run of "%xy" the characters of those UTF-8 bytes; undefined where
* Java throws (an escape cut off or not hexadecimal; Java's parseInt also
* takes a sign there, and refuses only a negative value).
*/
export function javaUrlDecode(text: string): string | undefined {
let decoded = '';
let index = 0;
while (index < text.length) {
const character = text[index]!;
if (character === '+') {
decoded += ' ';
index += 1;
} else if (character === '%') {
const bytes: number[] = [];
while (index + 2 < text.length && text[index] === '%') {
const hex = text.slice(index + 1, index + 3);
if (!/^[+-]?[0-9A-Fa-f]+$/.test(hex)) return undefined;
const value = Number.parseInt(hex, 16);
if (value < 0) return undefined;
bytes.push(value);
index += 3;
}
if (index < text.length && text[index] === '%') return undefined;
decoded += new TextDecoder().decode(new Uint8Array(bytes));
} else {
decoded += character;
index += 1;
}
}
return decoded;
}
@@ -0,0 +1,90 @@
# Cases for tools/java-oracle's KonfigCases: FILIUS' firewall settings page
# (konfig) fed with what a browser posts. Read by firewallSettings.test.ts.
case getEmpty empty
GET
case getThree three
GET
POST
case deactivate three
POST btnSave=speichern
POST firewallActivate=1&dropICMP=1&onlySYN=1&btnSave=speichern
case defaultPolicy three
POST defaultPolicy=accept&defPolSet=speichern
POST defaultPolicy=xyz&defPolSet=speichern
POST defaultPolicy=accept
POST defaultPolicy=accept&defPolSet=speichern
POST defPolSet=speichern
case moveUp three
POST radioRule=2&moveup=rauf
POST radioRule=1&moveup=rauf
POST radioRule=4&moveup=rauf
POST moveup=rauf
POST radioRule=3&radioRule=2&moveup=rauf
POST radioRule=2&radioRule=abc&moveup=rauf
POST radioRule=%2B3&moveup=rauf
case moveDown three
POST radioRule=1&movedown=runter
POST radioRule=3&movedown=runter
POST movedown=runter
case moveDownZero three
POST radioRule=0&movedown=runter
case delete three
POST radioRule=2&delrule=l%C3%B6schen
POST radioRule=0&delrule=l%C3%B6schen
POST radioRule=abc&delrule=l%C3%B6schen
POST radioRule=9&delrule=l%C3%B6schen
case addEmpty empty
POST srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=80&action=accept&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=&destIP=&destMask=&protocol=UDP&port=-1&action=drop&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=&destIP=&destMask=&protocol=tcp&port=+53+&action=ACCEPT&addrule=hinzuf%C3%BCgen
POST addrule=x
case addSource empty
POST srcIP=10.0.0.0&srcMask=255.255.255.0&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen
case addSourceMaskFirst empty
POST srcMask=255.255.255.0&srcIP=10.0.0.0&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen
case sourceRule sourceRule
GET
case addDestination empty
POST srcIP=&srcMask=&destIP=20.0.0.0&destMask=255.0.0.0&protocol=UDP&port=&action=accept&addrule=hinzuf%C3%BCgen
case addDestinationMaskFirst empty
POST srcIP=&srcMask=&destMask=255.0.0.0&destIP=20.0.0.0&protocol=UDP&port=&action=accept&addrule=hinzuf%C3%BCgen
case destinationRule destinationRule
GET
case errors three
POST srcIP=10.0.0.300&srcMask=&destIP=&destMask=255.0.255.0&protocol=UDP&port=abc&action=accept&addrule=hinzuf%C3%BCgen
GET
POST radioRule=1&moveup=rauf
POST srcIP=10.0.0.1&srcMask=1.2.3.4&destIP=&destMask=255.0.0.0&protocol=TCP&port=0&action=drop&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=0.0.0.0&destIP=1.2.3.4&destMask=0.0.0.0&protocol=*&port=65536&action=drop&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=080&action=drop&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=%2B80&action=drop&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=255.255.255.0.&destIP=&destMask=255.255.255&protocol=*&port=65535&action=drop&addrule=hinzuf%C3%BCgen
case errorAndSave three
POST srcIP=1.2.3&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&btnSave=speichern&addrule=hinzuf%C3%BCgen
case markup empty
POST srcIP=%3Cb%3E%22&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
case dollar empty
POST srcIP=%241&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
case encoding empty
POST srcIP=+&srcMask=&destIP=&destMask=&protocol=%2A&port=%38&action=drop&addrule=hinzuf%C3%BCgen
POST \u000abtnSave=speichern&firewallActivate=1
POST BTNSAVE=x&firewallActivate=1
POST =x&btnSave
case masks empty
POST srcIP=&srcMask=%2B255.255.255.0&destIP=1.2.3&destMask=255.255.255.255.0&protocol=TCP&protocol=UDP&port=&action=drop&addrule=hinzuf%C3%BCgen
POST srcIP=&srcMask=255.255.255.256&destIP=&destMask=255.255.255.255&protocol=*&port=&action=accept&action=drop&addrule=hinzuf%C3%BCgen
case portZero portZero
GET
case ampersand empty
POST srcIP=%26&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
# What Java's browser window posts (Swing's form: every field, the button pressed).
case browserForm three
POST firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=drop&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
POST onlySYN=1&defaultPolicy=accept&defPolSet=speichern&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
POST firewallActivate=1&onlySYN=1&defaultPolicy=drop&radioRule=3&moveup=rauf&srcIP=10.0.0.0&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
POST firewallActivate=1&onlySYN=1&defaultPolicy=drop&srcIP=&srcMask=&destIP=&destMask=&protocol=UDP&port=53&action=accept&addrule=hinzuf%C3%BCgen
POST firewallActivate=1&onlySYN=1&defaultPolicy=drop&radioRule=1&delrule=l%C3%B6schen&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
case malformed three
POST radioRule=2&moveup=rauf&x=%zz
POST radioRule=2&moveup=rauf&x=%
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+163
View File
@@ -0,0 +1,163 @@
// The firewall settings page of routers and home routers (konfig,
// apps/firewallSettings.ts) against Java FILIUS' own: tools/java-oracle's
// KonfigCases fed Java's page with the steps of firewall-settings.cases.txt
// (firewall-settings.<de|en>.java.json). Each step's page and the firewall
// after it must be Java's, in both languages, except where Java errs.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
applyFirewallSettings,
firewallConfiguration,
firewallRule,
firewallSettingsPage,
javaUrlDecode,
type FirewallConfiguration,
type FirewallRule,
type PageLanguage,
} from '../src/index.js';
const here = dirname(fileURLToPath(import.meta.url));
type JavaRule = [string, string, string, string, number, string, string];
interface JavaFirewall {
isActive: boolean;
dropIcmp: boolean;
filterSynSegmentsOnly: boolean;
defaultPolicy: string;
rules: JavaRule[];
}
interface JavaStep {
post: string | null;
page: string | null;
firewall: JavaFirewall;
}
interface JavaCase {
name: string;
start: string;
steps: JavaStep[];
}
function javaRule(rule: FirewallRule): JavaRule {
return [rule.sourceIPAddress, rule.sourceSubnetMask, rule.destinationIPAddress, rule.destinationSubnetMask, rule.port, rule.protocol, rule.action];
}
function asJava(configuration: FirewallConfiguration): JavaFirewall {
return {
isActive: configuration.isActive,
dropIcmp: configuration.dropIcmp,
filterSynSegmentsOnly: configuration.filterSynSegmentsOnly,
defaultPolicy: configuration.defaultPolicy,
rules: configuration.rules.map(javaRule),
};
}
/** KonfigCases' start states. */
const STARTS: Record<string, FirewallConfiguration> = {
empty: firewallConfiguration(),
three: firewallConfiguration({
isActive: true,
rules: [
firewallRule({ port: 80, protocol: 'tcp', action: 'accept' }),
firewallRule({ sourceIPAddress: '999.999.999.999', destinationIPAddress: '20.0.0.0', destinationSubnetMask: '255.0.0.0', protocol: 'udp', action: 'drop' }),
firewallRule({ sourceIPAddress: '10.0.0.0', sourceSubnetMask: '255.0.0.0', protocol: 'all', action: 'drop' }),
],
}),
sourceRule: firewallConfiguration({ rules: [firewallRule({ sourceIPAddress: '10.0.0.0', sourceSubnetMask: '255.255.255.0', port: 80, protocol: 'tcp', action: 'drop' })] }),
portZero: firewallConfiguration({ rules: [firewallRule({ port: 0, protocol: 'icmp', action: 'accept' })] }),
destinationRule: firewallConfiguration({ rules: [firewallRule({ destinationIPAddress: '20.0.0.0', destinationSubnetMask: '255.0.0.0', protocol: 'udp', action: 'accept' })] }),
};
type Cases = Record<string, JavaCase>;
const step = (cases: Cases, name: string, index: number) => cases[name]!.steps[index]!;
/** A step as Java means it: another step's page and firewall, which Java answers for the same state. */
const asStep =
(name: string, index: number) =>
(cases: Cases, measured: JavaStep): JavaStep => ({ ...step(cases, name, index), post: measured.post });
/**
* Where Java errs, each step as Java means it (the core's departures, see
* firewallSettings.ts).
*/
const JAVA_BUGS: Record<string, Record<number, (cases: Cases, measured: JavaStep) => JavaStep>> = {
// Java checks the source IP against the mask not read yet, and the mask
// against the IP not read yet: it refuses every rule with an address,
// whichever field comes first. The core takes the rule.
addSource: { 0: asStep('sourceRule', 0) },
addSourceMaskFirst: { 0: asStep('sourceRule', 0) },
addDestination: { 0: asStep('destinationRule', 0) },
addDestinationMaskFirst: { 0: asStep('destinationRule', 0) },
// Java keeps the error of a failed add for every later page.
errors: { 1: asStep('getThree', 0) },
// Save and add at once: Java saves, but writes the add's errors as a bare
// list and fills the form in. The core answers the saved firewall.
errorAndSave: { 0: asStep('deactivate', 0) },
// Java writes the form's text into the page as it came.
markup: { 0: (_, measured) => ({ ...measured, page: measured.page!.replace('value="<b>""', 'value="&lt;b&gt;&quot;"') }) },
// A "$" in the form's text: Java's replaceAll reads it as a group, throws,
// and the page fails (500). The core answers as for any other bad address.
dollar: { 0: (cases, measured) => ({ ...measured, page: step(cases, 'markup', 0).page!.replace('value="<b>""', 'value="$1"') }) },
// An "&" typed into a field: Java decodes the data before it splits it,
// reads an empty source and adds a rule. The core keeps the "&" in its
// field and answers as for any other bad address.
ampersand: { 0: (cases) => ({ ...step(cases, 'markup', 0), page: step(cases, 'markup', 0).page!.replace('value="<b>""', 'value="&amp;"') }) },
// Moving rule 0 down: Java throws (its worker thread ends, no page comes).
moveDownZero: { 0: asStep('getThree', 0) },
// An escape that is not one: Java's decoder throws. The core reads the
// data as it came, as Java's code means to.
malformed: { 0: asStep('moveUp', 0), 1: asStep('getThree', 0) },
};
function javaCases(language: PageLanguage): Cases {
const list = JSON.parse(readFileSync(join(here, `firewall-settings.${language}.java.json`), 'utf8')) as JavaCase[];
return Object.fromEntries(list.map((each) => [each.name, each]));
}
describe('the firewall settings page as Java FILIUS answers it', () => {
for (const language of ['de', 'en'] as const) {
const cases = javaCases(language);
it(`corrects Java only where it errs (${language})`, () => {
for (const [name, steps] of Object.entries(JAVA_BUGS)) {
for (const [index, correct] of Object.entries(steps)) {
const measured = step(cases, name, Number(index));
expect(correct(cases, measured), `${name} ${index}`).not.toEqual(measured);
}
}
});
for (const [name, javaCase] of Object.entries(cases)) {
it(`${name} (${language})`, () => {
let configuration = STARTS[javaCase.start]!;
for (const [index, measured] of javaCase.steps.entries()) {
const expected = JAVA_BUGS[name]?.[index]?.(cases, measured) ?? measured;
const result = applyFirewallSettings(configuration, measured.post ?? undefined);
configuration = result.configuration;
expect(asJava(configuration), `step ${index}: the firewall`).toEqual(expected.firewall);
expect(firewallSettingsPage(configuration, language, result.rejected), `step ${index}: the page`).toBe(expected.page);
}
});
}
}
});
describe("a form's data as Java's URLDecoder reads it", () => {
it('turns + into spaces and runs of escapes into UTF-8 characters', () => {
expect(javaUrlDecode('a+b%C3%BC%E2%82%AC%41')).toBe('a bü€A');
expect(javaUrlDecode('%+f%-0x')).toBe('\u000f\u0000x');
});
it('fails where Java throws: an escape cut off, not hexadecimal, or negative', () => {
expect(javaUrlDecode('%zz')).toBeUndefined();
expect(javaUrlDecode('%4')).toBeUndefined();
expect(javaUrlDecode('%41%')).toBeUndefined();
expect(javaUrlDecode('%-1')).toBeUndefined();
});
});
+37
View File
@@ -9,6 +9,7 @@ import {
firewallLogPage,
firewallRule,
firewallRuleText,
firewallSettingsPage,
forwardingTablePage,
interfaceKey,
javaTextLength,
@@ -140,6 +141,42 @@ describe("a router's firewall log", () => {
});
});
describe("a router's settings page (konfig)", () => {
const pageOf = (engine: NetworkRuntimeEngine) => {
const pages = engine.webBrowser(LEFT)!.log.all.flatMap((line) => (line.kind === 'page' ? [line] : []));
return pages[pages.length - 1];
};
const ask = (engine: NetworkRuntimeEngine, data?: string) => {
engine.webBrowser(LEFT)!.open('10.0.0.1/konfig', data);
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 5_000 });
return pageOf(engine);
};
it('changes the firewall at run time, into the snapshot; a page without data changes nothing', () => {
const engine = startedEngine({ ...network(), webBrowserNodeIds: new Set([LEFT]) });
const before = engine.state.topologySnapshot;
expect(ask(engine)).toMatchObject({ statusCode: 200, contentType: 'text/html' });
expect(engine.state.topologySnapshot).toBe(before);
const page = ask(engine, 'firewallActivate=1&btnSave=speichern&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop');
expect(engine.state.topologySnapshot.firewallConfigurationsByNodeId.get(ROUTER)).toEqual(
firewallConfiguration({ isActive: true, defaultPolicy: 'accept', filterSynSegmentsOnly: false, rules: [firewallRule({ protocol: 'udp', port: 7, action: 'drop' })] }),
);
// The browser reads the answer as HTTPNachricht does: without its last empty lines.
expect(page?.body).toBe(firewallSettingsPage(engine.firewallConfiguration(ROUTER), 'de').replace(/\n+$/, ''));
});
it("answers in the project's language, and on a home router as on a router", () => {
const gateway = network({ language: 'en', webBrowserNodeIds: new Set([LEFT]) });
const engine = startedEngine({ ...gateway, nodes: gateway.nodes.map((node) => (node.id === ROUTER ? { ...node, kind: 'gateway' as const } : node)) });
const page = ask(engine, 'srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=22&action=accept&addrule=add');
expect(page?.body).toContain('<title>Router Firewall Configuration</title>');
expect(engine.firewallConfiguration(ROUTER).rules).toEqual([firewallRule({ protocol: 'udp', port: 7, action: 'drop' }), firewallRule({ protocol: 'tcp', port: 22, action: 'accept' })]);
const rejected = ask(engine, 'srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=0&action=accept&addrule=add');
expect(rejected?.body).toContain('<font color="red">Erroneous input(s):<ul><li>Port invalid</li></ul></font><br>');
expect(engine.firewallConfiguration(ROUTER).rules).toHaveLength(2);
});
});
describe("a RIP route's public address (hopPublicIp)", () => {
it('is the telling router\'s, also when a shorter way replaces the route', () => {
const engine = startedEngine(network({ ripEnabledByNodeId: new Map([[ROUTER, true]]), firewallConfigurationsByNodeId: new Map() }));
+58 -3
View File
@@ -11,7 +11,9 @@
// not compared. Java's router server sometimes never listens (setAktiv wakes
// its thread only when it already waits), and takes no initial sequence
// number then; the engine's always listens: sequence numbers are compared
// below the millions. The settings page (konfig) comes with F3.2c.
// below the millions. The settings page (konfig) changes the firewall at run
// time: a ping passes once "drop ICMP" is saved off; a rule moved above the
// one that accepts the browser shuts it out.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
@@ -46,15 +48,53 @@ const open =
engine.webBrowser(browser)!.open(typed);
};
const post =
(typed: string, data: string) =>
(engine: NetworkRuntimeEngine, browser: string): void => {
engine.webBrowser(browser)!.open(typed, data);
};
const ping = (engine: NetworkRuntimeEngine, browser: string): void => {
engine.sendIcmpEcho(browser, '20.0.0.2', { sequenceNumber: 1 });
};
/** The form as Java's browser window posts it: every field, then the button pressed. */
const FORM = {
saveWithoutIcmp: 'firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop',
addTcpDrop: 'firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen',
moveSecondUp: 'firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&radioRule=2&moveup=rauf&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop',
};
const STORIES: Record<string, Story> = {
routerIndex: { scenario: 'router-web', browser: id(301), router: id(302), acts: [[500, open('10.0.0.1')]] },
routerRoutes: { scenario: 'router-web', browser: id(301), router: id(302), acts: [[500, open('10.0.0.1/routes')]] },
routerKonfig: { scenario: 'router-web', browser: id(301), router: id(302), acts: [[500, open('10.0.0.1/konfig')]] },
konfigIcmp: {
scenario: 'router-web',
browser: id(301),
router: id(302),
acts: [
[500, ping],
[3500, post('10.0.0.1/konfig', FORM.saveWithoutIcmp)],
[7000, ping],
],
},
konfigBlock: {
scenario: 'router-web',
browser: id(301),
router: id(302),
acts: [
[500, post('10.0.0.1/konfig', FORM.addTcpDrop)],
[4000, post('10.0.0.1/konfig', FORM.moveSecondUp)],
[8000, open('10.0.0.1')],
],
},
routerLog: {
scenario: 'router-web',
browser: id(301),
router: id(302),
acts: [
[500, (engine, browser) => engine.sendIcmpEcho(browser, '20.0.0.2', { sequenceNumber: 1 })],
[500, ping],
[3500, open('10.0.0.1')],
[7000, open('10.0.0.1/log')],
],
@@ -78,12 +118,21 @@ const withoutInitialNumbers = (text: string) =>
.replace(/(seq|ack)=(\d+)/g, (_, name: string, value: string) => `${name}=${Number(value) % 1_000_000}`)
.replace(/(SEQ|ACK): \d+\.(\d{3}\.\d{3})/g, '$1: n.$2');
/** Java's wait for a SYN's answer (TCPSocket: three times 2.75 s, its timeout here). */
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
/**
* Where Java errs: its run corrected, as Java means it. routerLog: the
* German log page closes its title with </titel> (sw_firewallweblog_msg1);
* the engine writes </title>.
* the engine writes </title>. konfigBlock: the browser's SYN, dropped by the
* firewall, goes once in Java, not three times (webJava.test.ts, noServer);
* the page "server not reachable" comes two attempts later in the engine.
*/
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
konfigBlock: (java) => ({
...java,
events: java.events.map(([time, who, text]) => (who === 'browser' && text.startsWith('page 0') ? [time + 2 * JAVA_TIMEOUT_MILLISECONDS, who, text] : [time, who, text])),
}),
routerLog: (java) => {
const fixed = (text: string | null) => text?.replace('</titel>', '</title>') ?? null;
const rows = (byCard: Record<string, (string | null)[][]> | undefined) =>
@@ -103,6 +152,11 @@ const PAGE_PLACEHOLDERS: Record<string, (text: string) => string> = {
ripRoutes: (text) => text.replace(/<td>-?\d+<\/td>(<td>[\d.]+<\/td><td><a )/g, '<td>validity</td>$1'),
};
/** Where the frames are compared only up to a time (Java's). konfigBlock: the engine's second SYN, which Java does not send. */
const UNTIL: Record<string, (java: JavaRun) => number> = {
konfigBlock: (java) => java.events.find(([, who, text]) => who === 'browser' && text.startsWith('page 0'))![0] - 250,
};
type Lines = Record<Program, [number, string][]>;
function javaLines(java: JavaRun): Lines {
@@ -162,6 +216,7 @@ describe('the pages of routers and home routers as Java FILIUS serves them', ()
cell: (text) => placeholders(withoutInitialNumbers(text)),
tables: java,
from: firstCall,
until: UNTIL[name]?.(measured),
starts: { java: 0, engine: 0 },
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5
View File
@@ -137,6 +137,11 @@ describe('the web server', () => {
expect(ask(engine, 'HALLO', 'GET', 'GET /')).toEqual(['HTTP/1.1 501 Not Implemented\n', 'HTTP/1.1 400 Bad Request\n', 'HTTP/1.1 200 OK\nContent-type: text/html\n\nStart']);
});
it('answers only GET and POST as Java writes them: a method in small letters is read, then answered 501 (WebServerMitarbeiter)', () => {
const engine = startedEngine(network(files({ '/webserver/index.html': 'Start' })));
expect(ask(engine, 'get /', 'Post /\n\nx=1')).toEqual(['HTTP/1.1 501 Not Implemented\n', 'HTTP/1.1 501 Not Implemented\n']);
});
it('serves nothing outside its document root (Java serves any file of the host), and no folder', () => {
const engine = startedEngine(network(files({ '/geheim.txt': 'geheim', '/webserver/ordner/a.txt': 'a' })));
expect(ask(engine, 'GET /../geheim.txt HTTP/1.1', 'GET /ordner/../../geheim.txt HTTP/1.1', 'GET /ordner HTTP/1.1', 'GET /ordner/./a.txt?x HTTP/1.1')).toEqual([
+9
View File
@@ -8,6 +8,10 @@
// echo or web server is there either way, switched on or off (it can be
// switched at run time). The programs read the nodes' files (a web server its
// pages).
//
// Back the other way: what the simulation changes that Java keeps in the
// project (its objects are the project's), so a project saved after it holds
// it. That is a router's or home router's firewall, set on its page konfig.
import { portSnapshot, topologySnapshot, type DnsServerConfiguration, type EchoServerSnapshot, type Id, type WebServerSnapshot, type TopologySnapshot } from '@mdg-filius/core';
import type { ImportedProject } from './importProject.js';
@@ -51,3 +55,8 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot {
fileSystemsByNodeId: project.virtualFileSystems,
});
}
/** The project with what the simulation changed that Java keeps: the firewalls, as the run's snapshot holds them. */
export function projectWithSimulationChanges(project: ImportedProject, snapshot: TopologySnapshot): ImportedProject {
return { ...project, firewallConfigurationsByNodeId: new Map(snapshot.firewallConfigurationsByNodeId) };
}
+28
View File
@@ -12,6 +12,7 @@ import {
defaultDisplayName,
defaultSwitchConfiguration,
firewallConfiguration,
firewallRule,
FIREWALL_PROTOCOL,
interfaceKey,
deepEqual,
@@ -28,8 +29,11 @@ import {
import {
decodeJavaBeans,
importProject,
openProjectArchive,
projectSnapshot,
projectWithSimulationChanges,
readFlsArchive,
saveProjectArchive,
readTopology,
type FlsApplication,
type JavaEmail,
@@ -773,6 +777,30 @@ describe('importProject rules', () => {
expect(snapshot.fileSystemsByNodeId).toBe(project.virtualFileSystems);
});
it("keeps a router's firewall set on its page konfig in the project saved after the run (projectWithSimulationChanges)", () => {
const project = openProjectArchive(new Uint8Array(readFileSync(join(corpusRoot, 'java-examples/internetwork_komplett.fls'))), counter());
const node = (name: string) => project.graph.nodes.find((each) => each.displayName === name)!.id;
const router = project.graph.nodes.find((each) => each.kind === 'router' && project.deviceConfigurations.get(each.id)?.ipAddress === '141.99.1.1')!.id;
const notebook = node('Notebook 1.10');
const engine = new NetworkRuntimeEngine(14);
engine.handle({ type: 'start', snapshot: { ...projectSnapshot(project), webBrowserNodeIds: new Set([notebook]) }, seed: 14, initialTimeMilliseconds: 0 });
const form = 'srcIP=&srcMask=&destIP=&destMask=&protocol=UDP&port=53&action=accept';
// Added first: once switched on, the firewall drops the browser too (by default).
engine.webBrowser(notebook)!.open('141.99.1.1/konfig', `onlySYN=1&defaultPolicy=drop&${form}&addrule=hinzuf%C3%BCgen`);
engine.handle({ type: 'advance', toMilliseconds: 5_000 });
engine.webBrowser(notebook)!.open('141.99.1.1/konfig', `firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=drop&${form}`);
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
const expected = firewallConfiguration({ isActive: true, rules: [firewallRule({ port: 53, protocol: 'udp', action: 'accept' })] });
expect(engine.firewallConfiguration(router)).toEqual(expected);
// The project itself is as it was opened, until the run's changes go back into it.
expect(project.firewallConfigurationsByNodeId.get(router)).toEqual(firewallConfiguration());
const saved = saveProjectArchive(projectWithSimulationChanges(project, engine.state.topologySnapshot)).archive;
const reopened = openProjectArchive(saved, counter());
const reopenedRouter = reopened.graph.nodes.find((each) => each.kind === 'router' && reopened.deviceConfigurations.get(each.id)?.ipAddress === '141.99.1.1')!.id;
expect(reopened.firewallConfigurationsByNodeId.get(reopenedRouter)).toEqual(expected);
});
it("keeps a node's IP forwarding where it differs from its kind's: a host that forwards, a router that does not", () => {
const system = (ipForwardingEnabled: boolean) => ({ manualRoutes: [], ipForwardingEnabled });
const project = importProject(
+2
View File
@@ -79,3 +79,5 @@ packages/core/test/programLines.ts::tcpsocket
# Java's German firewall log page closes its title with the typo </titel>; quoted where the core names the departure.
packages/core/src/runtime/apps/routerPages.ts::titel
packages/core/test/routerWebJava.test.ts::titel
# konfig: the path of Java's firewall settings plug-in (FirewallWebKonfig); the stories that use it are named after it, as in RouterWebRun.
packages/core/test/routerWebJava.test.ts::konfig
+38
View File
@@ -646,3 +646,41 @@ Gemessen (01.10.2026):
* Java zählt TCP-Sequenznummern und Segmente in Zeichen seiner Strings: Ein
„ä“ auf der Routen-Seite ist eine Stelle, nicht zwei Bytes.
## Firewall-Einstellungen der Router (F3.2c)
`KonfigCases` füttert Javas Seite `konfig` (`FirewallWebKonfig`) direkt mit
den Schritten aus `packages/core/test/firewall-settings.cases.txt` (eine
Anfrage ohne Daten oder die Daten eines Formulars) und schreibt je Schritt
die Seite und die Firewall danach, auf Deutsch und auf Englisch
(`firewall-settings.{de,en}.java.json`; das Gebietsschema wird vor dem
ersten Lesen des Nachrichtenbündels gesetzt). Jeder Fall beginnt mit einem
neuen Router, ohne Regeln, mit drei oder mit der einen Regel, die ein Fall
anlegt (die Seite, die Java zeigen würde, hätte es sie angenommen).
`RouterWebRun` spielt dazu auf `router-web` `konfigIcmp` und `konfigBlock`
über das Netz.
Gemessen (01.10.2026):
* Javas Browserfenster (Swing, `FormSubmitEvent.getData`) schickt jedes Feld
des Formulars in seiner Reihenfolge, UTF-8-kodiert, angehakte Kästchen und
die gewählte Option, dazu nur den gedrückten Knopf.
* Java prüft die Felder beim Lesen: die Quell-IP gegen die Maske, die noch
nicht gelesen ist (also leer), die Maske gegen die IP, ebenso. Eine Regel
mit Quell- oder Zieladresse nimmt die Seite deshalb nie an, in keiner
Reihenfolge der Felder.
* Ein Fehler bleibt im Plug-in stehen und erscheint auf jeder späteren Seite,
auch ohne Daten. Mit zwei Knöpfen zugleich (Speichern und Hinzufügen)
speichert Java und schreibt die Fehler als nackte Liste.
* Java dekodiert die Daten, bevor es sie an `&` und `=` zerlegt: Ein
getipptes `&` zerreißt das Formular. Eine kaputte Kodierung (`%zz`, `%` am
Ende) wirft im Dekodierer, Regel 0 nach unten wirft
(`ArrayIndexOutOfBoundsException`), ein `$` im wieder gefüllten Formular
lässt `replaceAll` scheitern (die Seite ist `null`, also 500).
* Die Maske prüft `isValidSubnetmask`: vier Zahlen (eine leere am Ende fällt
bei Javas `split` weg), `parseInt` mit Vorzeichen, die Bits Einsen, dann
Nullen, mindestens eine Eins (0.0.0.0 ist keine Maske).
* Über das Netz: „ICMP filtern“ aus, und der nächste Ping kommt durch; eine
Regel „TCP 80 verwerfen“ über die erlaubende geschoben, und das SYN des
Browsers bleibt an der Firewall hängen. Javas Router-Server lauschte auch
hier in einem Lauf nie (`run.sh` wiederholt).
+12 -1
View File
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build"
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java"
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java"
# FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing.
@@ -224,6 +224,7 @@ done
# sometimes never listens (a race in setAktiv): a run without a page is
# repeated.
for run in router-web:routerIndex:6000 router-web:routerRoutes:6000 router-web:routerLog:12000 router-web:routerKonfig:6000 \
router-web:konfigIcmp:12000 router-web:konfigBlock:30000 \
nat-home:gatewayIndex:9000 nat-home:gatewayFromWan:15000 rip-chain:ripRoutes:46000; do
network="${run%%:*}"; rest="${run#*:}"; story="${rest%%:*}"
output="$root/packages/core/test/scenarios/routerweb-java-$story.java.json"
@@ -236,6 +237,16 @@ for run in router-web:routerIndex:6000 router-web:routerRoutes:6000 router-web:r
test -s "$output"
done
# The firewall settings page (konfig) fed with what a browser posts, in both
# languages (KonfigCases; packages/core/test/firewallSettings.test.ts).
for language in de en; do
output="$root/packages/core/test/firewall-settings.$language.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" KonfigCases "$language" \
"$root/packages/core/test/firewall-settings.cases.txt" "$output" 2>&1 | grep -v 'logback\|^$' || true
test -s "$output"
done
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts).
for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
+105
View File
@@ -0,0 +1,105 @@
// Feeds FILIUS' firewall settings page (FirewallWebKonfig, the router's web
// server's plug-in konfig) with what a browser posts, step after step, and
// writes each page and the firewall after it: the reference for the core's
// konfig page (packages/core/src/runtime/apps/firewallSettings.ts). Each case
// starts from a new router; its firewall holds no rules ("empty"), three
// ("three"), one only a file can bring ("portZero": port 0, ICMP), or the
// one rule a case adds ("sourceRule", "destinationRule":
// the page Java would answer, had it taken the rule). The cases file is
// ASCII, with Java-style unicode escapes (backslash, u, four hex digits) for
// anything else:
//
// case <name> <start>
// GET the page without data (Java: holeHtmlSeite(null))
// POST <data> the page with the data a form posts ("POST" alone: empty)
//
// Usage: java -cp filius.jar:lib/*:classes KonfigCases <de|en> <cases txt> <output json>
import filius.hardware.knoten.Vermittlungsrechner;
import filius.rahmenprogramm.Information;
import filius.software.firewall.Firewall;
import filius.software.firewall.FirewallRule;
import filius.software.system.VermittlungsrechnerBetriebssystem;
import filius.software.www.WebServerPlugIn;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class KonfigCases {
public static void main(String[] args) throws Exception {
// Before any class reads the message bundle (I18n.messages is read once).
Information.getInformation().setLocale(args[0].equals("en") ? new Locale("en", "GB") : Locale.GERMANY);
List<Object> cases = new ArrayList<>();
Map<String, Object> current = null;
List<Object> steps = null;
WebServerPlugIn page = null;
Firewall firewall = null;
for (String line : Files.readAllLines(Path.of(args[1]), StandardCharsets.US_ASCII)) {
if (line.isEmpty() || line.startsWith("#")) continue;
if (line.startsWith("case ")) {
String[] words = line.split(" ");
current = new LinkedHashMap<>();
steps = new ArrayList<>();
current.put("name", words[1]);
current.put("start", words[2]);
current.put("steps", steps);
cases.add(current);
VermittlungsrechnerBetriebssystem os = (VermittlungsrechnerBetriebssystem) new Vermittlungsrechner().getSystemSoftware();
firewall = os.holeFirewall();
page = os.holeWebServer().holePlugin("konfig");
if (words[2].equals("three")) {
firewall.setActivated(true);
firewall.addRule(new FirewallRule("", "", "", "", 80, FirewallRule.TCP, FirewallRule.ACCEPT));
firewall.addRule(new FirewallRule("999.999.999.999", "", "20.0.0.0", "255.0.0.0", FirewallRule.ALL_PORTS, FirewallRule.UDP, FirewallRule.DROP));
firewall.addRule(new FirewallRule("10.0.0.0", "255.0.0.0", "", "", FirewallRule.ALL_PORTS, FirewallRule.ALL_PROTOCOLS, FirewallRule.DROP));
} else if (words[2].equals("portZero")) {
firewall.addRule(new FirewallRule("", "", "", "", 0, FirewallRule.ICMP, FirewallRule.ACCEPT));
} else if (words[2].equals("sourceRule")) {
firewall.addRule(new FirewallRule("10.0.0.0", "255.255.255.0", "", "", 80, FirewallRule.TCP, FirewallRule.DROP));
} else if (words[2].equals("destinationRule")) {
firewall.addRule(new FirewallRule("", "", "20.0.0.0", "255.0.0.0", FirewallRule.ALL_PORTS, FirewallRule.UDP, FirewallRule.ACCEPT));
}
continue;
}
String post = line.equals("GET") ? null : line.equals("POST") ? "" : IpCheckCases.unescape(line.substring("POST ".length()));
Map<String, Object> step = new LinkedHashMap<>();
step.put("post", post);
try {
step.put("page", page.holeHtmlSeite(post));
} catch (RuntimeException error) {
step.put("page", "!" + error.getClass().getSimpleName());
}
step.put("firewall", state(firewall));
steps.add(step);
}
try (FileOutputStream out = new FileOutputStream(args[2])) {
out.write((FlsOracle.Json.write(cases, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
System.out.println("konfig cases (" + args[0] + "): " + cases.size());
System.exit(0);
}
static Map<String, Object> state(Firewall firewall) {
Map<String, Object> state = new LinkedHashMap<>();
state.put("isActive", firewall.isActivated());
state.put("dropIcmp", firewall.getDropICMP());
state.put("filterSynSegmentsOnly", firewall.getFilterSYNSegmentsOnly());
state.put("defaultPolicy", firewall.getDefaultPolicy() == FirewallRule.ACCEPT ? "accept" : "drop");
List<Object> rules = new ArrayList<>();
for (FirewallRule rule : firewall.getRuleset()) {
String protocol = rule.protocol == FirewallRule.TCP ? "tcp" : rule.protocol == FirewallRule.UDP ? "udp" : rule.protocol == FirewallRule.ICMP ? "icmp" : rule.protocol == FirewallRule.ALL_PROTOCOLS ? "all" : String.valueOf(rule.protocol);
rules.add(Arrays.asList(rule.srcIP, rule.srcMask, rule.destIP, rule.destMask, rule.port, protocol, rule.action == FirewallRule.ACCEPT ? "accept" : "drop"));
}
state.put("rules", rules);
return state;
}
}
+36 -6
View File
@@ -15,6 +15,11 @@
// routerLog a ping to 20.0.0.2 (dropped), the index, then
// 10.0.0.1/log
// routerKonfig 10.0.0.1/konfig
// konfigIcmp a ping to 20.0.0.2 (dropped), the settings saved
// without "drop ICMP" (the form as Java's browser window
// posts it), the ping again
// konfigBlock a rule added (TCP 80: drop), moved up above the rule
// that accepts the browser, then the index again
// nat-home (home router 42.0.0.10/192.168.0.1):
// gatewayIndex from the LAN PC 192.168.0.10: 192.168.0.1, then
// 192.168.0.1/routes (no such page on a home router)
@@ -61,6 +66,22 @@ public class RouterWebRun {
browser.holeWebseite(new URL("http", parts[0], path.isEmpty() ? "/" : path));
}
/** What the browser window posts when a button of the settings form is pressed (Swing: every field, the button). */
static void post(String typed, String data) throws Exception {
event("browser", "call holeWebseite " + typed + " " + data);
String[] parts = typed.split("/", 2);
browser.holeWebseite(new URL("http", parts[0], "/" + parts[1]), data);
}
static void ping(InternetKnotenBetriebssystem host) {
Thread ping = new Thread(() -> {
event("ping", "call sendProbe 20.0.0.2");
Object answer = host.holeICMP().sendProbe("20.0.0.2", 64, 1, 1);
event("ping", "returned " + (answer == null ? "null" : "answer"));
});
ping.start();
}
static String id(int suffix) {
return String.format("00000000-0000-0000-0000-%012d", suffix);
}
@@ -124,12 +145,7 @@ public class RouterWebRun {
open("10.0.0.1/routes");
break;
case "routerLog": {
Thread ping = new Thread(() -> {
event("ping", "call sendProbe 20.0.0.2");
Object answer = host.holeICMP().sendProbe("20.0.0.2", 64, 1, 1);
event("ping", "returned " + (answer == null ? "null" : "answer"));
});
ping.start();
ping(host);
sleepUntil(3500);
open("10.0.0.1");
sleepUntil(7000);
@@ -139,6 +155,20 @@ public class RouterWebRun {
case "routerKonfig":
open("10.0.0.1/konfig");
break;
case "konfigIcmp":
ping(host);
sleepUntil(3500);
post("10.0.0.1/konfig", "firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop");
sleepUntil(7000);
ping(host);
break;
case "konfigBlock":
post("10.0.0.1/konfig", "firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen");
sleepUntil(4000);
post("10.0.0.1/konfig", "firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&radioRule=2&moveup=rauf&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop");
sleepUntil(8000);
open("10.0.0.1");
break;
case "gatewayIndex":
open("192.168.0.1");
sleepUntil(4000);