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:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user