feat(core): SYN+ACK and FIN go once, as in Java, measured with Java FILIUS

TcpRun loses exactly one frame (loseFirst: the card's output buffer drops
it; the sender's packet log still shows it, as with setDrop) in three new
stories: lostSynAck, lostFin, lostLastFin.

Measured: Java never repeats a SYN+ACK or a FIN. The server gives a
handshake up after one timeout (3 RTT) and closes with a FIN from SYN_RCVD
(its boundary check sometimes lets it wait twice; it means once). After its
FIN a socket waits at most five times, each wait ended by a segment or the
timeout; then it is gone. The core does the same (TCP_CLOSING_WAITS, with
TIME_WAIT in the loop) and shows CLOSED where Java leaves the state
standing (a named Java bug).

One departure: a repeated SYN in SYN_RCVD gets the SYN+ACK once more and
the wait starts anew; Java closes there, but its client never repeats the
SYN. lostSynAck is compared up to the second SYN (JAVA_UNTIL); the rest is
an engine test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 18:08:18 +00:00
co-authored by Claude Opus 5.5
parent a88fa1512c
commit 7e0348636c
11 changed files with 1056 additions and 40 deletions
+19
View File
@@ -203,6 +203,17 @@ und Mitschnitt teilen `DhcpRun` und `TcpRun` in `ScenarioNetwork`.
* `lostData`: verbunden, dann gehen alle Frames verloren
(`Verbindung.setDrop`, wie der Paketverlust des Kerns), während der Client
sendet; der Verlust endet, wenn `senden` scheitert.
* `lostSynAck`: das erste SYN+ACK des Servers geht verloren.
* `lostFin`: verbunden, der Client schließt; sein FIN geht verloren.
* `lostLastFin`: verbunden, der Client schließt; das FIN des Servers geht
verloren.
Einen einzelnen Frame verliert `loseFirst`: Per Reflection ersetzt es den
Ausgangspuffer der Karte (`Port.ausgangsPuffer`) durch einen, der den ersten
passenden Frame verwirft, statt ihn einzureihen. Das Kabel wartet am alten
Puffer und wird dort einmal geweckt. Der Lauscher des Absenders sieht den
Frame wie bei `setDrop` (er schreibt nach dem Einreihen); anders als dort
belegt der Frame das Kabel nicht.
Gemessen (30.09.2026, 90 %, RTT 2750 ms), was Swift anders macht:
@@ -220,6 +231,14 @@ Gemessen (30.09.2026, 90 %, RTT 2750 ms), was Swift anders macht:
`connectClientMode` `closeSocket`, und die Schleife über drei Versuche
endet (`verbinden` scheitert nach 3 RTT). Der Server-Socket wiederholt sein
SYN+ACK nie.
* Nach einem verlorenen SYN+ACK gibt der Server nach 3 RTT auf und schickt aus
`SYN_RCVD` ein FIN (`close`). In zwei von drei Läufen wartete er 6 RTT: Der
Vergleich `vergangen > defaultTimeout()` liegt genau auf der Grenze.
* Ein FIN geht einmal hinaus, nie wieder. Danach wartet `close` höchstens
fünfmal 3 RTT; jedes ankommende Segment beendet eine Wartezeit. Dann ist
der Socket weg, sein Zustand bleibt aber stehen (`FIN_WAIT_1`,
`FIN_WAIT_2`, `LAST_ACK`). Die Gegenseite eines verlorenen FIN bleibt
`ESTABLISHED`.
* Startnummern aus einem Zähler über alle TCP-Sockets (1000000, 2000000, …),
wie im Kern; der freie Port ist zufällig.
+1 -1
View File
@@ -122,7 +122,7 @@ 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; do
for run in exchange:30000 closedPort:30000 lostData:45000 lostSynAck:70000 lostFin:60000 lostLastFin:60000; do
story="${run%%:*}"
output="$root/packages/core/test/scenarios/tcp-java-$story.java.json"
rm -f "$output"
+116
View File
@@ -14,17 +14,31 @@
// closedPort a client connects to port 81, where nothing listens
// lostData connected, all frames are lost (Verbindung.setDrop) while
// the client sends; loss ends when senden fails
// lostSynAck the server's first SYN+ACK is lost
// lostFin connected, the client closes; its first FIN is lost
// lostLastFin connected, the client closes; the server's first FIN is lost
//
// A single frame is lost on its sender's card (loseFirst): the card's output
// buffer drops it instead of queueing it. The packet log sees it on the
// sender, as with setDrop; unlike setDrop, the cable does not carry it.
import filius.hardware.Port;
import filius.hardware.Verbindung;
import filius.hardware.knoten.InternetKnoten;
import filius.software.netzzugangsschicht.EthernetFrame;
import filius.software.transportschicht.ServerSocket;
import filius.software.transportschicht.Socket;
import filius.software.transportschicht.TCPSocket;
import filius.software.transportschicht.TcpSegment;
import filius.software.vermittlungsschicht.IpPaket;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
public class TcpRun {
static final String SERVER = "00000000-0000-0000-0000-000000000101";
@@ -85,6 +99,75 @@ public class TcpRun {
return thread;
}
/**
* The node's card drops the first frame whose TCP segment matches, instead
* of putting it into its output buffer. The cable waits on the buffer it
* found: woken there once, it takes the new one.
*/
@SuppressWarnings("unchecked")
static void loseFirst(String nodeId, String label, Predicate<TcpSegment> which) throws Exception {
Port port = ((InternetKnoten) network.system(nodeId).getKnoten()).getNetzwerkInterfaces().get(0).getPort();
Field field = Port.class.getDeclaredField("ausgangsPuffer");
field.setAccessible(true);
LinkedList<EthernetFrame> old = (LinkedList<EthernetFrame>) field.get(port);
field.set(port, new LinkedList<EthernetFrame>() {
boolean lost;
@Override
public boolean add(EthernetFrame frame) {
if (!lost && frame.getDaten() instanceof IpPaket
&& ((IpPaket) frame.getDaten()).getSegment() instanceof TcpSegment
&& which.test((TcpSegment) ((IpPaket) frame.getDaten()).getSegment())) {
lost = true;
event("lost " + label);
return true;
}
return super.add(frame);
}
});
synchronized (old) {
old.notifyAll();
}
}
/** The listener's socket that waits for a connection, as soon as there is one. */
static void watchListening(String label, ServerSocket listener) {
Thread thread = new Thread(() -> {
try {
Field field = ServerSocket.class.getDeclaredField("aktuellerSocket");
field.setAccessible(true);
while (field.get(listener) == null) Thread.sleep(1);
watch(label, (Socket) field.get(listener));
} catch (Exception e) {
event("watch failed " + e.getClass().getSimpleName());
}
});
thread.setDaemon(true);
thread.start();
}
/** Connected, then the client closes; the server's socket closes by itself. */
static void connectAndClose() throws Exception {
server(() -> {
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
Socket accepted = (Socket) call("server oeffnen", listener::oeffnen);
watch("server", accepted);
return null;
});
Thread.sleep(500);
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
watch("client", client);
call("client verbinden", () -> {
client.verbinden();
return null;
});
Thread.sleep(500);
call("client schliessen", () -> {
client.schliessen();
return null;
});
}
static String repeat(String text, int count) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) result.append(text);
@@ -95,6 +178,19 @@ public class TcpRun {
network = ScenarioNetwork.read(args[0]);
String story = args[1];
long duration = Long.parseLong(args[2]);
switch (story) {
case "lostSynAck":
loseFirst(SERVER, "SYN+ACK", segment -> segment.isSyn() && segment.isAck());
break;
case "lostFin":
loseFirst(CLIENT, "FIN", TcpSegment::isFin);
break;
case "lostLastFin":
loseFirst(SERVER, "FIN", TcpSegment::isFin);
break;
default:
break;
}
network.start();
Thread poller = new Thread(() -> {
while (true) {
@@ -182,6 +278,26 @@ public class TcpRun {
event("loss off");
break;
}
case "lostSynAck": {
server(() -> {
ServerSocket listener = new ServerSocket(network.system(SERVER), 80, IpPaket.TCP, new ArrayList<>());
watchListening("server", listener);
call("server oeffnen", listener::oeffnen);
return null;
});
Thread.sleep(500);
TCPSocket client = new TCPSocket(network.system(CLIENT), "192.168.0.10", 80);
watch("client", client);
call("client verbinden", () -> {
client.verbinden();
return null;
});
break;
}
case "lostFin":
case "lostLastFin":
connectAndClose();
break;
default:
throw new IllegalArgumentException("unknown story " + story);
}