feat(core): the pages of routers and home routers after Java, measured against Java

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>
This commit is contained in:
2026-10-01 17:41:52 +00:00
co-authored by Claude Opus 5.5
parent 4cd5749350
commit 7dac57cbbe
29 changed files with 20398 additions and 39 deletions
+29
View File
@@ -617,3 +617,32 @@ Gemessen (01.10.2026):
* Die Fehlerseiten macht der Browser selbst (im Messaufbau Javas Ersatztext,
weil die Vorlage fehlt); der Vergleich sieht nur den Status.
## Seiten der Router und Heimrouter (F3.2b)
`RouterWebRun` lässt den Browser eines Rechners die Seiten eines Routers
oder Heimrouters holen: auf `router-web` (`arp-routed` mit Firewall auf dem
Router: ICMP verwerfen, TCP-Port 80 aus 10.0.0.0/24 erlauben) Index,
`routes`, `log` (nach einem verworfenen Ping und zwei Verbindungen) und
`konfig`; auf `nat-home` den Index des Heimrouters von innen und von außen
und `routes` (gibt es dort nicht: 404); auf `rip-chain` nach 40 s die
RIP-Tabelle (die Router wie bei `RipRun` gesetzt). `ScenarioNetwork` baut
dafür jetzt auch Firewalls nach dem Szenario-Feld `firewalls`.
Gemessen (01.10.2026):
* Router und Heimrouter schalten ihren Webserver beim Start ein
(`setAktiv(true)`, Fensterzeile „Annahme … gestartet“). Ob er lauscht, ist
ein Wettlauf: `setAktiv` weckt den Thread nur, wenn er schon wartet. In
einem von drei RIP-Läufen blieb das SYN an den Router unbeantwortet; in den
NAT-Aufnahmen hat er nie gelauscht (er nahm keine Startnummer).
* Index-Seiten aus Javas Vorlagen, zeilenweise gelesen und mit `\n`
wieder zusammengesetzt (auch die Windows-Zeilenenden der deutschen).
* `log`: ein Eintrag je Regeltreffer bei TCP und UDP und je verworfenem
ICMP-Paket; die deutsche Seite schließt den Titel mit `</titel>`.
* `routes` ohne RIP: Javas Weiterleitungstabelle (eigene Adressen, Netze,
localhost, manuelle Routen, Standardgateway), grün, was direkt ist, blau das
Standardgateway; mit RIP: Hops, Gültigkeit in ganzen Sekunden, nächster Hop
und die erste Adresse des meldenden Routers.
* Java zählt TCP-Sequenznummern und Segmente in Zeichen seiner Strings: Ein
„ä“ auf der Routen-Seite ist eine Stelle, nicht zwei Bytes.
+18 -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"
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"
# 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.
@@ -219,6 +219,23 @@ for run in page:8000 byName:12000 notFound:6000 vhost:14000 noServer:14000 unres
test -s "$output"
done
# The pages of routers and home routers as Java FILIUS serves them
# (RouterWebRun): router-web, nat-home and rip-chain. Java's router server
# 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 \
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"
for attempt in 1 2 3; do
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" RouterWebRun \
"$root/packages/core/test/scenarios/$network.scenario.json" "$story" "${rest##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true
grep -q '"page 200' "$output" && break
done
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 \
+172
View File
@@ -0,0 +1,172 @@
// Runs a story with the web pages of FILIUS' routers and home routers: each
// starts a web server on port 80 with its own index page and the plug-ins
// log (the firewall's log), konfig (the firewall's settings) and, on a
// router, routes (VermittlungWeb: forwarding or RIP table). A host's web
// browser asks for them; recorded are every frame each card saw, the
// router's web server's window lines and everything the browser told its
// window.
//
// RouterWebRun <scenario.json> <story> <milliseconds> <out.json>
//
// router-web (router 10.0.0.1/20.0.0.1 with a firewall: drops ICMP,
// accepts TCP to port 80 from 10.0.0.0/24; browser on 10.0.0.2):
// routerIndex 10.0.0.1
// routerRoutes 10.0.0.1/routes (the forwarding table)
// routerLog a ping to 20.0.0.2 (dropped), the index, then
// 10.0.0.1/log
// routerKonfig 10.0.0.1/konfig
// 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)
// gatewayFromWan from the WAN host 42.0.0.20: 42.0.0.10
// rip-chain (three RIP routers, each seeded as RipRun seeds it):
// ripRoutes from 10.0.1.2, after 40 s: 10.0.1.1/routes
import filius.hardware.knoten.Knoten;
import filius.rahmenprogramm.EingabenUeberpruefung;
import filius.software.rip.RIPBeacon;
import filius.software.system.InternetKnotenBetriebssystem;
import filius.software.system.VermittlungsrechnerBetriebssystem;
import filius.software.www.HTTPNachricht;
import filius.software.www.WebBrowser;
import filius.software.www.WebServer;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class RouterWebRun {
static ScenarioNetwork network;
static WebBrowser browser;
static final List<Object[]> events = new ArrayList<>();
static synchronized void event(String who, String what) {
events.add(new Object[] { System.currentTimeMillis() - network.start, who, what });
}
static void sleepUntil(long milliseconds) throws InterruptedException {
long rest = network.start + milliseconds - System.currentTimeMillis();
if (rest > 0) Thread.sleep(rest);
}
static void open(String typed) throws Exception {
event("browser", "call holeWebseite " + typed);
String[] parts = typed.split("/");
String path = "";
for (int i = 1; i < parts.length; i++) path = path + "/" + parts[i];
if (!EingabenUeberpruefung.isGueltig(parts[0], EingabenUeberpruefung.musterIpAdresse)) throw new IllegalArgumentException(typed);
browser.holeWebseite(new URL("http", parts[0], path.isEmpty() ? "/" : path));
}
static String id(int suffix) {
return String.format("00000000-0000-0000-0000-%012d", suffix);
}
public static void main(String[] args) throws Exception {
network = ScenarioNetwork.read(args[0]);
String story = args[1];
long end = Long.parseLong(args[2]);
String browserNode;
String routerNode;
switch (story) {
case "gatewayIndex":
browserNode = id(501);
routerNode = id(502);
break;
case "gatewayFromWan":
browserNode = id(504);
routerNode = id(502);
break;
case "ripRoutes":
browserNode = id(401);
routerNode = id(402);
break;
default:
browserNode = id(301);
routerNode = id(302);
}
InternetKnotenBetriebssystem host = network.system(browserNode);
host.installApp("filius.software.www.WebBrowser", false);
browser = (WebBrowser) host.holeSoftware("filius.software.www.WebBrowser");
WebServer server = (WebServer) network.system(routerNode).holeSoftware("filius.software.www.WebServer");
server.hinzuBeobachter((observable, data) -> event("server", String.valueOf(data)));
browser.hinzuBeobachter((observable, data) -> {
if (data instanceof HTTPNachricht) {
HTTPNachricht page = (HTTPNachricht) data;
event("browser", "page " + page.getStatusCode() + " " + page.getContentType() + "\n" + page.getDaten());
} else {
event("browser", data == null ? "update" : String.valueOf(data));
}
});
network.start();
if (story.equals("ripRoutes")) {
long seed = ScenarioNetwork.seed(args[0]);
Field beaconField = VermittlungsrechnerBetriebssystem.class.getDeclaredField("ripbeacon");
beaconField.setAccessible(true);
Field randomField = RIPBeacon.class.getDeclaredField("rand");
randomField.setAccessible(true);
for (Map.Entry<String, Knoten> entry : network.nodes.entrySet()) {
if (!(entry.getValue().getSystemSoftware() instanceof VermittlungsrechnerBetriebssystem)) continue;
VermittlungsrechnerBetriebssystem os = (VermittlungsrechnerBetriebssystem) entry.getValue().getSystemSoftware();
if (os.isRipEnabled()) randomField.set(beaconField.get(os), new Random(RipRun.ripSeed(seed, entry.getKey())));
}
}
sleepUntil(500);
switch (story) {
case "routerIndex":
open("10.0.0.1");
break;
case "routerRoutes":
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();
sleepUntil(3500);
open("10.0.0.1");
sleepUntil(7000);
open("10.0.0.1/log");
break;
}
case "routerKonfig":
open("10.0.0.1/konfig");
break;
case "gatewayIndex":
open("192.168.0.1");
sleepUntil(4000);
open("192.168.0.1/routes");
break;
case "gatewayFromWan":
open("42.0.0.10");
break;
case "ripRoutes":
sleepUntil(40000);
open("10.0.1.1/routes");
break;
default:
throw new IllegalArgumentException("unknown story " + story);
}
sleepUntil(end);
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
out.open('{');
network.writeFrames(out);
synchronized (RouterWebRun.class) {
out.key("events").open('[');
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).close(']');
out.close(']');
}
out.close('}');
network.stop();
ScenarioNetwork.write(args[3], out);
System.exit(0);
}
}
@@ -24,6 +24,8 @@ import filius.hardware.knoten.Vermittlungsrechner;
import filius.rahmenprogramm.nachrichten.Lauscher;
import filius.software.dhcp.DHCPServer;
import filius.software.dns.DNSServer;
import filius.software.firewall.Firewall;
import filius.software.firewall.FirewallRule;
import filius.software.netzzugangsschicht.EthernetFrame;
import filius.software.system.Betriebssystem;
import filius.software.system.GatewayFirmware;
@@ -233,6 +235,29 @@ public class ScenarioNetwork {
for (Object each : list(scenario.getOrDefault("rip", new ArrayList<>()))) {
((VermittlungsrechnerBetriebssystem) system(nodes.get((String) each))).setRipEnabled(true);
}
// Firewalls: the core's defaults (inactive, drop, ICMP through, SYN only, UDP filtered) and its rules.
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("firewalls", new HashMap<>())).entrySet()) {
Map<String, Object> config = map(entry.getValue());
InternetKnotenBetriebssystem os = system(nodes.get(entry.getKey()));
Firewall firewall = os instanceof VermittlungsrechnerBetriebssystem ? ((VermittlungsrechnerBetriebssystem) os).holeFirewall()
: os instanceof GatewayFirmware ? ((GatewayFirmware) os).holeFirewall()
: (Firewall) os.holeSoftware("filius.software.firewall.Firewall");
firewall.setActivated(Boolean.TRUE.equals(config.get("active")));
firewall.setDefaultPolicy("accept".equals(config.get("defaultPolicy")) ? FirewallRule.ACCEPT : FirewallRule.DROP);
firewall.setDropICMP(Boolean.TRUE.equals(config.get("dropIcmp")));
firewall.setFilterSYNSegmentsOnly(!Boolean.FALSE.equals(config.get("synOnly")));
firewall.setFilterUdp(!Boolean.FALSE.equals(config.get("filterUdp")));
for (Object each : list(config.getOrDefault("rules", new ArrayList<>()))) {
Map<String, Object> rule = map(each);
String protocol = (String) rule.getOrDefault("protocol", "tcp");
short number = protocol.equals("all") ? FirewallRule.ALL_PROTOCOLS
: protocol.equals("icmp") ? FirewallRule.ICMP : protocol.equals("udp") ? FirewallRule.UDP : FirewallRule.TCP;
firewall.addRule(new FirewallRule((String) rule.getOrDefault("source", ""), (String) rule.getOrDefault("sourceMask", ""),
(String) rule.getOrDefault("destination", ""), (String) rule.getOrDefault("destinationMask", ""),
rule.containsKey("port") ? ((Number) rule.get("port")).intValue() : FirewallRule.ALL_PORTS, number,
"accept".equals(rule.get("action")) ? FirewallRule.ACCEPT : FirewallRule.DROP));
}
}
}