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:
2026-09-30 10:03:16 +00:00
co-authored by Claude Opus 5.5
parent e80be3bbdf
commit 12849b1535
14 changed files with 20973 additions and 22 deletions
+6 -2
View File
@@ -21,8 +21,12 @@ npm test # vergleicht mit den geschriebenen *.swift.json
`sat`, `speed`; UDP (seit F2): `bindUdp` (Knoten, optional Port, lokale
Adresse, Gegenstelle; Ergebnis die Socket-ID), `sendUdp` (Socket, Text,
optional Ziel und Port), `receiveUdp` (Socket, optional Wartezeit),
`closeSocket`. Das Ergebnis enthält die Sockets mit der Länge ihrer
Warteschlange.
`closeSocket`; TCP: `openTcpServer`, `openTcpClient`, `connectTcp`
(Ergebnis `connected`, `invalidSocket`, `unreachable`, `timedOut`),
`sendTcp` (mit `repeatCount` für lange Nachrichten), `receiveTcp`,
`acceptedTcp`, `peerTcp`, `cleanTcp`; `closeSocket` schließt auch TCP.
Das Ergebnis enthält die Sockets mit der Länge ihrer Warteschlange, dem
TCP-Zustand, dem annehmenden Socket und den Sitzungswerten.
## Korpus
@@ -39,6 +39,8 @@ struct ScenarioStep: Decodable {
// UDP: a socket is named by the id bindUdp returned.
let port: UInt16?; let localIp: String?; let remoteIp: String?; let remotePort: UInt16?
let socket: String?; let payload: String?; let timeout: UInt64?
// TCP: the payload repeated this many times, for messages longer than a segment.
let repeatCount: Int?; let localPort: UInt16?
}
struct Scenario: Decodable {
let seed: UInt64
@@ -245,6 +247,28 @@ func run(_ scenario: Scenario) -> JSON {
"payload": String(decoding: item.datagram.payload, as: UTF8.self), "at": item.receivedAtMilliseconds,
]
} ?? NSNull())
case "openTcpServer":
results.append(engine.openTCPServerSocket(
nodeID: uuid(step.node!), localPort: step.port!, localIPAddress: step.localIp
)?.uuidString ?? NSNull())
case "openTcpClient":
results.append(engine.openTCPClientSocket(
nodeID: uuid(step.node!), remoteIPAddress: step.target!, remotePort: step.port!,
localIPAddress: step.localIp, localPort: step.localPort
)?.uuidString ?? NSNull())
case "connectTcp":
results.append(String(describing: engine.connectTCPWithResult(socketID: uuid(step.socket!))))
case "sendTcp":
let text = String(repeating: step.payload ?? "", count: step.repeatCount ?? 1)
results.append(engine.sendTCP(socketID: uuid(step.socket!), payload: Data(text.utf8)))
case "receiveTcp":
results.append(engine.receiveTCP(socketID: uuid(step.socket!)).map { String(decoding: $0, as: UTF8.self) } ?? NSNull())
case "acceptedTcp":
results.append((engine.state.tcpAcceptedSocketIDsByListenerID[uuid(step.socket!)] ?? []).map(\.uuidString))
case "peerTcp":
results.append(engine.peerTCPSocketID(socketID: uuid(step.socket!))?.uuidString ?? NSNull())
case "cleanTcp":
results.append(engine.cleanClosedTCPSocket(socketID: uuid(step.socket!)))
case "closeSocket":
engine.closeSocket(socketID: uuid(step.socket!))
results.append(engine.state.socketsByID[uuid(step.socket!)] == nil)
@@ -280,6 +304,15 @@ func run(_ scenario: Scenario) -> JSON {
"localIp": socket.localIPAddress ?? NSNull(), "localPort": socket.localPort,
"remoteIp": socket.remoteIPAddress ?? NSNull(), "remotePort": socket.remotePort.map { $0 as Any } ?? NSNull(),
"queued": state.udpReceiveQueuesBySocketID[socket.id]?.count ?? 0,
"tcpState": socket.tcpState?.rawValue ?? NSNull(),
"parent": socket.parentListenerSocketID?.uuidString ?? NSNull(),
"session": state.tcpSessionsByID[socket.id].map { session -> JSON in
[
"attempts": session.sendAttempts, "next": session.nextSendSequenceNumber,
"remote": session.remoteSequenceNumber, "pending": session.lastSentSegment != nil,
"assembly": session.receiveAssembly.count, "messages": session.receivedMessages.count,
]
} ?? NSNull(),
]
},
"firewallDecisions": state.firewallDecisions.map { decision -> JSON in