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:
@@ -44,7 +44,7 @@ import {
|
||||
type RuntimeOutput,
|
||||
type ScheduledEventKind,
|
||||
} from '../src/index.js';
|
||||
import { device, letFramesArrive, link, pc, startedEngine, uuid } from './fixtures.js';
|
||||
import { device, letFramesArrive, link, loseFirst, pc, startedEngine, uuid } from './fixtures.js';
|
||||
|
||||
const marker = (label: string): ScheduledEventKind => ({ kind: 'parityMarker', label });
|
||||
|
||||
@@ -995,6 +995,58 @@ describe('TCP sockets', () => {
|
||||
expect(engine.state.packetTraces.filter((trace) => trace.detail?.includes('TCP SYN retransmission') === true)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('answers a repeated SYN with its SYN+ACK once more: a lost SYN+ACK costs one attempt (Java would close)', () => {
|
||||
const { engine, clientId, serverId } = pair(0x17f1, 0x17f2, '10.26.0');
|
||||
loseFirst(engine, serverId, (segment) => segment.flags === (TCP_FLAG.synchronize | TCP_FLAG.acknowledgement));
|
||||
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
|
||||
const clientSocketId = engine.openTcpClientSocket(clientId, '10.26.0.2', 80)!;
|
||||
|
||||
expect(engine.connectTcpWithResult(clientSocketId)).toBe('connected');
|
||||
// The second SYN leaves a timeout after the first was handed down, its SYN+ACK comes a cable later each way.
|
||||
expect(engine.state.currentTimeMilliseconds).toBe(3 * 2_750 + 2 * 55);
|
||||
letFramesArrive(engine);
|
||||
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
||||
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('ESTABLISHED');
|
||||
expect(engine.state.packetTraces.filter((trace) => trace.detail?.startsWith('TCP SYN+ACK') === true).map((trace) => trace.detail)).toEqual([
|
||||
'TCP SYN+ACK',
|
||||
'TCP SYN+ACK retransmission attempt 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it("gives a handshake up after one timeout and closes with a FIN, sent once, then five waits", () => {
|
||||
const { engine, clientId, serverId } = pair(0x17f5, 0x17f6, '10.27.0');
|
||||
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
|
||||
// A SYN from the client's address, but no socket there: the SYN+ACK finds no one.
|
||||
engine.sendIPv4Packet(clientId, {
|
||||
identity: engine.allocatePacketIdentity(),
|
||||
senderIPAddress: '10.27.0.1',
|
||||
receiverIPAddress: '10.27.0.2',
|
||||
timeToLive: 64,
|
||||
protocol: 'tcp',
|
||||
payload: {
|
||||
kind: 'tcp',
|
||||
segment: { sourcePort: 5000, destinationPort: 80, sequenceNumber: 1, acknowledgementNumber: 0, flags: TCP_FLAG.synchronize, payload: new Uint8Array() },
|
||||
},
|
||||
});
|
||||
engine.handle({ type: 'advance', toMilliseconds: 60_000 });
|
||||
|
||||
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
||||
const states = engine.state.packetTraces
|
||||
.filter((trace) => trace.nodeId === serverId && trace.detail?.includes(' -> ') === true && !trace.detail.includes('LISTEN -> CLOSED') && !trace.detail.includes('CLOSED -> LISTEN'))
|
||||
.map((trace) => [trace.timeMilliseconds, trace.detail]);
|
||||
// The SYN arrives after ARP there and back: 165 ms.
|
||||
expect(states).toEqual([
|
||||
[165, 'TCP LISTEN -> SYN_RCVD'],
|
||||
[165 + 3 * 2_750, 'TCP SYN_RCVD -> FIN_WAIT_1'],
|
||||
[165 + 3 * 2_750 + 5 * 3 * 2_750, 'TCP FIN_WAIT_1 -> CLOSED'],
|
||||
]);
|
||||
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('CLOSED');
|
||||
expect(engine.state.packetTraces.filter((trace) => trace.nodeId === serverId && trace.layer === 'transport' && trace.detail?.startsWith('TCP ') === true && trace.operation !== 'accepted' && trace.operation !== 'dropped').map((trace) => trace.detail)).toEqual([
|
||||
'TCP SYN+ACK',
|
||||
'TCP FIN',
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops a SYN+ACK at a listening port with no connection of its own', () => {
|
||||
const { engine, clientId, serverId } = pair(0x1781, 0x1782, '10.21.0');
|
||||
const listenerId = engine.openTcpServerSocket(serverId, 7000)!;
|
||||
|
||||
Reference in New Issue
Block a user