feat(core): modems run — the pair in the project, and a modem of another project hands its frames on (K0–K2)

Measured first: tools/java-oracle's ModemRun joins two networks by a pair
of Java modems over TCP on localhost. The modem has no card and adds no
delay of its own — a frame takes the cables on both sides, nothing more.

The core pairs the two enabled modems on a port (Swift's inProject, with
its conditions and cycle suppression) on Java's timing: no 20 ms. The
modem example of FILIUS now pings through instead of failing the core.
A modem of another project (relay) gives its frames to whoever runs the
engine and puts on its cable what comes back, with this engine's frame
and packet numbers; a frame dies after MAXIMUM_RELAY_HOPS trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:12:27 +00:00
co-authored by Claude Opus 5.5
parent 6f583616f6
commit c85b0e45da
19 changed files with 1743 additions and 44 deletions
+29
View File
@@ -345,6 +345,35 @@ seit F2.4b-2: ein Senden kehrt sofort zurück wie in Java (Swifts Senden wartete
alle Frames angekommen waren), und je Knoten und Protokoll sendet ein Transport-Thread
ein Segment nach dem anderen.
## Modems zur Laufzeit
`ModemRun` verbindet auf dem Netz von `modem-lan` zwei Netze über ein
Modem-Paar: ein Server und ein Client auf `localhost`, Javas
`ModemFirmware` über einen echten TCP-Socket. `ScenarioNetwork` baut dazu
`Modem`s und stellt sie nach `modems` ein (Modus, Port).
`connectModems()` drückt den Knopf des Einstellungsfelds: erst lauschen die
Server, dann verbinden sich die Clients. 10.0.0.1 und 10.0.0.3 hängen an
einem Switch mit dem Server-Modem, 10.0.0.2 direkt am Client-Modem. Das
Werkzeug schreibt nach `modem-java-<ablauf>.java.json`. Abläufe: `ping`
(die Modems verbunden, zwei Pings von 10.0.0.1 an 10.0.0.2) und
`unconnected` (nicht verbunden, ein Ping).
Gemessen (11.10.2026):
* Das Modem hat **keine Karte**: Es erscheint in keinem Mitschnitt.
* Das Paar fügt **keine eigene Verzögerung** hinzu. Die ARP-Anfrage
braucht zu 10.0.0.3 zwei Kabel (124 ms), zu 10.0.0.2 drei (181 ms). Die
TCP-Strecke zwischen den Modems ist dagegen nicht messbar. Swifts 20 ms
je Richtung übernimmt der Kern deshalb nicht.
* Ohne Verbindung fragt ARP zweimal und bekommt keine Antwort, der Ping
endet nach 5,5 s mit „Host unreachable“ vom eigenen Rechner. Auf der
anderen Seite kommt nichts an.
Der Kern spielt beide Abläufe wie Java (`packages/core/test/modemJava.test.ts`).
Er paart die beiden Modems an einem Port, und das Modem ohne Partner
verwirft. Dass die Verzögerung genau null ist, prüft ein eigener Test: Die
Toleranz des Vergleichs (120 ms) sähe 20 ms nicht.
## RIP zur Laufzeit
`RipRun` startet das Netz von `rip-chain` (PC 10.0.1.2 – R1 – R2 – R3 – PC
+13 -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" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" "$here/src/DocuRun.java" "$here/src/ExportRun.java" "$here/src/ReportRun.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/ModemRun.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" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" "$here/src/DocuRun.java" "$here/src/ExportRun.java" "$here/src/ReportRun.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.
@@ -135,6 +135,18 @@ for run in dhcp-lan:8000 dhcp-two-servers:8000 dhcp-conflict:25000 dhcp-no-serve
test -s "$output"
done
# Modems as Java FILIUS runs them: modem-lan's two networks joined by a pair
# of modems (server and client over TCP on localhost); frames and the calls,
# for the core's modem comparison (packages/core/test/modemJava.test.ts).
for run in ping:8000 unconnected:9000; do
story="${run%%:*}"
output="$root/packages/core/test/scenarios/modem-java-$story.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" ModemRun \
"$root/packages/core/test/scenarios/modem-lan.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true
test -s "$output"
done
# TCP as Java FILIUS runs it: stories of socket calls on tcp-lan's network
# (TcpRun); frames, calls and socket states, for the core's TCP comparison.
for run in exchange:30000 closedPort:30000 lostData:45000 lostSynAck:70000 lostFin:60000 lostLastFin:60000; do
+81
View File
@@ -0,0 +1,81 @@
// Runs a story on modem-lan's network in Java FILIUS (ScenarioNetwork): two
// networks joined by a pair of modems, a server and a client on localhost
// (ModemFirmware over a real TCP socket). Records every frame each card saw
// and when each call started and ended.
//
// ModemRun <scenario.json> <story> <milliseconds> <out.json>
//
// 10.0.0.1 and 10.0.0.3 sit on a switch with the server modem; 10.0.0.2 is
// cabled straight to the client modem. The stories:
// ping the modems connected, 10.0.0.1 pings 10.0.0.2 twice
// unconnected the modems not connected, 10.0.0.1 pings 10.0.0.2: ARP
// goes into the modem and no further
import filius.software.vermittlungsschicht.IcmpPaket;
import java.util.ArrayList;
import java.util.List;
public class ModemRun {
static final String HOST = "00000000-0000-0000-0000-000000000401";
static ScenarioNetwork network;
static final List<Object[]> events = new ArrayList<>();
interface Call {
String run() throws Exception;
}
static synchronized void event(String what) {
events.add(new Object[] { System.currentTimeMillis() - network.start, what });
}
static void call(String label, Call call) {
event(label + " called");
try {
event(label + " returned " + call.run());
} catch (Exception e) {
event(label + " failed " + e.getClass().getSimpleName());
}
}
static String icmp(IcmpPaket packet) {
return packet == null ? "null" : "ICMP " + packet.getIcmpType() + "/" + packet.getIcmpCode() + " from " + packet.getSender();
}
public static void main(String[] args) throws Exception {
network = ScenarioNetwork.read(args[0]);
String story = args[1];
long end = Long.parseLong(args[2]);
// Connected in the design, as one does with the settings panel's button before starting.
if (!story.equals("unconnected")) network.connectModems();
network.start();
Thread.sleep(500);
switch (story) {
case "ping":
call("sendProbe 10.0.0.2", () -> icmp(network.system(HOST).holeICMP().sendProbe("10.0.0.2", 64, 1, 1)));
call("sendProbe 10.0.0.2", () -> icmp(network.system(HOST).holeICMP().sendProbe("10.0.0.2", 64, 1, 2)));
break;
case "unconnected":
call("sendProbe 10.0.0.2", () -> icmp(network.system(HOST).holeICMP().sendProbe("10.0.0.2", 64, 1, 1)));
break;
default:
throw new IllegalArgumentException("unknown story " + story);
}
long rest = network.start + end - System.currentTimeMillis();
if (rest > 0) Thread.sleep(rest);
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
out.open('{');
network.writeFrames(out);
synchronized (ModemRun.class) {
out.key("events").open('[');
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).close(']');
out.close(']');
}
out.close('}');
network.stop();
ScenarioNetwork.write(args[3], out);
System.exit(0);
}
}
@@ -17,6 +17,7 @@ import filius.hardware.knoten.Gateway;
import filius.hardware.knoten.InternetKnoten;
import filius.hardware.knoten.Knoten;
import filius.hardware.knoten.LokalerKnoten;
import filius.hardware.knoten.Modem;
import filius.hardware.knoten.Notebook;
import filius.hardware.knoten.Rechner;
import filius.hardware.knoten.Switch;
@@ -30,6 +31,7 @@ import filius.software.netzzugangsschicht.EthernetFrame;
import filius.software.system.Betriebssystem;
import filius.software.system.GatewayFirmware;
import filius.software.system.InternetKnotenBetriebssystem;
import filius.software.system.ModemFirmware;
import filius.software.system.SwitchFirmware;
import filius.software.system.VermittlungsrechnerBetriebssystem;
import filius.software.transportschicht.Segment;
@@ -58,6 +60,8 @@ public class ScenarioNetwork {
/** Each card's MAC address, under its node's id (the first card) or `id#index` (the others). */
final Map<String, String> macByNode = new LinkedHashMap<>();
final List<String> servers = new ArrayList<>();
/** The modems of the scenario, servers and clients. */
final List<String> modems = new ArrayList<>();
/** Loaded from a .fls file (fromFile): started as the action mode starts it, in the file's order. */
boolean fromFile;
/** fromFile: each card's MAC address and its node's name with the card's index, `name/index`. */
@@ -136,6 +140,10 @@ public class ScenarioNetwork {
// Its first card is the WAN port, its second the LAN port.
knoten = new Gateway();
break;
case "remoteLink":
// Java's modem: one port, no card; it joins its partner over TCP (modems below).
knoten = new Modem();
break;
default:
throw new IllegalArgumentException("node kind " + kind + " is not built by this tool");
}
@@ -235,6 +243,17 @@ public class ScenarioNetwork {
for (Object each : list(scenario.getOrDefault("rip", new ArrayList<>()))) {
((VermittlungsrechnerBetriebssystem) system(nodes.get((String) each))).setRipEnabled(true);
}
// Modems: Java pairs a server and a client over TCP on the port (the client
// connects to ipAdresse); connectModems() makes the connection, as the
// button of the settings panel does.
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("modems", new HashMap<>())).entrySet()) {
Map<String, Object> config = map(entry.getValue());
ModemFirmware firmware = (ModemFirmware) nodes.get(entry.getKey()).getSystemSoftware();
firmware.setMode("server".equals(config.get("mode")) ? ModemFirmware.SERVER : ModemFirmware.CLIENT);
firmware.setPort(((Number) config.get("port")).intValue());
firmware.setIpAdresse("localhost");
modems.add(entry.getKey());
}
// 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());
@@ -285,6 +304,20 @@ public class ScenarioNetwork {
}
}
/** Every server modem listens, then every client connects (JModemKonfiguration's button). */
void connectModems() throws InterruptedException {
for (String id : modems) {
ModemFirmware firmware = (ModemFirmware) nodes.get(id).getSystemSoftware();
if (firmware.getMode() == ModemFirmware.SERVER) firmware.starteServer();
}
Thread.sleep(300);
for (String id : modems) {
ModemFirmware firmware = (ModemFirmware) nodes.get(id).getSystemSoftware();
if (firmware.getMode() == ModemFirmware.CLIENT) firmware.starteClient();
}
Thread.sleep(300);
}
InternetKnotenBetriebssystem system(String nodeId) {
return (InternetKnotenBetriebssystem) nodes.get(nodeId).getSystemSoftware();
}