feat(core): TCP timing and closing after Java, measured with Java FILIUS

TCP follows Java's TCPSocket where it differs from Swift: each attempt and
TIME_WAIT wait three round trips that follow the speed (8250 ms at 90 %),
counted from when the segment is handed down, before ARP; the next segment
ends TIME_WAIT; the other end closes by itself on FIN; data no one
acknowledges closes with FIN; the accepted socket has the listener's
initial sequence number, and the listener takes the next.

Not taken from Java: three SYNs, not one (Java's retry loop ends after
the first timeout), named in tcpJava.test.ts as JAVA_BUGS.

tcpJava.test.ts compares TcpRun's three stories frame by frame and state
by state; the Swift comparison only checks that the TCP scenarios differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 11:17:44 +00:00
co-authored by Claude Opus 5.5
parent cd3c6a432c
commit cec0a92f95
8 changed files with 488 additions and 129 deletions
+52 -13
View File
@@ -6,7 +6,10 @@
// without remote link, wireless and gateway NAT; with the firewall)
// "UDP sockets and Router RIP" (lines ~4472–5187, the
// UDP sockets; RIP not yet)
// "Java-compatible TCP sockets" (lines ~5187–5913)
// "Java-compatible TCP sockets" (lines ~5187–5913; the
// timing and closing after Java's TCPSocket, measured with Java
// FILIUS: 3 round trips per attempt and for TIME_WAIT, the other end
// closes by itself on FIN, failed data closes with FIN)
// "DHCP server and automatic client" (lines ~5913–6694; the
// client rebuilt after Java's DHCPClient, measured with Java FILIUS:
// one message per attempt, an ARP check that waits a round trip,
@@ -366,8 +369,6 @@ function sameIgnoringCase(lhs: string | undefined, rhs: string | undefined): boo
export const TCP_MAXIMUM_SEGMENT_SIZE = 1_460;
export const TCP_MAXIMUM_SEND_ATTEMPTS = 3;
export const TCP_ROUND_TRIP_TIME_MILLISECONDS = 2_500;
export const TCP_DEFAULT_TIMEOUT_MILLISECONDS = TCP_MAXIMUM_SEND_ATTEMPTS * TCP_ROUND_TRIP_TIME_MILLISECONDS;
export const TCP_EPHEMERAL_PORT_LOWER_BOUND = 49_152;
export const TCP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE = 65_535;
@@ -1330,7 +1331,8 @@ export class NetworkRuntimeEngine {
tcpState: 'SYN_RCVD',
parentListenerSocketId: listener.id,
});
const session = this.makeTcpSession(acceptedId, 'SYN_RCVD');
// Java accepts on the socket that listened: it has the listener's initial sequence number.
const session = this.makeTcpSession(acceptedId, 'SYN_RCVD', state.tcpSessionsById.get(listener.id)?.nextSendSequenceNumber);
session.remoteSequenceNumber = nextTcpSequenceNumber(segment);
state.tcpSessionsById.set(acceptedId, session);
getOrCreate(state.tcpAcceptedSocketIdsByListenerId, listener.id, () => []).push(acceptedId);
@@ -1370,14 +1372,28 @@ export class NetworkRuntimeEngine {
this.sendTcpAcknowledgement(socketId, segment);
return;
}
case 'SYN_RCVD':
case 'SYN_RCVD': {
if (!has(TCP_FLAG.acknowledgement) || !this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)) return;
this.updateTcpState(socketId, 'ESTABLISHED');
// Java: the socket that listened is the connection now; the server
// listens on with a new socket, and a new initial sequence number.
const listenerId = socket.parentListenerSocketId;
if (listenerId !== undefined && this.stateValue.tcpSessionsById.has(listenerId)) {
this.stateValue.tcpSessionsById.set(listenerId, this.makeTcpSession(listenerId, 'LISTEN'));
}
return;
}
case 'TIME_WAIT':
this.clearTcpPendingTransmission(socketId);
this.updateTcpState(socketId, 'CLOSED');
return;
case 'ESTABLISHED':
if (has(TCP_FLAG.finish)) {
this.sendTcpAcknowledgement(socketId, segment);
this.updateTcpState(socketId, 'CLOSE_WAIT');
// Java's socket thread leaves listen once the state is no longer
// ESTABLISHED and closes by itself, without waiting for the application.
this.closeTcpSocket(socketId);
} else if (segment.payload.length > 0 || has(TCP_FLAG.push)) {
this.receiveTcpPayload(socketId, segment);
} else if (has(TCP_FLAG.acknowledgement)) {
@@ -1453,6 +1469,9 @@ export class NetworkRuntimeEngine {
if (socket === undefined || socket.remoteIPAddress === undefined || socket.remotePort === undefined || session === undefined) {
return undefined;
}
// Java's socket starts waiting when it hands the segment down, before
// ARP has found the next hop; the timeout counts from here.
const handedDownAt = state.currentTimeMilliseconds;
let segment: TcpSegment;
if (retransmission && session.lastSentSegment !== undefined) {
segment = session.lastSentSegment;
@@ -1500,14 +1519,19 @@ export class NetworkRuntimeEngine {
detail: retransmission ? `${detail} retransmission attempt ${session.sendAttempts}` : detail,
});
const delivery = this.sendIPv4Packet(socket.nodeId, packet, networkInterface.portId);
if (state.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined) this.scheduleTcpTimeoutIfNeeded(socketId);
if (state.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined) this.scheduleTcpTimeoutIfNeeded(socketId, handedDownAt);
return delivery;
}
private scheduleTcpTimeoutIfNeeded(socketId: Id): void {
private scheduleTcpTimeoutIfNeeded(socketId: Id, fromMilliseconds: number): void {
const pending = this.stateValue.pendingEvents.some((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId);
if (pending) return;
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_DEFAULT_TIMEOUT_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
this.schedule(fromMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
}
/** Java's defaultTimeout: MAX_SENDEVERSUCHE round trips, for each attempt and for TIME_WAIT. */
private tcpTimeoutMilliseconds(): number {
return TCP_MAXIMUM_SEND_ATTEMPTS * this.runtimeSpeed.roundTripTimeMilliseconds;
}
private handleTcpTimeout(socketId: Id): void {
@@ -1523,7 +1547,14 @@ export class NetworkRuntimeEngine {
if (session === undefined || last === undefined) return;
if (session.sendAttempts >= TCP_MAXIMUM_SEND_ATTEMPTS) {
this.clearTcpPendingTransmission(socketId);
this.updateTcpState(socketId, 'CLOSED');
// Java: data that no one acknowledges (sent only while ESTABLISHED)
// ends senden with schliessen, an orderly close with FIN. A handshake
// or a FIN that fails closes at once.
if (socket.tcpState === 'ESTABLISHED') {
this.closeTcpSocket(socketId);
} else {
this.updateTcpState(socketId, 'CLOSED');
}
return;
}
const synchronize = (last.flags & TCP_FLAG.synchronize) !== 0;
@@ -1560,10 +1591,11 @@ export class NetworkRuntimeEngine {
);
}
/** Java's close loop: TIME_WAIT ends with the next segment, else after defaultTimeout. */
private enterTcpTimeWait(socketId: Id): void {
this.clearTcpPendingTransmission(socketId);
this.updateTcpState(socketId, 'TIME_WAIT');
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_ROUND_TRIP_TIME_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
this.schedule(this.stateValue.currentTimeMilliseconds + this.tcpTimeoutMilliseconds(), { kind: 'tcpTimeout', sessionId: socketId });
}
/**
@@ -1579,8 +1611,12 @@ export class NetworkRuntimeEngine {
const socket = this.stateValue.socketsById.get(socketId);
if (socket === undefined || socket.tcpState === undefined) return advancedToTimeout;
const waitingOnState = options.waitingStates?.includes(socket.tcpState) === true;
// Data waits for its acknowledgement only while connected: once the
// socket closes (Java: senden gives up and calls schliessen), it returns.
const waitingOnAck =
options.waitingForAcknowledgement === true && this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined;
options.waitingForAcknowledgement === true &&
socket.tcpState === 'ESTABLISHED' &&
this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined;
if (!waitingOnState && !waitingOnAck) return advancedToTimeout;
const deadlines = this.stateValue.pendingEvents
.filter((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId)
@@ -1637,8 +1673,11 @@ export class NetworkRuntimeEngine {
return Number(BigInt.asUintN(32, (value % 4_294_967_296n) * 1_000_000n));
}
private makeTcpSession(socketId: Id, socketState: TcpSocketState): TcpSession {
const initialSequence = this.allocateTcpInitialSequenceNumber();
private makeTcpSession(
socketId: Id,
socketState: TcpSocketState,
initialSequence: number = this.allocateTcpInitialSequenceNumber(),
): TcpSession {
return {
id: socketId,
socketId,