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
+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);
}
}