test(core): RIP measured with Java FILIUS

RipRun runs rip-chain's three routers in Java FILIUS, each router's random
set by reflection to the core's ripSeed, and writes the frames, every
change of each RIP table and the calls. converge: the tables fill, a ping
at 45 s. expire: R3 falls silent at 10 s; R2 marks its network 16 hops only
at its own next beacon, R1 passes it on at once, the ping gets ICMP 3/0.
Both match: per sending card the same messages at the same times, every
table through the same states. The first 5 s are compared by the state the
tables reach, since Java's routers start one after the other.

Measured on the JDK: Java's pause divides nextFloat() by 3 in float; the
core does too now (a millisecond off in one draw of about 10,000 before).
Scenarios name their RIP routers in the new field rip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:41:56 +00:00
co-authored by Claude Opus 5.5
parent a4036f12a3
commit 37f5d77477
12 changed files with 1695 additions and 6 deletions
+31
View File
@@ -344,3 +344,34 @@ Frames, Abstände und die Zeiten, zu denen die Aufrufe zurückkehren). Die UDP-A
seit F2.4b-2: ein Senden kehrt sofort zurück wie in Java (Swifts Senden wartete, bis
alle Frames angekommen waren), und je Knoten und Protokoll sendet ein Transport-Thread
ein Segment nach dem anderen.
## RIP zur Laufzeit
`RipRun` startet das Netz von `rip-chain` (PC 10.0.1.2 – R1 – R2 – R3 – PC
10.0.3.2, jeder Router mit RIP; `ScenarioNetwork` schaltet RIP für die im
Feld `rip` genannten Router ein) und schreibt nach `rip-java-<ablauf>.java.json`
die Frames, jede Änderung jeder RIP-Tabelle (alle 2 ms abgefragt, Zeile
`netz/maske gateway schnittstelle hops; …`) und die Aufrufe. Java würfelt die
Pausen eines Routers aus einem neuen `Random`; `RipRun` setzt jedem Router
per Reflection (`VermittlungsrechnerBetriebssystem.ripbeacon`,
`RIPBeacon.rand`) den Startwert, den der Kern ihm gibt (`ripSeed`: Startwert
des Szenarios, `0x52495000` und FNV-1a der Router-ID). Dann würfeln beide
dieselben Pausen.
* `converge`: die Tabellen füllen sich, bei 45 s pingt 10.0.1.2 10.0.3.2.
* `expire`: bei 10 s schweigt R3 (`RIPTable.setNextBeacon(Long.MAX_VALUE)`),
bei 125 s der Ping noch einmal.
Gemessen (30.09.2026, dazu die Pausenformel am JDK):
* Die Pause ist `(int) (30000 * (rand.nextFloat() / 3 + 0.84))`; `nextFloat() / 3`
rechnet Java in `float`, in etwa einem von 10.000 Fällen eine Millisekunde
anders als in `double`.
* Eine gelernte Route bekommt 16 Hops erst beim nächsten Beacon ihres Routers
nach dem Verfallen (`RIPTable.check`): R3s Netz verfiel bei R2 nach rund
76,4 s, R2 setzte es bei seinem Beacon um 86,7 s auf 16; R1 hörte die 16 vom
selben Router und meldete sofort. Der Ping bekommt dann ICMP 3/0 von R1.
* Die Router starten nacheinander, rund 20 ms auseinander; wer einen
Nachbarn vor seinem ersten Beacon hört, sendet die gelernte Route gleich
mit. Senden mehrere zugleich, fehlt im Lauscher mancher eigene Frame.
+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"
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"
# 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.
@@ -152,6 +152,17 @@ for run in address:5000 referral:5000 unknown:5000 unknownInZone:5000 unknownInO
test -s "$output"
done
# RIP as Java FILIUS runs it: rip-chain's routers, each with the core's seed
# for its pauses (RipRun); frames, table changes and the ping. Real time.
for run in converge:60000 expire:135000; do
story="${run%%:*}"
output="$root/packages/core/test/scenarios/rip-java-$story.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" RipRun \
"$root/packages/core/test/scenarios/rip-chain.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true
test -s "$output"
done
# ARP as Java FILIUS runs it: sends to addresses no card has on arp-routed's
# network (ArpRun); frames and the calls, for the core's ARP comparison.
for story in pingLan pingRouted udpTwice udpBehind routedUdpBehind routedPingBesideUdp routedPingBehindPing; do
+157
View File
@@ -0,0 +1,157 @@
// Runs rip-chain's network in Java FILIUS (ScenarioNetwork) and records what
// RIP really does: every frame each card saw, every change of each router's
// RIP table, and the calls of the story.
//
// RipRun <scenario.json> <story> <milliseconds> <out.json>
//
// PC 10.0.1.2 - R1 (10.0.1.1, 10.0.12.1) - R2 (10.0.12.2, 10.0.23.2) -
// R3 (10.0.23.3, 10.0.3.1) - PC 10.0.3.2, each router with RIP. Java draws a
// router's pauses from a new Random; this tool gives each the seed the core
// gives it (ripSeed: the scenario's seed, 0x52495000 and the FNV-1a hash of
// the router's id). The stories:
// converge the tables fill; at 45 s 10.0.1.2 pings 10.0.3.2
// expire at 10 s R3 falls silent (its next beacon never comes); its
// network expires at R2, which tells R1; at 125 s the ping again
import filius.hardware.knoten.Knoten;
import filius.software.rip.RIPBeacon;
import filius.software.rip.RIPRoute;
import filius.software.rip.RIPTable;
import filius.software.system.VermittlungsrechnerBetriebssystem;
import filius.software.vermittlungsschicht.IcmpPaket;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class RipRun {
static final String PC_A = "00000000-0000-0000-0000-000000000401";
static final String R3 = "00000000-0000-0000-0000-000000000404";
static ScenarioNetwork network;
static final List<Object[]> events = new ArrayList<>();
static final List<Object[]> tables = new ArrayList<>();
static final Map<String, String> lastTables = new LinkedHashMap<>();
static synchronized void event(String what) {
events.add(new Object[] { System.currentTimeMillis() - network.start, what });
}
/** The table as one line: `network/mask gateway interface hops; …`, in its order. */
static String tableText(RIPTable table) {
StringBuilder text = new StringBuilder();
synchronized (table) {
for (RIPRoute route : table.routes) {
if (text.length() > 0) text.append("; ");
text.append(route.getNetAddress()).append('/').append(route.getNetMask()).append(' ').append(route.getGateway())
.append(' ').append(route.getInterfaceIpAddress()).append(' ').append(route.hops);
}
}
return text.toString();
}
static synchronized void pollTables() {
for (Map.Entry<String, Knoten> entry : network.nodes.entrySet()) {
if (!(entry.getValue().getSystemSoftware() instanceof VermittlungsrechnerBetriebssystem)) continue;
RIPTable table = ((VermittlungsrechnerBetriebssystem) entry.getValue().getSystemSoftware()).getRIPTable();
if (table == null) continue;
String text = tableText(table);
if (!text.equals(lastTables.get(entry.getKey()))) {
lastTables.put(entry.getKey(), text);
tables.add(new Object[] { System.currentTimeMillis() - network.start, entry.getKey(), text });
}
}
}
/** The core's ripSeed. */
static long ripSeed(long seed, String nodeId) {
int hash = 0x811c9dc5;
for (int index = 0; index < nodeId.length(); index++) {
hash = (hash ^ nodeId.charAt(index)) * 0x01000193;
}
return seed ^ 0x52495000L ^ (hash & 0xffffffffL);
}
static String icmp(IcmpPaket packet) {
return packet == null ? "null" : "ICMP " + packet.getIcmpType() + "/" + packet.getIcmpCode() + " from " + packet.getSender();
}
static void ping(String target) {
event("sendProbe " + target + " called");
IcmpPaket answer = network.system(PC_A).holeICMP().sendProbe(target, 64, 1, 1);
event("sendProbe " + target + " returned " + icmp(answer));
}
static void sleepUntil(long milliseconds) throws InterruptedException {
long rest = network.start + milliseconds - System.currentTimeMillis();
if (rest > 0) Thread.sleep(rest);
}
public static void main(String[] args) throws Exception {
network = ScenarioNetwork.read(args[0]);
String story = args[1];
long end = Long.parseLong(args[2]);
long seed = ScenarioNetwork.seed(args[0]);
network.start();
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()) continue;
randomField.set(beaconField.get(os), new Random(ripSeed(seed, entry.getKey())));
}
event("seeded");
Thread poller = new Thread(() -> {
while (true) {
pollTables();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
return;
}
}
});
poller.setDaemon(true);
poller.start();
switch (story) {
case "converge":
sleepUntil(45_000);
ping("10.0.3.2");
break;
case "expire":
sleepUntil(10_000);
((VermittlungsrechnerBetriebssystem) network.system(R3)).getRIPTable().setNextBeacon(Long.MAX_VALUE);
event("R3 silent");
sleepUntil(125_000);
ping("10.0.3.2");
break;
default:
throw new IllegalArgumentException("unknown story " + story);
}
sleepUntil(end);
pollTables();
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
out.open('{');
network.writeFrames(out);
synchronized (RipRun.class) {
out.key("events").open('[');
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']');
out.close(']');
out.key("tables").open('[');
for (Object[] each : tables) 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);
}
}
@@ -22,6 +22,7 @@ import filius.software.dhcp.DHCPServer;
import filius.software.dns.DNSServer;
import filius.software.netzzugangsschicht.EthernetFrame;
import filius.software.system.InternetKnotenBetriebssystem;
import filius.software.system.VermittlungsrechnerBetriebssystem;
import filius.software.transportschicht.Segment;
import filius.software.transportschicht.TcpSegment;
import filius.software.vermittlungsschicht.ArpPaket;
@@ -58,6 +59,14 @@ public class ScenarioNetwork {
return network;
}
/** The scenario's seed. */
@SuppressWarnings("unchecked")
static long seed(String path) throws Exception {
Map<String, Object> scenario = (Map<String, Object>) new JsonReader(
new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value();
return ((Number) scenario.get("seed")).longValue();
}
private void build(Map<String, Object> scenario) {
Map<String, Port> ports = new HashMap<>();
Map<String, NetzwerkInterface> nicsByPort = new HashMap<>();
@@ -156,6 +165,9 @@ public class ScenarioNetwork {
for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) {
system(nodes.get((String) each)).setDHCPKonfiguration(true);
}
for (Object each : list(scenario.getOrDefault("rip", new ArrayList<>()))) {
((VermittlungsrechnerBetriebssystem) system(nodes.get((String) each))).setRipEnabled(true);
}
}