feat(core): TCP in the simulation core
engine.ts ports Swift's "Java-compatible TCP sockets" in one piece, since half a state machine could not be checked: server and client sockets (a free port from Java's random numbers on their own seed), the three-way handshake with its result (connected, invalid socket, unreachable, timed out), segments of 1460 bytes, messages up to the push, a timeout of 3 × 2.5 s with at most three attempts, closing through every Java state including TIME_WAIT (2.5 s), sequence numbers modulo 2^32. Three scenarios match the Swift engine trace for trace: a LAN with data over three segments, closing from both ends, a closed port and an unreachable host; two networks through a router with a firewall rule, data lost until the connection closes, and an accepted socket that does not hold the port of its closed listener; CLOSING, reached by a lost FIN. The engine delivers a FIN within the same close, so both ends close at once only with packet loss; an empty message does not arrive in Swift (it does not advance the sequence number), nor here. Swift's four TCP engine tests are ported, with tests for stopping, a foreign SYN+ACK at a listener and the flag names. 17 mutation probes, 16 caught; the other (delivered, not waited, not connected gives unreachable) cannot be reached through Swift's engine, which never sends a reset and always acknowledges correctly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,9 @@ interface Scenario {
|
||||
socket?: string;
|
||||
payload?: string;
|
||||
timeout?: number;
|
||||
// TCP: the payload repeated this many times, for messages longer than a segment.
|
||||
repeatCount?: number;
|
||||
localPort?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -281,6 +284,37 @@ function run(scenario: Scenario): Record<string, unknown> {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'openTcpServer':
|
||||
results.push(engine.openTcpServerSocket(step.node!, step.port!, step.localIp) ?? null);
|
||||
break;
|
||||
case 'openTcpClient':
|
||||
results.push(
|
||||
engine.openTcpClientSocket(step.node!, step.target!, step.port!, {
|
||||
...(step.localIp === undefined ? {} : { localIPAddress: step.localIp }),
|
||||
...(step.localPort === undefined ? {} : { localPort: step.localPort }),
|
||||
}) ?? null,
|
||||
);
|
||||
break;
|
||||
case 'connectTcp':
|
||||
results.push(engine.connectTcpWithResult(step.socket!));
|
||||
break;
|
||||
case 'sendTcp':
|
||||
results.push(engine.sendTcp(step.socket!, new TextEncoder().encode((step.payload ?? '').repeat(step.repeatCount ?? 1))));
|
||||
break;
|
||||
case 'receiveTcp': {
|
||||
const message = engine.receiveTcp(step.socket!);
|
||||
results.push(message === undefined ? null : new TextDecoder().decode(message));
|
||||
break;
|
||||
}
|
||||
case 'acceptedTcp':
|
||||
results.push(engine.state.tcpAcceptedSocketIdsByListenerId.get(step.socket!) ?? []);
|
||||
break;
|
||||
case 'peerTcp':
|
||||
results.push(engine.peerTcpSocketId(step.socket!) ?? null);
|
||||
break;
|
||||
case 'cleanTcp':
|
||||
results.push(engine.cleanClosedTcpSocket(step.socket!));
|
||||
break;
|
||||
case 'closeSocket':
|
||||
engine.closeSocket(step.socket!);
|
||||
results.push(!engine.state.socketsById.has(step.socket!));
|
||||
@@ -316,6 +350,19 @@ function run(scenario: Scenario): Record<string, unknown> {
|
||||
remoteIp: socket.remoteIPAddress ?? null,
|
||||
remotePort: socket.remotePort ?? null,
|
||||
queued: state.udpReceiveQueuesBySocketId.get(socket.id)?.length ?? 0,
|
||||
tcpState: socket.tcpState ?? null,
|
||||
parent: socket.parentListenerSocketId === undefined ? null : upper(socket.parentListenerSocketId),
|
||||
session: ((session) =>
|
||||
session === undefined
|
||||
? null
|
||||
: {
|
||||
attempts: session.sendAttempts,
|
||||
next: session.nextSendSequenceNumber,
|
||||
remote: session.remoteSequenceNumber,
|
||||
pending: session.lastSentSegment !== undefined,
|
||||
assembly: session.receiveAssembly.length,
|
||||
messages: session.receivedMessages.length,
|
||||
})(state.tcpSessionsById.get(socket.id)),
|
||||
})),
|
||||
firewallDecisions: state.firewallDecisions.map((decision) => ({
|
||||
packet: decision.packetIdentity,
|
||||
|
||||
Reference in New Issue
Block a user