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)!;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import {
|
||||
deviceConfiguration,
|
||||
type EthernetFrame,
|
||||
type TcpSegment,
|
||||
NetworkRuntimeEngine,
|
||||
portSnapshot,
|
||||
topologySnapshot,
|
||||
@@ -62,3 +64,23 @@ export function letFramesArrive(engine: NetworkRuntimeEngine): void {
|
||||
engine.handle({ type: 'advance', toMilliseconds: Math.min(...deadlines) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first frame the node sends whose TCP segment matches is lost, as with
|
||||
* TcpRun's loseFirst: it leaves (the sender's card shows it) and never arrives.
|
||||
*/
|
||||
export function loseFirst(engine: NetworkRuntimeEngine, nodeId: Id, which: (segment: TcpSegment) => boolean): void {
|
||||
type Depart = (fromNodeId: Id, portId: Id, endpoint: unknown, frame: EthernetFrame) => void;
|
||||
const target = engine as unknown as { departEthernetFrame: Depart };
|
||||
const depart = target.departEthernetFrame.bind(engine);
|
||||
let lost = false;
|
||||
target.departEthernetFrame = (fromNodeId, portId, endpoint, frame) => {
|
||||
depart(fromNodeId, portId, endpoint, frame);
|
||||
const payload = frame.payload.kind === 'ipv4' ? frame.payload.packet.payload : undefined;
|
||||
if (lost || fromNodeId !== nodeId || payload?.kind !== 'tcp' || !which(payload.segment)) return;
|
||||
lost = true;
|
||||
engine.state.pendingEvents = engine.state.pendingEvents.filter(
|
||||
(event) => !(event.kind.kind === 'ethernetLinkDelivery' && event.kind.frame.identity === frame.identity),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000101": [
|
||||
{
|
||||
"time": 850,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 852,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 1079,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1081,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 61346,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1305,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102": [
|
||||
{
|
||||
"time": 729,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 963,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 966,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1193,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 61346,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1193,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
},
|
||||
{
|
||||
"time": 1716,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 61346,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103": [
|
||||
{
|
||||
"time": 850,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
214,
|
||||
"server oeffnen called"
|
||||
],
|
||||
[
|
||||
715,
|
||||
"client verbinden called"
|
||||
],
|
||||
[
|
||||
1216,
|
||||
"client verbinden returned"
|
||||
],
|
||||
[
|
||||
1317,
|
||||
"server oeffnen returned"
|
||||
],
|
||||
[
|
||||
1716,
|
||||
"client schliessen called"
|
||||
],
|
||||
[
|
||||
1716,
|
||||
"client schliessen returned"
|
||||
],
|
||||
[
|
||||
1716,
|
||||
"lost FIN"
|
||||
]
|
||||
],
|
||||
"states": {
|
||||
"client": [
|
||||
[
|
||||
715,
|
||||
"CLOSED"
|
||||
],
|
||||
[
|
||||
725,
|
||||
"SYN_SENT"
|
||||
],
|
||||
[
|
||||
1195,
|
||||
"ESTABLISHED"
|
||||
],
|
||||
[
|
||||
1717,
|
||||
"FIN_WAIT_1"
|
||||
]
|
||||
],
|
||||
"server": [
|
||||
[
|
||||
1317,
|
||||
"ESTABLISHED"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000101": [
|
||||
{
|
||||
"time": 856,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 858,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 1086,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1088,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1311,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
},
|
||||
{
|
||||
"time": 1834,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1834,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 2000002
|
||||
},
|
||||
{
|
||||
"time": 1890,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102": [
|
||||
{
|
||||
"time": 734,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 970,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 972,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1199,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 1200,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 1000001
|
||||
},
|
||||
{
|
||||
"time": 1722,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 54938,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 2000001,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1946,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 54938,
|
||||
"payload": null,
|
||||
"tcpFlags": "A",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 2000002
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103": [
|
||||
{
|
||||
"time": 856,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
219,
|
||||
"server oeffnen called"
|
||||
],
|
||||
[
|
||||
720,
|
||||
"client verbinden called"
|
||||
],
|
||||
[
|
||||
1221,
|
||||
"client verbinden returned"
|
||||
],
|
||||
[
|
||||
1321,
|
||||
"server oeffnen returned"
|
||||
],
|
||||
[
|
||||
1721,
|
||||
"client schliessen called"
|
||||
],
|
||||
[
|
||||
1721,
|
||||
"client schliessen returned"
|
||||
],
|
||||
[
|
||||
1890,
|
||||
"lost FIN"
|
||||
]
|
||||
],
|
||||
"states": {
|
||||
"client": [
|
||||
[
|
||||
720,
|
||||
"CLOSED"
|
||||
],
|
||||
[
|
||||
731,
|
||||
"SYN_SENT"
|
||||
],
|
||||
[
|
||||
1201,
|
||||
"ESTABLISHED"
|
||||
],
|
||||
[
|
||||
1724,
|
||||
"FIN_WAIT_1"
|
||||
],
|
||||
[
|
||||
1947,
|
||||
"FIN_WAIT_2"
|
||||
]
|
||||
],
|
||||
"server": [
|
||||
[
|
||||
1322,
|
||||
"ESTABLISHED"
|
||||
],
|
||||
[
|
||||
1835,
|
||||
"LAST_ACK"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000101": [
|
||||
{
|
||||
"time": 855,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 856,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 1083,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 52633,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 1085,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 52633,
|
||||
"payload": null,
|
||||
"tcpFlags": "SA",
|
||||
"sequence": 1000000,
|
||||
"acknowledgement": 2000001
|
||||
},
|
||||
{
|
||||
"time": 17585,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 52633,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102": [
|
||||
{
|
||||
"time": 733,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 968,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 970,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "02:00:00:00:01:11",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 52633,
|
||||
"destinationPort": 80,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 2000000,
|
||||
"acknowledgement": 0
|
||||
},
|
||||
{
|
||||
"time": 17697,
|
||||
"source": "02:00:00:00:01:11",
|
||||
"destination": "02:00:00:00:01:12",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 6,
|
||||
"sourcePort": 80,
|
||||
"destinationPort": 52633,
|
||||
"payload": null,
|
||||
"tcpFlags": "F",
|
||||
"sequence": 1000001,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103": [
|
||||
{
|
||||
"time": 855,
|
||||
"source": "02:00:00:00:01:12",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
218,
|
||||
"server oeffnen called"
|
||||
],
|
||||
[
|
||||
719,
|
||||
"client verbinden called"
|
||||
],
|
||||
[
|
||||
1085,
|
||||
"lost SYN+ACK"
|
||||
],
|
||||
[
|
||||
9025,
|
||||
"client verbinden failed TimeOutException"
|
||||
],
|
||||
[
|
||||
17632,
|
||||
"server oeffnen returned"
|
||||
]
|
||||
],
|
||||
"states": {
|
||||
"server": [
|
||||
[
|
||||
221,
|
||||
"LISTEN"
|
||||
],
|
||||
[
|
||||
1085,
|
||||
"SYN_RCVD"
|
||||
],
|
||||
[
|
||||
17585,
|
||||
"FIN_WAIT_1"
|
||||
]
|
||||
],
|
||||
"client": [
|
||||
[
|
||||
719,
|
||||
"CLOSED"
|
||||
],
|
||||
[
|
||||
731,
|
||||
"SYN_SENT"
|
||||
],
|
||||
[
|
||||
8980,
|
||||
"CLOSED"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,15 @@
|
||||
// client draws differs (Java's random, the engine's seed); it is compared as
|
||||
// "client". Times count from the client's SYN.
|
||||
//
|
||||
// The lost* stories lose one frame on its sender's card, as TcpRun's
|
||||
// loseFirst does in Java: the card shows it, no one receives it.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Id, NetworkRuntimeEngine } from '../src/index.js';
|
||||
import { TCP_FLAG, type Id, type NetworkRuntimeEngine, type TcpSegment } from '../src/index.js';
|
||||
import { loseFirst } from './fixtures.js';
|
||||
import { GAP_TOLERANCE_MILLISECONDS, gaps, javaFrames, watchedEngine, type JavaFrame, type SeenFrame } from './javaRun.js';
|
||||
import type { Scenario } from './scenario.js';
|
||||
|
||||
@@ -39,12 +42,30 @@ interface JavaRun {
|
||||
|
||||
const text = (value: string) => new TextEncoder().encode(value);
|
||||
|
||||
/** The story as the engine plays it; the nodes whose socket stands for Java's "client" and "server". */
|
||||
/**
|
||||
* The story as the engine plays it; the node whose socket stands for Java's
|
||||
* "client". Java watched the server's socket from ESTABLISHED on, or, with
|
||||
* `serverListening`, from the start (LISTEN).
|
||||
*/
|
||||
interface Story {
|
||||
readonly client: Id;
|
||||
readonly serverListening?: boolean;
|
||||
readonly play: (engine: NetworkRuntimeEngine, advanceTo: (milliseconds: number) => void) => void;
|
||||
}
|
||||
|
||||
const isSynAck = (segment: TcpSegment) => (segment.flags & TCP_FLAG.synchronize) !== 0 && (segment.flags & TCP_FLAG.acknowledgement) !== 0;
|
||||
const isFin = (segment: TcpSegment) => (segment.flags & TCP_FLAG.finish) !== 0;
|
||||
|
||||
/** Connected, then the client closes half a second later; the server closes by itself. */
|
||||
function connectAndClose(engine: NetworkRuntimeEngine, advanceTo: (milliseconds: number) => void): void {
|
||||
engine.openTcpServerSocket(SERVER, 80);
|
||||
advanceTo(500);
|
||||
const client = engine.openTcpClientSocket(CLIENT, '192.168.0.10', 80)!;
|
||||
engine.connectTcpWithResult(client);
|
||||
advanceTo(engine.state.currentTimeMilliseconds + 500);
|
||||
engine.closeTcpSocket(client);
|
||||
}
|
||||
|
||||
const STORIES: Record<string, Story> = {
|
||||
exchange: {
|
||||
client: CLIENT,
|
||||
@@ -88,6 +109,30 @@ const STORIES: Record<string, Story> = {
|
||||
engine.setGlobalPacketLossEnabled(false);
|
||||
},
|
||||
},
|
||||
lostSynAck: {
|
||||
client: CLIENT,
|
||||
serverListening: true,
|
||||
play: (engine, advanceTo) => {
|
||||
loseFirst(engine, SERVER, isSynAck);
|
||||
engine.openTcpServerSocket(SERVER, 80);
|
||||
advanceTo(500);
|
||||
engine.connectTcpWithResult(engine.openTcpClientSocket(CLIENT, '192.168.0.10', 80)!);
|
||||
},
|
||||
},
|
||||
lostFin: {
|
||||
client: CLIENT,
|
||||
play: (engine, advanceTo) => {
|
||||
loseFirst(engine, CLIENT, isFin);
|
||||
connectAndClose(engine, advanceTo);
|
||||
},
|
||||
},
|
||||
lostLastFin: {
|
||||
client: CLIENT,
|
||||
play: (engine, advanceTo) => {
|
||||
loseFirst(engine, SERVER, isFin);
|
||||
connectAndClose(engine, advanceTo);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** The states of the node's connection socket, from the engine's traces: when each was entered. */
|
||||
@@ -138,8 +183,21 @@ function normalized(frames: readonly SeenFrame[]): SeenFrame[] {
|
||||
* second and third), but its timeout branch sets closeSocket, and the loop
|
||||
* ends after the first: one SYN, and verbinden fails after one wait. The
|
||||
* engine sends the SYN three times, a wait of three round trips each.
|
||||
*
|
||||
* lostFin, lostLastFin: close waits at most five times (TCP_CLOSING_WAITS),
|
||||
* each ended by a segment or after the timeout, and then the socket is gone —
|
||||
* but its state stays FIN_WAIT_1, FIN_WAIT_2 or LAST_ACK. The engine shows
|
||||
* CLOSED after the last wait.
|
||||
*/
|
||||
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
|
||||
const JAVA_CLOSING_WAITS = 5;
|
||||
|
||||
/** The socket's states, with CLOSED after Java's close loop: from `state`, `waits` more waits of the timeout. */
|
||||
function closedAfterWaits(states: [number, string][], state: string, waits: number): [number, string][] {
|
||||
const from = states.find(([, each]) => each === state)![0];
|
||||
return [...states, [from + waits * JAVA_TIMEOUT_MILLISECONDS, 'CLOSED']];
|
||||
}
|
||||
|
||||
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
closedPort: (java) => {
|
||||
const client = OTHER_CLIENT;
|
||||
@@ -157,6 +215,30 @@ const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
},
|
||||
};
|
||||
},
|
||||
// The client's FIN is lost: its five waits pass without a segment.
|
||||
lostFin: (java) => ({ ...java, states: { ...java.states, client: closedAfterWaits(java.states.client!, 'FIN_WAIT_1', JAVA_CLOSING_WAITS) } }),
|
||||
// The server's FIN is lost: the client's first wait ended with the ACK, the server's five pass.
|
||||
lostLastFin: (java) => ({
|
||||
...java,
|
||||
states: {
|
||||
...java.states,
|
||||
client: closedAfterWaits(java.states.client!, 'FIN_WAIT_2', JAVA_CLOSING_WAITS - 1),
|
||||
server: closedAfterWaits(java.states.server!, 'LAST_ACK', JAVA_CLOSING_WAITS),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Where a story is compared only up to a time (Java's, from its start):
|
||||
* after it, Java follows its own error and the engine does not.
|
||||
*
|
||||
* lostSynAck: Java's client sends one SYN (closedPort above) and gives up a
|
||||
* timeout later; the engine's client sends it again then, the server answers
|
||||
* it with its SYN+ACK once more, and they connect (engine.test.ts). Compared
|
||||
* up to just before the second SYN.
|
||||
*/
|
||||
const JAVA_UNTIL: Record<string, (java: JavaRun) => number> = {
|
||||
lostSynAck: (java) => java.states.client!.find(([, state]) => state === 'SYN_SENT')![0] + JAVA_TIMEOUT_MILLISECONDS - GAP_TOLERANCE_MILLISECONDS,
|
||||
};
|
||||
|
||||
describe('TCP as Java FILIUS runs it', () => {
|
||||
@@ -167,6 +249,15 @@ describe('TCP as Java FILIUS runs it', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('cuts only where Java goes on with its error', () => {
|
||||
for (const [name, until] of Object.entries(JAVA_UNTIL)) {
|
||||
const java = JSON.parse(readFileSync(join(scenarioDirectory, `tcp-java-${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const cut = until(java);
|
||||
const later = [...Object.values(java.frames).flat().map((frame) => frame.time), ...Object.values(java.states).flat().map(([time]) => time)];
|
||||
expect(later.some((time) => time >= cut), name).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'tcp-lan.scenario.json'), 'utf8')) as Scenario;
|
||||
|
||||
for (const [name, story] of Object.entries(STORIES)) {
|
||||
@@ -180,8 +271,14 @@ describe('TCP as Java FILIUS runs it', () => {
|
||||
story.play(engine, advanceTo);
|
||||
advanceTo(Math.max(...java.events.map(([time]) => time), ...Object.values(java.states).flatMap((each) => each.map(([time]) => time))) + 5_000);
|
||||
|
||||
const expected = normalized(javaFrames(java.frames));
|
||||
const actual = normalized(frames());
|
||||
// Times from the SYN; with JAVA_UNTIL, only what comes before the cut.
|
||||
const javaSyn = java.frames[story.client]!.find((frame) => frame.tcpFlags === 'S')!.time;
|
||||
const engineSyn = frames().find((frame) => / TCP .* S seq=/.test(frame.text))!.time;
|
||||
const until = (JAVA_UNTIL[name]?.(measured) ?? Number.POSITIVE_INFINITY) - javaSyn;
|
||||
const before = <T extends { time: number } | [number, string]>(items: T[]): T[] =>
|
||||
items.filter((item) => (Array.isArray(item) ? item[0] : item.time) < until);
|
||||
const expected = before(normalized(javaFrames(java.frames)));
|
||||
const actual = before(normalized(frames()));
|
||||
const expectedSteps = gaps(expected);
|
||||
const actualSteps = gaps(actual);
|
||||
for (let index = 0; index < Math.max(expectedSteps.length, actualSteps.length); index += 1) {
|
||||
@@ -191,14 +288,14 @@ describe('TCP as Java FILIUS runs it', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const javaSyn = java.frames[story.client]!.find((frame) => frame.tcpFlags === 'S')!.time;
|
||||
const engineSyn = frames().find((frame) => / TCP .* S seq=/.test(frame.text))!.time;
|
||||
const shift = (states: [number, string][], syn: number): [number, string][] => states.map(([time, state]) => [time - syn, state]);
|
||||
const shift = (states: [number, string][], syn: number): [number, string][] => before(states.map(([time, state]) => [time - syn, state]));
|
||||
// From SYN_SENT: Java's first CLOSED is when the socket was made.
|
||||
const javaClient = java.states.client!.filter(([, state], index) => !(index === 0 && state === 'CLOSED'));
|
||||
expectSameStates('client', shift(engineStates(engine, story.client, false), engineSyn), shift(javaClient, javaSyn));
|
||||
if (java.states.server !== undefined) {
|
||||
expectSameStates('server', shift(engineStates(engine, SERVER, true), engineSyn), shift(java.states.server, javaSyn));
|
||||
// A listening server from its first change: Java's first LISTEN is when it began to listen.
|
||||
const javaServer = story.serverListening === true ? java.states.server.slice(1) : java.states.server;
|
||||
expectSameStates('server', shift(engineStates(engine, SERVER, story.serverListening !== true), engineSyn), shift(javaServer, javaSyn));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user