test(java-oracle): DhcpRun records DHCP as Java FILIUS runs it

Builds a core scenario's network in Java FILIUS, starts it as the action
mode does and writes every frame each card saw (the Lauscher) and the
addresses at the end. References for the four DHCP scenarios: one
DISCOVER/REQUEST per attempt, an ARP check that waits one RTT, DECLINE on a
conflict, link-local 169.254.1.x after the fifth failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 10:33:58 +00:00
co-authored by Claude Opus 5.5
parent 396dbacfc5
commit fd445e8889
7 changed files with 1725 additions and 1 deletions
+396
View File
@@ -0,0 +1,396 @@
// Runs a core scenario's network in Java FILIUS and records what DHCP really
// does: every frame each network card saw (FILIUS' own packet log, the
// Lauscher) and each host's address configuration at the end.
//
// DhcpRun <scenario.json> <milliseconds> <out.json>
//
// The network is built from the scenario (nodes, links, devices,
// dhcpServers, dhcpClients; steps are ignored) the way the core reads it: a
// port's MAC is its `mac`, else the core's stableGeneratedMacAddress. The
// simulation then runs as the action mode starts it (every system software's
// starten()), in real time, for the given milliseconds. Threads make the
// times vary from run to run; the order of one client's messages does not.
import filius.gui.GUIContainer;
import filius.hardware.Kabel;
import filius.hardware.NetzwerkInterface;
import filius.hardware.Port;
import filius.hardware.knoten.InternetKnoten;
import filius.hardware.knoten.Knoten;
import filius.hardware.knoten.LokalerKnoten;
import filius.hardware.knoten.Notebook;
import filius.hardware.knoten.Rechner;
import filius.hardware.knoten.Switch;
import filius.rahmenprogramm.nachrichten.Lauscher;
import filius.software.dhcp.DHCPServer;
import filius.software.netzzugangsschicht.EthernetFrame;
import filius.software.system.InternetKnotenBetriebssystem;
import filius.software.transportschicht.Segment;
import filius.software.vermittlungsschicht.ArpPaket;
import filius.software.vermittlungsschicht.IcmpPaket;
import filius.software.vermittlungsschicht.IpPaket;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class DhcpRun {
public static void main(String[] args) throws Exception {
@SuppressWarnings("unchecked")
Map<String, Object> scenario = (Map<String, Object>) new JsonReader(
new String(Files.readAllBytes(Paths.get(args[0])), StandardCharsets.UTF_8)).value();
long duration = Long.parseLong(args[1]);
Map<String, Knoten> nodes = new LinkedHashMap<>();
Map<String, Port> ports = new HashMap<>();
Map<String, String> macByNode = new LinkedHashMap<>();
for (Object each : list(scenario.get("nodes"))) {
Map<String, Object> node = map(each);
String id = (String) node.get("id");
String kind = (String) node.get("kind");
List<Object> nodePorts = list(node.get("ports"));
Knoten knoten;
switch (kind) {
case "pc":
knoten = new Rechner();
break;
case "notebook":
knoten = new Notebook();
break;
case "networkSwitch":
knoten = new Switch();
break;
default:
throw new IllegalArgumentException("node kind " + kind + " is not built by this tool");
}
knoten.setName(id);
if (knoten instanceof InternetKnoten) {
if (nodePorts.size() != 1) throw new IllegalArgumentException("a host has one port here");
Map<String, Object> port = map(nodePorts.get(0));
NetzwerkInterface nic = ((InternetKnoten) knoten).getNetzwerkInterfaces().get(0);
String mac = port.containsKey("mac") ? (String) port.get("mac") : stableMac((String) port.get("id"));
nic.setMac(mac);
ports.put((String) port.get("id"), nic.getPort());
macByNode.put(id, mac);
} else {
LinkedList<Port> switchPorts = ((LokalerKnoten) knoten).getAnschluesse();
for (int index = 0; index < nodePorts.size(); index++) {
ports.put((String) map(nodePorts.get(index)).get("id"), switchPorts.get(index));
}
}
nodes.put(id, knoten);
}
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("devices", new HashMap<>())).entrySet()) {
Map<String, Object> device = map(entry.getValue());
NetzwerkInterface nic = ((InternetKnoten) nodes.get(entry.getKey())).getNetzwerkInterfaces().get(0);
nic.setIp((String) device.get("ip"));
nic.setSubnetzMaske((String) device.get("mask"));
nic.setGateway((String) device.getOrDefault("gateway", ""));
nic.setDns((String) device.getOrDefault("dns", ""));
}
for (Object each : list(scenario.get("links"))) {
Map<String, Object> link = map(each);
Kabel cable = new Kabel();
cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) });
}
List<String> servers = new ArrayList<>();
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) {
Map<String, Object> config = map(entry.getValue());
DHCPServer server = system(nodes.get(entry.getKey())).getDHCPServer();
server.setUntergrenze((String) config.getOrDefault("lower", "0.0.0.0"));
server.setObergrenze((String) config.getOrDefault("upper", "0.0.0.0"));
server.setGatewayip((String) config.getOrDefault("gateway", "0.0.0.0"));
server.setDnsserverip((String) config.getOrDefault("dns", "0.0.0.0"));
server.setOwnSettings(Boolean.TRUE.equals(config.get("own")));
for (Object assignment : list(config.getOrDefault("staticAssignments", new ArrayList<>()))) {
server.addStaticAssignment((String) list(assignment).get(0), (String) list(assignment).get(1));
}
server.setAktiv(Boolean.TRUE.equals(config.get("active")));
servers.add(entry.getKey());
}
for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) {
system(nodes.get((String) each)).setDHCPKonfiguration(true);
}
// Switches and DHCP servers first: FILIUS' client waits for every active
// server to have started (waitUntilDhcpServerStarted, through the GUI's
// node list, which this tool has not got).
// FILIUS' window exists before a simulation starts; created lazily and
// unsynchronized, the first clients to ask for it would race.
GUIContainer.getGUIContainer();
long start = System.currentTimeMillis();
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (entry.getValue() instanceof Switch || servers.contains(entry.getKey())) entry.getValue().getSystemSoftware().starten();
}
Thread.sleep(200);
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (!(entry.getValue() instanceof Switch || servers.contains(entry.getKey()))) entry.getValue().getSystemSoftware().starten();
}
Thread.sleep(duration);
Field field = Lauscher.class.getDeclaredField("datenEinheiten");
field.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, LinkedList<Object[]>> log = (Map<String, LinkedList<Object[]>>) field.get(Lauscher.getLauscher());
Json out = new Json();
out.open('{');
out.key("frames").open('{');
for (Map.Entry<String, String> entry : macByNode.entrySet()) {
out.key(entry.getKey()).open('[');
LinkedList<Object[]> frames = log.get(entry.getValue());
if (frames != null) {
synchronized (frames) {
for (Object[] stamped : frames) frame(out, ((Long) stamped[0]) - start, (EthernetFrame) stamped[1]);
}
}
out.close(']');
}
out.close('}');
out.key("devices").open('{');
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
if (!(entry.getValue() instanceof InternetKnoten)) continue;
NetzwerkInterface nic = ((InternetKnoten) entry.getValue()).getNetzwerkInterfaces().get(0);
out.key(entry.getKey()).open('[').string(nic.getIp()).string(nic.getSubnetzMaske()).string(nic.getGateway())
.string(nic.getDns()).close(']');
}
out.close('}');
out.close('}');
for (Knoten knoten : nodes.values()) {
try {
knoten.getSystemSoftware().beenden();
} catch (Exception e) {}
}
try (Writer writer = new OutputStreamWriter(new FileOutputStream(args[2]), StandardCharsets.UTF_8)) {
writer.write(out.toString());
writer.write('\n');
}
System.exit(0);
}
private static void frame(Json out, long time, EthernetFrame frame) {
out.open('{').key("time").number(time).key("source").string(frame.getQuellMacAdresse()).key("destination")
.string(frame.getZielMacAdresse());
Object data = frame.getDaten();
if (data instanceof ArpPaket) {
ArpPaket arp = (ArpPaket) data;
out.key("arp").number(arp.getOperation()).key("senderIp").string(arp.getSenderIP()).key("targetIp")
.string(arp.getTargetIP());
} else if (data instanceof IcmpPaket) {
out.key("icmp").number(((IcmpPaket) data).getIcmpType());
} else if (data instanceof IpPaket) {
IpPaket ip = (IpPaket) data;
Segment segment = ip.getSegment();
out.key("sourceIp").string(ip.getSender()).key("destinationIp").string(ip.getEmpfaenger()).key("protocol")
.number(ip.getProtocol()).key("sourcePort").number(segment.getQuellPort()).key("destinationPort")
.number(segment.getZielPort()).key("payload").string(segment.getDaten());
}
out.close('}');
}
private static InternetKnotenBetriebssystem system(Knoten knoten) {
return (InternetKnotenBetriebssystem) knoten.getSystemSoftware();
}
/** The core's stableGeneratedMacAddress (packages/core/src/model/node.ts). */
static String stableMac(String portId) {
String compact = portId.toUpperCase().replace("-", "");
String suffix = compact.substring(compact.length() - 10);
StringBuilder mac = new StringBuilder("02");
for (int offset = 0; offset < suffix.length(); offset += 2) mac.append(':').append(suffix, offset, offset + 2);
return mac.toString();
}
@SuppressWarnings("unchecked")
private static Map<String, Object> map(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private static List<Object> list(Object value) {
return (List<Object>) value;
}
/** Just enough JSON for a scenario: objects, arrays, strings, numbers, booleans, null. */
static final class JsonReader {
private final String text;
private int position;
JsonReader(String text) {
this.text = text;
}
Object value() {
skip();
char c = text.charAt(position);
if (c == '{') {
Map<String, Object> result = new TreeMap<>();
position++;
skip();
if (text.charAt(position) == '}') {
position++;
return result;
}
for (;;) {
skip();
String key = (String) value();
skip();
expect(':');
result.put(key, value());
skip();
if (text.charAt(position++) == '}') return result;
}
}
if (c == '[') {
List<Object> result = new ArrayList<>();
position++;
skip();
if (text.charAt(position) == ']') {
position++;
return result;
}
for (;;) {
result.add(value());
skip();
if (text.charAt(position++) == ']') return result;
}
}
if (c == '"') {
StringBuilder result = new StringBuilder();
position++;
for (char each = text.charAt(position++); each != '"'; each = text.charAt(position++)) {
if (each == '\\') {
char escaped = text.charAt(position++);
switch (escaped) {
case 'n':
result.append('\n');
break;
case 't':
result.append('\t');
break;
case 'u':
result.append((char) Integer.parseInt(text.substring(position, position + 4), 16));
position += 4;
break;
default:
result.append(escaped);
}
} else {
result.append(each);
}
}
return result.toString();
}
if (text.startsWith("true", position)) {
position += 4;
return Boolean.TRUE;
}
if (text.startsWith("false", position)) {
position += 5;
return Boolean.FALSE;
}
if (text.startsWith("null", position)) {
position += 4;
return null;
}
int begin = position;
while (position < text.length() && "+-.eE0123456789".indexOf(text.charAt(position)) >= 0) position++;
return Double.parseDouble(text.substring(begin, position));
}
private void skip() {
while (position < text.length() && Character.isWhitespace(text.charAt(position))) position++;
}
private void expect(char c) {
if (text.charAt(position++) != c) throw new IllegalArgumentException("expected " + c + " at " + (position - 1));
}
}
/** A JSON writer with commas and indentation. */
static final class Json {
private final StringBuilder text = new StringBuilder();
private final LinkedList<Boolean> first = new LinkedList<>();
private boolean afterKey;
private void separate() {
if (afterKey) {
afterKey = false;
return;
}
if (first.isEmpty()) return;
if (!first.peek()) text.append(',');
first.pop();
first.push(false);
text.append('\n');
for (int i = 0; i < first.size(); i++) text.append(" ");
}
Json open(char bracket) {
separate();
text.append(bracket);
first.push(true);
return this;
}
Json close(char bracket) {
boolean empty = first.pop();
if (!empty) {
text.append('\n');
for (int i = 0; i < first.size(); i++) text.append(" ");
}
text.append(bracket);
return this;
}
Json key(String key) {
separate();
quote(key);
text.append(": ");
afterKey = true;
return this;
}
Json string(String value) {
separate();
if (value == null) text.append("null");
else quote(value);
return this;
}
Json number(long value) {
separate();
text.append(value);
return this;
}
private void quote(String value) {
text.append('"');
for (char c : value.toCharArray()) {
if (c == '"' || c == '\\') text.append('\\').append(c);
else if (c == '\n') text.append("\\n");
else if (c < 0x20) text.append(String.format("\\u%04x", (int) c));
else text.append(c);
}
text.append('"');
}
@Override
public String toString() {
return text.toString();
}
}
}