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
+71 -6
View File
@@ -26,7 +26,6 @@ import {
stableMacAddress,
switchConfiguration,
TCP_FLAG,
TCP_ROUND_TRIP_TIME_MILLISECONDS,
tcpFlagDescription,
topologySnapshot,
traceRetentionPolicy,
@@ -899,7 +898,7 @@ describe('TCP sockets', () => {
expect(tcpFlagDescription(0)).toBe('');
});
it("closes with Java's FIN states, and TIME_WAIT releases both ends", () => {
it("closes with Java's FIN states: the other end closes by itself, TIME_WAIT lasts three round trips", () => {
const { engine, clientId, serverId } = pair(0x1761, 0x1762, '10.20.0');
const listenerId = engine.openTcpServerSocket(serverId, 9090)!;
const clientSocketId = engine.openTcpClientSocket(clientId, '10.20.0.2', 9090)!;
@@ -907,16 +906,82 @@ describe('TCP sockets', () => {
const serverSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
engine.closeTcpSocket(clientSocketId);
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('FIN_WAIT_2');
expect(engine.state.socketsById.get(serverSocketId)?.tcpState).toBe('CLOSE_WAIT');
engine.closeTcpSocket(serverSocketId);
// Java's socket thread leaves listen on the FIN and closes without the application.
const serverStates = engine.state.packetTraces
.filter((trace) => trace.nodeId === serverId && trace.detail?.startsWith('TCP ') === true && trace.detail.includes(' -> '))
.map((trace) => trace.detail);
expect(serverStates.slice(-3)).toEqual(['TCP ESTABLISHED -> CLOSE_WAIT', 'TCP CLOSE_WAIT -> LAST_ACK', 'TCP LAST_ACK -> CLOSED']);
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
engine.closeTcpSocket(serverSocketId);
expect(engine.state.socketsById.get(serverSocketId)?.tcpState).toBe('CLOSED');
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + TCP_ROUND_TRIP_TIME_MILLISECONDS });
// Java's defaultTimeout: three round trips of 2750 ms at 90 %.
const timeWait = engine.state.packetTraces.find((trace) => trace.nodeId === clientId && trace.detail?.endsWith('-> TIME_WAIT') === true)!
.timeMilliseconds;
engine.handle({ type: 'advance', toMilliseconds: timeWait + 3 * 2_750 - 1 });
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
engine.handle({ type: 'advance', toMilliseconds: timeWait + 3 * 2_750 });
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('CLOSED');
expect(engine.state.socketsById.get(listenerId)?.tcpState).toBe('LISTEN');
});
it('ends TIME_WAIT with the next segment that comes', () => {
const { engine, clientId, serverId } = pair(0x1771, 0x1772, '10.21.0');
engine.openTcpServerSocket(serverId, 9090);
const clientSocketId = engine.openTcpClientSocket(clientId, '10.21.0.2', 9090)!;
expect(engine.connectTcp(clientSocketId)).toBe(true);
engine.closeTcpSocket(clientSocketId);
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
const localPort = engine.state.socketsById.get(clientSocketId)!.localPort;
engine.sendIPv4Packet(serverId, {
identity: 77,
senderIPAddress: '10.21.0.2',
receiverIPAddress: '10.21.0.1',
timeToLive: 64,
protocol: 'tcp',
payload: {
kind: 'tcp',
segment: { sourcePort: 9090, destinationPort: localPort, sequenceNumber: 1, acknowledgementNumber: 0, flags: TCP_FLAG.acknowledgement, payload: new Uint8Array() },
},
});
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 100 });
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('CLOSED');
});
it('accepts on the socket that listened, and listens on with the next sequence number', () => {
const [firstId, firstPortId, secondId, secondPortId, serverId, serverPortId, switchId] = [
0x1781, 0x1782, 0x1783, 0x1784, 0x1785, 0x1786, 0x1787,
].map(uuid) as [string, string, string, string, string, string, string];
const switchPorts = [0x1791, 0x1792, 0x1793].map(uuid) as [string, string, string];
const engine = startedEngine({
nodes: [
pc(firstId, firstPortId),
pc(secondId, secondPortId),
pc(serverId, serverPortId),
{ id: switchId, kind: 'networkSwitch', ports: switchPorts.map((id, index) => portSnapshot(id, `sw${index + 1}`)) },
],
links: [
link(firstId, firstPortId, switchId, switchPorts[0]),
link(secondId, secondPortId, switchId, switchPorts[1]),
link(serverId, serverPortId, switchId, switchPorts[2]),
],
deviceConfigurations: new Map([
[firstId, device('10.22.0.1')],
[secondId, device('10.22.0.2')],
[serverId, device('10.22.0.3')],
]),
});
// Java counts initial sequence numbers over all TCP sockets: the listener 1, the first client 2,
// the listener's successor 3 (made when the first is accepted), the second client 4.
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
expect(engine.connectTcp(engine.openTcpClientSocket(firstId, '10.22.0.3', 80)!)).toBe(true);
expect(engine.connectTcp(engine.openTcpClientSocket(secondId, '10.22.0.3', 80)!)).toBe(true);
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)!;
expect(accepted.map((id) => engine.state.tcpSessionsById.get(id)?.nextSendSequenceNumber)).toEqual([1_000_001, 3_000_001]);
});
});
describe('DHCP', () => {