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
+22
View File
@@ -3,6 +3,8 @@
import {
deviceConfiguration,
type EthernetFrame,
type TcpSegment,
NetworkRuntimeEngine,
portSnapshot,
topologySnapshot,
@@ -62,3 +64,23 @@ export function letFramesArrive(engine: NetworkRuntimeEngine): void {
engine.handle({ type: 'advance', toMilliseconds: Math.min(...deadlines) });
}
}
/**
* The first frame the node sends whose TCP segment matches is lost, as with
* TcpRun's loseFirst: it leaves (the sender's card shows it) and never arrives.
*/
export function loseFirst(engine: NetworkRuntimeEngine, nodeId: Id, which: (segment: TcpSegment) => boolean): void {
type Depart = (fromNodeId: Id, portId: Id, endpoint: unknown, frame: EthernetFrame) => void;
const target = engine as unknown as { departEthernetFrame: Depart };
const depart = target.departEthernetFrame.bind(engine);
let lost = false;
target.departEthernetFrame = (fromNodeId, portId, endpoint, frame) => {
depart(fromNodeId, portId, endpoint, frame);
const payload = frame.payload.kind === 'ipv4' ? frame.payload.packet.payload : undefined;
if (lost || fromNodeId !== nodeId || payload?.kind !== 'tcp' || !which(payload.segment)) return;
lost = true;
engine.state.pendingEvents = engine.state.pendingEvents.filter(
(event) => !(event.kind.kind === 'ethernetLinkDelivery' && event.kind.frame.identity === frame.identity),
);
};
}