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:
+2
-2
@@ -13,7 +13,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) |
|
| `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) |
|
||||||
| `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft |
|
| `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft |
|
||||||
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
|
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
|
||||||
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen). Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, TCP, DHCP, DNS, RIP, Remote Link, WLAN |
|
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, Zeitgrenze 3 × 2,5 s mit höchstens drei Versuchen, Abbau über alle Java-Zustände samt `TIME_WAIT`). Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DHCP, DNS, RIP, Remote Link, WLAN |
|
||||||
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
|
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
|
||||||
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
|
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
|
||||||
| `src/runtime/random.ts` | Engine, `TopologyJavaCompatibleRandom` | übertragen; BigInt für die 48-Bit-Arithmetik |
|
| `src/runtime/random.ts` | Engine, `TopologyJavaCompatibleRandom` | übertragen; BigInt für die 48-Bit-Arithmetik |
|
||||||
@@ -27,7 +27,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `test/virtualFileSystem.test.ts`, `test/vfs.*.json` | `TopologyProjectPersistenceTests.swift`, `TopologyEditorReducerTests.swift` (drei Dateisystem-Tests, als Fälle) | übertragen und erweitert: 11 Fälle, Ergebnis aus dem Swift-Kern |
|
| `test/virtualFileSystem.test.ts`, `test/vfs.*.json` | `TopologyProjectPersistenceTests.swift`, `TopologyEditorReducerTests.swift` (drei Dateisystem-Tests, als Fälle) | übertragen und erweitert: 11 Fälle, Ergebnis aus dem Swift-Kern |
|
||||||
| `test/strings.test.ts`, `test/strings.*.json` | — | neu: Fälle und ganze Zeichenmengen, Ergebnis aus dem Swift-Kern |
|
| `test/strings.test.ts`, `test/strings.*.json` | — | neu: Fälle und ganze Zeichenmengen, Ergebnis aus dem Swift-Kern |
|
||||||
| `test/engine.test.ts` | `TopologyNetworkRuntimeEngineTests.swift` (21 von 72 Tests) | übertragen; `nextInt`-Referenzwerte an OpenJDK 21 gemessen |
|
| `test/engine.test.ts` | `TopologyNetworkRuntimeEngineTests.swift` (21 von 72 Tests) | übertragen; `nextInt`-Referenzwerte an OpenJDK 21 gemessen |
|
||||||
| `test/oracle.test.ts`, `test/scenarios/` | — | neu: 7 Szenarien (davon 2 UDP), Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`); Schritte `bindUdp`, `sendUdp`, `receiveUdp`, `closeSocket` |
|
| `test/oracle.test.ts`, `test/scenarios/` | — | neu: 10 Szenarien (davon 2 UDP, 3 TCP), Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`); Schritte für UDP und TCP |
|
||||||
| `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
|
| `test/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
|
||||||
|
|
||||||
## packages/fls
|
## packages/fls
|
||||||
|
|||||||
@@ -16,9 +16,10 @@
|
|||||||
//
|
//
|
||||||
// "UDP sockets and Router RIP" (lines ~4472–5187, the
|
// "UDP sockets and Router RIP" (lines ~4472–5187, the
|
||||||
// UDP sockets; RIP not yet)
|
// UDP sockets; RIP not yet)
|
||||||
|
// "Java-compatible TCP sockets" (lines ~5187–5913)
|
||||||
//
|
//
|
||||||
// Not ported yet — each place throws NotPortedError instead of guessing:
|
// Not ported yet — each place throws NotPortedError instead of guessing:
|
||||||
// gateway NAT, TCP delivery, RIP routing, remote links. DHCP is not in the
|
// gateway NAT, RIP routing, remote links. DHCP is not in the
|
||||||
// snapshot yet, so Swift's DHCP handling of a datagram has nothing to do.
|
// snapshot yet, so Swift's DHCP handling of a datagram has nothing to do.
|
||||||
|
|
||||||
import { compareIds, type Id } from '../ids.js';
|
import { compareIds, type Id } from '../ids.js';
|
||||||
@@ -51,6 +52,8 @@ import {
|
|||||||
type IcmpMessage,
|
type IcmpMessage,
|
||||||
type IcmpMessageKind,
|
type IcmpMessageKind,
|
||||||
type IPv4Packet,
|
type IPv4Packet,
|
||||||
|
type TcpSegment,
|
||||||
|
type TcpSocketState,
|
||||||
type UdpDatagram,
|
type UdpDatagram,
|
||||||
} from './packets.js';
|
} from './packets.js';
|
||||||
import { JavaCompatibleRandom } from './random.js';
|
import { JavaCompatibleRandom } from './random.js';
|
||||||
@@ -232,7 +235,7 @@ export type RuntimePhase = 'stopped' | 'running';
|
|||||||
|
|
||||||
export type SocketProtocol = 'udp' | 'tcp';
|
export type SocketProtocol = 'udp' | 'tcp';
|
||||||
|
|
||||||
/** Swift's TopologyRuntimeSocketRecord (UDP so far). */
|
/** Swift's TopologyRuntimeSocketRecord. */
|
||||||
export interface SocketRecord {
|
export interface SocketRecord {
|
||||||
readonly id: Id;
|
readonly id: Id;
|
||||||
readonly nodeId: Id;
|
readonly nodeId: Id;
|
||||||
@@ -242,6 +245,39 @@ export interface SocketRecord {
|
|||||||
readonly localPort: number;
|
readonly localPort: number;
|
||||||
remoteIPAddress: string | undefined;
|
remoteIPAddress: string | undefined;
|
||||||
remotePort: number | undefined;
|
remotePort: number | undefined;
|
||||||
|
/** TCP only. */
|
||||||
|
tcpState: TcpSocketState | undefined;
|
||||||
|
/** A socket a TCP listener accepted: that listener. */
|
||||||
|
readonly parentListenerSocketId: Id | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Swift's TopologyRuntimeTCPSessionRecord; sequence numbers are unsigned 32-bit. */
|
||||||
|
export interface TcpSession {
|
||||||
|
readonly id: Id;
|
||||||
|
readonly socketId: Id;
|
||||||
|
state: TcpSocketState;
|
||||||
|
sendAttempts: number;
|
||||||
|
nextSendSequenceNumber: number;
|
||||||
|
lastSentSequenceNumber: number;
|
||||||
|
remoteSequenceNumber: number;
|
||||||
|
lastAcknowledgedSequenceNumber: number | undefined;
|
||||||
|
lastSentSegment: TcpSegment | undefined;
|
||||||
|
receiveAssembly: Uint8Array;
|
||||||
|
receivedMessages: Uint8Array[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TcpConnectionResult = 'connected' | 'invalidSocket' | 'unreachable' | 'timedOut';
|
||||||
|
|
||||||
|
export const TCP_MAXIMUM_SEGMENT_SIZE = 1_460;
|
||||||
|
export const TCP_MAXIMUM_SEND_ATTEMPTS = 3;
|
||||||
|
export const TCP_ROUND_TRIP_TIME_MILLISECONDS = 2_500;
|
||||||
|
export const TCP_DEFAULT_TIMEOUT_MILLISECONDS = TCP_MAXIMUM_SEND_ATTEMPTS * TCP_ROUND_TRIP_TIME_MILLISECONDS;
|
||||||
|
export const TCP_EPHEMERAL_PORT_LOWER_BOUND = 49_152;
|
||||||
|
export const TCP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE = 65_535;
|
||||||
|
|
||||||
|
/** Addition modulo 2^32, as Swift's `&+` on UInt32. */
|
||||||
|
function uint32Add(value: number, addend: number): number {
|
||||||
|
return (value + addend) >>> 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A datagram waiting in a UDP socket's queue. */
|
/** A datagram waiting in a UDP socket's queue. */
|
||||||
@@ -281,7 +317,10 @@ export interface RuntimeState {
|
|||||||
firewallDecisions: FirewallDecision[];
|
firewallDecisions: FirewallDecision[];
|
||||||
socketsById: Map<Id, SocketRecord>;
|
socketsById: Map<Id, SocketRecord>;
|
||||||
udpReceiveQueuesBySocketId: Map<Id, UdpReceivedDatagram[]>;
|
udpReceiveQueuesBySocketId: Map<Id, UdpReceivedDatagram[]>;
|
||||||
|
tcpSessionsById: Map<Id, TcpSession>;
|
||||||
|
tcpAcceptedSocketIdsByListenerId: Map<Id, Id[]>;
|
||||||
nextSocketSequenceNumber: number;
|
nextSocketSequenceNumber: number;
|
||||||
|
nextTcpInitialSequenceValue: number;
|
||||||
nextEventSequenceNumber: number;
|
nextEventSequenceNumber: number;
|
||||||
nextFrameIdentity: number;
|
nextFrameIdentity: number;
|
||||||
nextPacketIdentity: number;
|
nextPacketIdentity: number;
|
||||||
@@ -308,7 +347,10 @@ function initialState(seed: bigint): RuntimeState {
|
|||||||
firewallDecisions: [],
|
firewallDecisions: [],
|
||||||
socketsById: new Map(),
|
socketsById: new Map(),
|
||||||
udpReceiveQueuesBySocketId: new Map(),
|
udpReceiveQueuesBySocketId: new Map(),
|
||||||
|
tcpSessionsById: new Map(),
|
||||||
|
tcpAcceptedSocketIdsByListenerId: new Map(),
|
||||||
nextSocketSequenceNumber: 1,
|
nextSocketSequenceNumber: 1,
|
||||||
|
nextTcpInitialSequenceValue: 1,
|
||||||
nextEventSequenceNumber: 0,
|
nextEventSequenceNumber: 0,
|
||||||
nextFrameIdentity: 1,
|
nextFrameIdentity: 1,
|
||||||
nextPacketIdentity: 1,
|
nextPacketIdentity: 1,
|
||||||
@@ -443,6 +485,8 @@ export class NetworkRuntimeEngine {
|
|||||||
state.icmpObservationsByNodeId.clear();
|
state.icmpObservationsByNodeId.clear();
|
||||||
state.socketsById.clear();
|
state.socketsById.clear();
|
||||||
state.udpReceiveQueuesBySocketId.clear();
|
state.udpReceiveQueuesBySocketId.clear();
|
||||||
|
state.tcpSessionsById.clear();
|
||||||
|
state.tcpAcceptedSocketIdsByListenerId.clear();
|
||||||
state.firewallDecisions = [];
|
state.firewallDecisions = [];
|
||||||
return [this.output({ type: 'stopped' })];
|
return [this.output({ type: 'stopped' })];
|
||||||
}
|
}
|
||||||
@@ -617,7 +661,8 @@ export class NetworkRuntimeEngine {
|
|||||||
case 'dhcpTimeout':
|
case 'dhcpTimeout':
|
||||||
throw new NotPortedError('DHCP client');
|
throw new NotPortedError('DHCP client');
|
||||||
case 'tcpTimeout':
|
case 'tcpTimeout':
|
||||||
throw new NotPortedError('TCP timeouts');
|
this.handleTcpTimeout(kind.sessionId);
|
||||||
|
return;
|
||||||
case 'remoteLinkDelivery':
|
case 'remoteLinkDelivery':
|
||||||
case 'lanRemoteLinkTransmission':
|
case 'lanRemoteLinkTransmission':
|
||||||
throw new NotPortedError('remote links');
|
throw new NotPortedError('remote links');
|
||||||
@@ -676,8 +721,7 @@ export class NetworkRuntimeEngine {
|
|||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const id = deterministicSocketId(state.nextSocketSequenceNumber);
|
const id = this.allocateSocketId();
|
||||||
state.nextSocketSequenceNumber += 1;
|
|
||||||
state.socketsById.set(id, {
|
state.socketsById.set(id, {
|
||||||
id,
|
id,
|
||||||
nodeId,
|
nodeId,
|
||||||
@@ -686,6 +730,8 @@ export class NetworkRuntimeEngine {
|
|||||||
localPort: reservedPort,
|
localPort: reservedPort,
|
||||||
remoteIPAddress: options.remoteIPAddress,
|
remoteIPAddress: options.remoteIPAddress,
|
||||||
remotePort: options.remotePort,
|
remotePort: options.remotePort,
|
||||||
|
tcpState: undefined,
|
||||||
|
parentListenerSocketId: undefined,
|
||||||
});
|
});
|
||||||
state.udpReceiveQueuesBySocketId.set(id, []);
|
state.udpReceiveQueuesBySocketId.set(id, []);
|
||||||
return id;
|
return id;
|
||||||
@@ -709,15 +755,38 @@ export class NetworkRuntimeEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeSocket(socketId: Id): void {
|
closeSocket(socketId: Id): void {
|
||||||
if (this.stateValue.socketsById.get(socketId)?.protocol === 'tcp') throw new NotPortedError('TCP sockets');
|
if (this.stateValue.socketsById.get(socketId)?.protocol === 'tcp') {
|
||||||
|
this.closeTcpSocket(socketId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.stateValue.socketsById.delete(socketId);
|
this.stateValue.socketsById.delete(socketId);
|
||||||
this.stateValue.udpReceiveQueuesBySocketId.delete(socketId);
|
this.stateValue.udpReceiveQueuesBySocketId.delete(socketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Forgets a socket without closing it (Swift's discardSocket; TCP children once TCP is ported). */
|
/** Forgets a socket without closing it, with the sockets a TCP listener accepted. */
|
||||||
discardSocket(socketId: Id): void {
|
discardSocket(socketId: Id): void {
|
||||||
this.stateValue.socketsById.delete(socketId);
|
const state = this.stateValue;
|
||||||
this.stateValue.udpReceiveQueuesBySocketId.delete(socketId);
|
const children = state.tcpAcceptedSocketIdsByListenerId.get(socketId) ?? [];
|
||||||
|
state.tcpAcceptedSocketIdsByListenerId.delete(socketId);
|
||||||
|
for (const child of children) {
|
||||||
|
state.socketsById.delete(child);
|
||||||
|
state.tcpSessionsById.delete(child);
|
||||||
|
}
|
||||||
|
state.socketsById.delete(socketId);
|
||||||
|
state.tcpSessionsById.delete(socketId);
|
||||||
|
state.udpReceiveQueuesBySocketId.delete(socketId);
|
||||||
|
for (const [listener, accepted] of state.tcpAcceptedSocketIdsByListenerId) {
|
||||||
|
state.tcpAcceptedSocketIdsByListenerId.set(
|
||||||
|
listener,
|
||||||
|
accepted.filter((id) => id !== socketId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private allocateSocketId(): Id {
|
||||||
|
const id = deterministicSocketId(this.stateValue.nextSocketSequenceNumber);
|
||||||
|
this.stateValue.nextSocketSequenceNumber += 1;
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -851,6 +920,621 @@ export class NetworkRuntimeEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── TCP sockets ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** A listening TCP socket; undefined while stopped, when the port is taken or the address is not the node's. */
|
||||||
|
openTcpServerSocket(nodeId: Id, localPort: number, localIPAddress?: string): Id | undefined {
|
||||||
|
const state = this.stateValue;
|
||||||
|
if (
|
||||||
|
state.phase !== 'running' ||
|
||||||
|
this.isTcpPortReserved(nodeId, localPort) ||
|
||||||
|
(localIPAddress !== undefined && !this.networkInterfaces(nodeId).some((each) => each.ipAddress === localIPAddress))
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const id = this.allocateSocketId();
|
||||||
|
state.socketsById.set(id, {
|
||||||
|
id,
|
||||||
|
nodeId,
|
||||||
|
protocol: 'tcp',
|
||||||
|
localIPAddress,
|
||||||
|
localPort,
|
||||||
|
remoteIPAddress: undefined,
|
||||||
|
remotePort: undefined,
|
||||||
|
tcpState: 'LISTEN',
|
||||||
|
parentListenerSocketId: undefined,
|
||||||
|
});
|
||||||
|
state.tcpSessionsById.set(id, this.makeTcpSession(id, 'LISTEN'));
|
||||||
|
state.tcpAcceptedSocketIdsByListenerId.set(id, []);
|
||||||
|
this.recordTcpStateTransition(id, 'CLOSED', 'LISTEN');
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A closed TCP client socket towards the remote end, on the given or a free port. */
|
||||||
|
openTcpClientSocket(
|
||||||
|
nodeId: Id,
|
||||||
|
remoteIPAddress: string,
|
||||||
|
remotePort: number,
|
||||||
|
options: { localIPAddress?: string; localPort?: number } = {},
|
||||||
|
): Id | undefined {
|
||||||
|
const state = this.stateValue;
|
||||||
|
if (state.phase !== 'running') return undefined;
|
||||||
|
let localIPAddress: string | undefined;
|
||||||
|
if (options.localIPAddress !== undefined) {
|
||||||
|
if (!this.networkInterfaces(nodeId).some((each) => each.ipAddress === options.localIPAddress)) return undefined;
|
||||||
|
localIPAddress = options.localIPAddress;
|
||||||
|
} else {
|
||||||
|
const route = this.routeForPacket(nodeId, remoteIPAddress);
|
||||||
|
localIPAddress = route.type === 'egress' ? route.interface.ipAddress : this.networkInterfaces(nodeId)[0]?.ipAddress;
|
||||||
|
}
|
||||||
|
if (localIPAddress === undefined) return undefined;
|
||||||
|
let localPort: number;
|
||||||
|
if (options.localPort !== undefined) {
|
||||||
|
if (this.isTcpPortReserved(nodeId, options.localPort)) return undefined;
|
||||||
|
localPort = options.localPort;
|
||||||
|
} else {
|
||||||
|
const port = this.reserveFreeTcpPort(nodeId);
|
||||||
|
if (port === undefined) return undefined;
|
||||||
|
localPort = port;
|
||||||
|
}
|
||||||
|
const id = this.allocateSocketId();
|
||||||
|
state.socketsById.set(id, {
|
||||||
|
id,
|
||||||
|
nodeId,
|
||||||
|
protocol: 'tcp',
|
||||||
|
localIPAddress,
|
||||||
|
localPort,
|
||||||
|
remoteIPAddress,
|
||||||
|
remotePort,
|
||||||
|
tcpState: 'CLOSED',
|
||||||
|
parentListenerSocketId: undefined,
|
||||||
|
});
|
||||||
|
state.tcpSessionsById.set(id, this.makeTcpSession(id, 'CLOSED'));
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A port held by an open (not closed) TCP socket that is no listener's child. */
|
||||||
|
isTcpPortReserved(nodeId: Id, port: number): boolean {
|
||||||
|
for (const socket of this.stateValue.socketsById.values()) {
|
||||||
|
if (
|
||||||
|
socket.nodeId === nodeId &&
|
||||||
|
socket.protocol === 'tcp' &&
|
||||||
|
socket.localPort === port &&
|
||||||
|
socket.parentListenerSocketId === undefined &&
|
||||||
|
socket.tcpState !== 'CLOSED'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectTcp(socketId: Id): boolean {
|
||||||
|
return this.connectTcpWithResult(socketId) === 'connected';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The three-way handshake from a closed client socket, waiting on the virtual clock as Java blocks. */
|
||||||
|
connectTcpWithResult(socketId: Id): TcpConnectionResult {
|
||||||
|
const state = this.stateValue;
|
||||||
|
const socket = state.socketsById.get(socketId);
|
||||||
|
if (
|
||||||
|
socket === undefined ||
|
||||||
|
socket.protocol !== 'tcp' ||
|
||||||
|
socket.parentListenerSocketId !== undefined ||
|
||||||
|
socket.remoteIPAddress === undefined ||
|
||||||
|
socket.remotePort === undefined ||
|
||||||
|
socket.tcpState !== 'CLOSED' ||
|
||||||
|
[...state.socketsById.values()].some(
|
||||||
|
(other) =>
|
||||||
|
other.id !== socketId &&
|
||||||
|
other.nodeId === socket.nodeId &&
|
||||||
|
other.protocol === 'tcp' &&
|
||||||
|
other.localPort === socket.localPort &&
|
||||||
|
other.parentListenerSocketId === undefined &&
|
||||||
|
other.tcpState !== 'CLOSED',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 'invalidSocket';
|
||||||
|
}
|
||||||
|
this.updateTcpState(socketId, 'SYN_SENT');
|
||||||
|
this.resetTcpSendAttempts(socketId);
|
||||||
|
const initialDelivery = this.transmitTcpSegment(socketId, TCP_FLAG.synchronize, new Uint8Array(), 0, 'TCP SYN');
|
||||||
|
const waitedForTimeout = this.advanceTcpUntilSettled(socketId, { waitingStates: ['SYN_SENT'] });
|
||||||
|
if (state.socketsById.get(socketId)?.tcpState === 'ESTABLISHED') return 'connected';
|
||||||
|
if (initialDelivery === undefined || initialDelivery.type !== 'delivered') return 'unreachable';
|
||||||
|
return waitedForTimeout ? 'timedOut' : 'unreachable';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends in segments of at most TCP_MAXIMUM_SEGMENT_SIZE bytes, each acknowledged before the next. */
|
||||||
|
sendTcp(socketId: Id, payload: Uint8Array): boolean {
|
||||||
|
const state = this.stateValue;
|
||||||
|
if (state.socketsById.get(socketId)?.tcpState !== 'ESTABLISHED') return false;
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
if (payload.length === 0) {
|
||||||
|
chunks.push(new Uint8Array());
|
||||||
|
} else {
|
||||||
|
for (let offset = 0; offset < payload.length; offset += TCP_MAXIMUM_SEGMENT_SIZE) {
|
||||||
|
chunks.push(payload.subarray(offset, Math.min(offset + TCP_MAXIMUM_SEGMENT_SIZE, payload.length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, chunk] of chunks.entries()) {
|
||||||
|
if (state.socketsById.get(socketId)?.tcpState !== 'ESTABLISHED') return false;
|
||||||
|
this.resetTcpSendAttempts(socketId);
|
||||||
|
const flags = index === chunks.length - 1 ? TCP_FLAG.push : 0;
|
||||||
|
this.transmitTcpSegment(socketId, flags, chunk, 0, `TCP data segment ${index + 1}/${chunks.length}`);
|
||||||
|
this.advanceTcpUntilSettled(socketId, { waitingForAcknowledgement: true });
|
||||||
|
const session = state.tcpSessionsById.get(socketId);
|
||||||
|
if (session === undefined || session.state !== 'ESTABLISHED' || session.lastSentSegment !== undefined) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The oldest whole message (up to a PSH) received on the socket. */
|
||||||
|
receiveTcp(socketId: Id): Uint8Array | undefined {
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
if (session === undefined || session.receivedMessages.length === 0) return undefined;
|
||||||
|
return session.receivedMessages.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTcpSocket(socketId: Id): void {
|
||||||
|
const socket = this.stateValue.socketsById.get(socketId);
|
||||||
|
if (socket === undefined || socket.protocol !== 'tcp' || socket.tcpState === undefined) return;
|
||||||
|
switch (socket.tcpState) {
|
||||||
|
case 'LISTEN':
|
||||||
|
case 'SYN_SENT':
|
||||||
|
this.updateTcpState(socketId, 'CLOSED');
|
||||||
|
this.clearTcpPendingTransmission(socketId);
|
||||||
|
return;
|
||||||
|
case 'ESTABLISHED':
|
||||||
|
case 'SYN_RCVD':
|
||||||
|
this.updateTcpState(socketId, 'FIN_WAIT_1');
|
||||||
|
this.resetTcpSendAttempts(socketId);
|
||||||
|
this.transmitTcpSegment(socketId, TCP_FLAG.finish, new Uint8Array(), 0, 'TCP FIN');
|
||||||
|
return;
|
||||||
|
case 'CLOSE_WAIT':
|
||||||
|
this.updateTcpState(socketId, 'LAST_ACK');
|
||||||
|
this.resetTcpSendAttempts(socketId);
|
||||||
|
this.transmitTcpSegment(socketId, TCP_FLAG.finish, new Uint8Array(), 0, 'TCP FIN after CLOSE_WAIT');
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The socket at the other end of a connection, the first by id. */
|
||||||
|
peerTcpSocketId(socketId: Id): Id | undefined {
|
||||||
|
const socket = this.stateValue.socketsById.get(socketId);
|
||||||
|
if (
|
||||||
|
socket === undefined ||
|
||||||
|
socket.protocol !== 'tcp' ||
|
||||||
|
socket.localIPAddress === undefined ||
|
||||||
|
socket.remoteIPAddress === undefined ||
|
||||||
|
socket.remotePort === undefined
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return [...this.stateValue.socketsById.values()]
|
||||||
|
.filter(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.id !== socketId &&
|
||||||
|
candidate.protocol === 'tcp' &&
|
||||||
|
candidate.tcpState !== 'LISTEN' &&
|
||||||
|
candidate.localIPAddress === socket.remoteIPAddress &&
|
||||||
|
candidate.localPort === socket.remotePort &&
|
||||||
|
candidate.remoteIPAddress === socket.localIPAddress &&
|
||||||
|
candidate.remotePort === socket.localPort,
|
||||||
|
)
|
||||||
|
.sort((lhs, rhs) => compareIds(lhs.id, rhs.id))[0]?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes a closed TCP socket; false when it is not one. */
|
||||||
|
cleanClosedTcpSocket(socketId: Id): boolean {
|
||||||
|
const state = this.stateValue;
|
||||||
|
const socket = state.socketsById.get(socketId);
|
||||||
|
if (socket?.protocol !== 'tcp' || socket.tcpState !== 'CLOSED') return false;
|
||||||
|
this.cancelTcpTimeouts(socketId);
|
||||||
|
state.socketsById.delete(socketId);
|
||||||
|
state.tcpSessionsById.delete(socketId);
|
||||||
|
state.tcpAcceptedSocketIdsByListenerId.delete(socketId);
|
||||||
|
for (const [listener, accepted] of state.tcpAcceptedSocketIdsByListenerId) {
|
||||||
|
state.tcpAcceptedSocketIdsByListenerId.set(
|
||||||
|
listener,
|
||||||
|
accepted.filter((id) => id !== socketId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private deliverTcp(packet: IPv4Packet, segment: TcpSegment, nodeId: Id, networkInterface: NetworkInterface): void {
|
||||||
|
const state = this.stateValue;
|
||||||
|
this.recordTrace({
|
||||||
|
packetIdentity: packet.identity,
|
||||||
|
nodeId,
|
||||||
|
interfaceId: networkInterface.portId,
|
||||||
|
direction: 'local',
|
||||||
|
layer: 'transport',
|
||||||
|
operation: 'accepted',
|
||||||
|
beforeHeaders: tcpHeaders(segment),
|
||||||
|
detail: 'TCP segment received',
|
||||||
|
});
|
||||||
|
const byId = (lhs: SocketRecord, rhs: SocketRecord) => compareIds(lhs.id, rhs.id);
|
||||||
|
const sockets = [...state.socketsById.values()];
|
||||||
|
const exact = sockets
|
||||||
|
.filter(
|
||||||
|
(socket) =>
|
||||||
|
socket.nodeId === nodeId &&
|
||||||
|
socket.protocol === 'tcp' &&
|
||||||
|
socket.tcpState !== 'LISTEN' &&
|
||||||
|
socket.tcpState !== 'CLOSED' &&
|
||||||
|
socket.localPort === segment.destinationPort &&
|
||||||
|
(socket.localIPAddress === undefined || socket.localIPAddress === packet.receiverIPAddress) &&
|
||||||
|
socket.remoteIPAddress === packet.senderIPAddress &&
|
||||||
|
socket.remotePort === segment.sourcePort,
|
||||||
|
)
|
||||||
|
.sort(byId)[0];
|
||||||
|
if (exact !== undefined) {
|
||||||
|
this.processIncomingTcpSegment(exact.id, segment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const listener = sockets
|
||||||
|
.filter(
|
||||||
|
(socket) =>
|
||||||
|
socket.nodeId === nodeId &&
|
||||||
|
socket.protocol === 'tcp' &&
|
||||||
|
socket.tcpState === 'LISTEN' &&
|
||||||
|
socket.localPort === segment.destinationPort &&
|
||||||
|
(socket.localIPAddress === undefined || socket.localIPAddress === packet.receiverIPAddress),
|
||||||
|
)
|
||||||
|
.sort(byId)[0];
|
||||||
|
if (listener === undefined || (segment.flags & TCP_FLAG.synchronize) === 0 || (segment.flags & TCP_FLAG.acknowledgement) !== 0) {
|
||||||
|
this.recordTrace({
|
||||||
|
packetIdentity: packet.identity,
|
||||||
|
nodeId,
|
||||||
|
interfaceId: networkInterface.portId,
|
||||||
|
direction: 'local',
|
||||||
|
layer: 'transport',
|
||||||
|
operation: 'dropped',
|
||||||
|
beforeHeaders: tcpHeaders(segment),
|
||||||
|
detail: 'TCP destination port closed',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const acceptedId = this.allocateSocketId();
|
||||||
|
state.socketsById.set(acceptedId, {
|
||||||
|
id: acceptedId,
|
||||||
|
nodeId,
|
||||||
|
protocol: 'tcp',
|
||||||
|
localIPAddress: packet.receiverIPAddress,
|
||||||
|
localPort: segment.destinationPort,
|
||||||
|
remoteIPAddress: packet.senderIPAddress,
|
||||||
|
remotePort: segment.sourcePort,
|
||||||
|
tcpState: 'SYN_RCVD',
|
||||||
|
parentListenerSocketId: listener.id,
|
||||||
|
});
|
||||||
|
const session = this.makeTcpSession(acceptedId, 'SYN_RCVD');
|
||||||
|
session.remoteSequenceNumber = nextTcpSequenceNumber(segment);
|
||||||
|
state.tcpSessionsById.set(acceptedId, session);
|
||||||
|
getOrCreate(state.tcpAcceptedSocketIdsByListenerId, listener.id, () => []).push(acceptedId);
|
||||||
|
this.recordTcpStateTransition(acceptedId, 'LISTEN', 'SYN_RCVD');
|
||||||
|
this.resetTcpSendAttempts(acceptedId);
|
||||||
|
this.transmitTcpSegment(
|
||||||
|
acceptedId,
|
||||||
|
TCP_FLAG.synchronize | TCP_FLAG.acknowledgement,
|
||||||
|
new Uint8Array(),
|
||||||
|
session.remoteSequenceNumber,
|
||||||
|
'TCP SYN+ACK',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processIncomingTcpSegment(socketId: Id, segment: TcpSegment): void {
|
||||||
|
const socket = this.stateValue.socketsById.get(socketId);
|
||||||
|
if (socket === undefined || socket.tcpState === undefined) return;
|
||||||
|
const has = (flag: number) => (segment.flags & flag) !== 0;
|
||||||
|
if (has(TCP_FLAG.reset)) {
|
||||||
|
this.clearTcpPendingTransmission(socketId);
|
||||||
|
this.updateTcpState(socketId, 'CLOSED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (socket.tcpState) {
|
||||||
|
case 'SYN_SENT': {
|
||||||
|
if (
|
||||||
|
!has(TCP_FLAG.synchronize) ||
|
||||||
|
!has(TCP_FLAG.acknowledgement) ||
|
||||||
|
!this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)
|
||||||
|
) {
|
||||||
|
this.updateTcpState(socketId, 'CLOSED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
if (session !== undefined) session.remoteSequenceNumber = nextTcpSequenceNumber(segment);
|
||||||
|
this.updateTcpState(socketId, 'ESTABLISHED');
|
||||||
|
this.sendTcpAcknowledgement(socketId, segment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'SYN_RCVD':
|
||||||
|
if (!has(TCP_FLAG.acknowledgement) || !this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)) return;
|
||||||
|
this.updateTcpState(socketId, 'ESTABLISHED');
|
||||||
|
return;
|
||||||
|
case 'ESTABLISHED':
|
||||||
|
if (has(TCP_FLAG.finish)) {
|
||||||
|
this.sendTcpAcknowledgement(socketId, segment);
|
||||||
|
this.updateTcpState(socketId, 'CLOSE_WAIT');
|
||||||
|
} else if (segment.payload.length > 0 || has(TCP_FLAG.push)) {
|
||||||
|
this.receiveTcpPayload(socketId, segment);
|
||||||
|
} else if (has(TCP_FLAG.acknowledgement)) {
|
||||||
|
this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'FIN_WAIT_1': {
|
||||||
|
const hasAck = has(TCP_FLAG.acknowledgement);
|
||||||
|
const hasFin = has(TCP_FLAG.finish);
|
||||||
|
if (hasAck) this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber);
|
||||||
|
if (hasAck && hasFin) {
|
||||||
|
this.sendTcpAcknowledgement(socketId, segment);
|
||||||
|
this.enterTcpTimeWait(socketId);
|
||||||
|
} else if (hasAck) {
|
||||||
|
this.updateTcpState(socketId, 'FIN_WAIT_2');
|
||||||
|
} else if (hasFin) {
|
||||||
|
this.sendTcpAcknowledgement(socketId, segment);
|
||||||
|
this.updateTcpState(socketId, 'CLOSING');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'FIN_WAIT_2':
|
||||||
|
if (has(TCP_FLAG.finish)) {
|
||||||
|
this.sendTcpAcknowledgement(socketId, segment);
|
||||||
|
this.enterTcpTimeWait(socketId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'CLOSING':
|
||||||
|
if (has(TCP_FLAG.acknowledgement) && this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)) {
|
||||||
|
this.enterTcpTimeWait(socketId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'LAST_ACK':
|
||||||
|
if (has(TCP_FLAG.acknowledgement) && this.acknowledgeTcpIfExpected(socketId, segment.acknowledgementNumber)) {
|
||||||
|
this.updateTcpState(socketId, 'CLOSED');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private receiveTcpPayload(socketId: Id, segment: TcpSegment): void {
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
if (session === undefined) return;
|
||||||
|
const nextRemoteSequence = nextTcpSequenceNumber(segment);
|
||||||
|
if (nextRemoteSequence > session.remoteSequenceNumber) {
|
||||||
|
session.remoteSequenceNumber = nextRemoteSequence;
|
||||||
|
session.receiveAssembly = concatBytes(session.receiveAssembly, segment.payload);
|
||||||
|
if ((segment.flags & TCP_FLAG.push) !== 0) {
|
||||||
|
session.receivedMessages.push(session.receiveAssembly);
|
||||||
|
session.receiveAssembly = new Uint8Array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sendTcpAcknowledgement(socketId, segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendTcpAcknowledgement(socketId: Id, segment: TcpSegment): void {
|
||||||
|
this.transmitTcpSegment(socketId, TCP_FLAG.acknowledgement, new Uint8Array(), nextTcpSequenceNumber(segment), 'TCP ACK');
|
||||||
|
}
|
||||||
|
|
||||||
|
private transmitTcpSegment(
|
||||||
|
socketId: Id,
|
||||||
|
flags: number,
|
||||||
|
payload: Uint8Array,
|
||||||
|
acknowledgementNumber: number,
|
||||||
|
detail: string,
|
||||||
|
retransmission = false,
|
||||||
|
): IPv4DeliveryResult | undefined {
|
||||||
|
const state = this.stateValue;
|
||||||
|
const socket = state.socketsById.get(socketId);
|
||||||
|
const session = state.tcpSessionsById.get(socketId);
|
||||||
|
if (socket === undefined || socket.remoteIPAddress === undefined || socket.remotePort === undefined || session === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let segment: TcpSegment;
|
||||||
|
if (retransmission && session.lastSentSegment !== undefined) {
|
||||||
|
segment = session.lastSentSegment;
|
||||||
|
session.sendAttempts += 1;
|
||||||
|
} else {
|
||||||
|
segment = {
|
||||||
|
sourcePort: socket.localPort,
|
||||||
|
destinationPort: socket.remotePort,
|
||||||
|
sequenceNumber: session.nextSendSequenceNumber,
|
||||||
|
acknowledgementNumber,
|
||||||
|
flags,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
session.lastSentSequenceNumber = segment.sequenceNumber;
|
||||||
|
session.nextSendSequenceNumber = nextTcpSequenceNumber(segment);
|
||||||
|
if (tcpSegmentRequiresAcknowledgement(segment)) {
|
||||||
|
session.lastSentSegment = segment;
|
||||||
|
session.sendAttempts = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const interfaces = this.networkInterfaces(socket.nodeId);
|
||||||
|
let networkInterface =
|
||||||
|
socket.localIPAddress === undefined ? undefined : interfaces.find((each) => each.ipAddress === socket.localIPAddress);
|
||||||
|
if (networkInterface === undefined) {
|
||||||
|
const route = this.routeForPacket(socket.nodeId, socket.remoteIPAddress);
|
||||||
|
networkInterface = route.type === 'egress' ? route.interface : interfaces[0];
|
||||||
|
}
|
||||||
|
if (networkInterface === undefined) return undefined;
|
||||||
|
const packet: IPv4Packet = {
|
||||||
|
identity: this.allocatePacketIdentity(),
|
||||||
|
senderIPAddress: networkInterface.ipAddress,
|
||||||
|
receiverIPAddress: socket.remoteIPAddress,
|
||||||
|
timeToLive: 64,
|
||||||
|
protocol: 'tcp',
|
||||||
|
payload: { kind: 'tcp', segment },
|
||||||
|
};
|
||||||
|
this.recordTrace({
|
||||||
|
packetIdentity: packet.identity,
|
||||||
|
nodeId: socket.nodeId,
|
||||||
|
interfaceId: networkInterface.portId,
|
||||||
|
direction: 'outbound',
|
||||||
|
layer: 'transport',
|
||||||
|
operation: retransmission ? 'retransmitted' : 'sent',
|
||||||
|
afterHeaders: tcpHeaders(segment),
|
||||||
|
detail: retransmission ? `${detail} retransmission attempt ${session.sendAttempts}` : detail,
|
||||||
|
});
|
||||||
|
const delivery = this.sendIPv4Packet(socket.nodeId, packet, networkInterface.portId);
|
||||||
|
if (state.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined) this.scheduleTcpTimeoutIfNeeded(socketId);
|
||||||
|
return delivery;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleTcpTimeoutIfNeeded(socketId: Id): void {
|
||||||
|
const pending = this.stateValue.pendingEvents.some((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId);
|
||||||
|
if (pending) return;
|
||||||
|
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_DEFAULT_TIMEOUT_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTcpTimeout(socketId: Id): void {
|
||||||
|
const socket = this.stateValue.socketsById.get(socketId);
|
||||||
|
if (socket === undefined || socket.tcpState === undefined) return;
|
||||||
|
if (socket.tcpState === 'TIME_WAIT') {
|
||||||
|
this.updateTcpState(socketId, 'CLOSED');
|
||||||
|
this.clearTcpPendingTransmission(socketId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
const last = session?.lastSentSegment;
|
||||||
|
if (session === undefined || last === undefined) return;
|
||||||
|
if (session.sendAttempts >= TCP_MAXIMUM_SEND_ATTEMPTS) {
|
||||||
|
this.clearTcpPendingTransmission(socketId);
|
||||||
|
this.updateTcpState(socketId, 'CLOSED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const synchronize = (last.flags & TCP_FLAG.synchronize) !== 0;
|
||||||
|
const baseDetail = synchronize
|
||||||
|
? (last.flags & TCP_FLAG.acknowledgement) === 0
|
||||||
|
? 'TCP SYN'
|
||||||
|
: 'TCP SYN+ACK'
|
||||||
|
: (last.flags & TCP_FLAG.finish) !== 0
|
||||||
|
? 'TCP FIN'
|
||||||
|
: 'TCP data';
|
||||||
|
this.transmitTcpSegment(socketId, last.flags, last.payload, last.acknowledgementNumber, baseDetail, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private acknowledgeTcpIfExpected(socketId: Id, acknowledgementNumber: number): boolean {
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
if (session === undefined || session.lastSentSegment === undefined || acknowledgementNumber !== session.nextSendSequenceNumber) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
session.lastAcknowledgedSequenceNumber = acknowledgementNumber;
|
||||||
|
session.lastSentSegment = undefined;
|
||||||
|
this.cancelTcpTimeouts(socketId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearTcpPendingTransmission(socketId: Id): void {
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
if (session !== undefined) session.lastSentSegment = undefined;
|
||||||
|
this.cancelTcpTimeouts(socketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelTcpTimeouts(socketId: Id): void {
|
||||||
|
this.stateValue.pendingEvents = this.stateValue.pendingEvents.filter(
|
||||||
|
(event) => !(event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enterTcpTimeWait(socketId: Id): void {
|
||||||
|
this.clearTcpPendingTransmission(socketId);
|
||||||
|
this.updateTcpState(socketId, 'TIME_WAIT');
|
||||||
|
this.schedule(this.stateValue.currentTimeMilliseconds + TCP_ROUND_TRIP_TIME_MILLISECONDS, { kind: 'tcpTimeout', sessionId: socketId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances the clock to the socket's timeouts while it waits in one of
|
||||||
|
* the states or for an acknowledgement; whether it advanced at all.
|
||||||
|
*/
|
||||||
|
private advanceTcpUntilSettled(
|
||||||
|
socketId: Id,
|
||||||
|
options: { waitingStates?: readonly TcpSocketState[]; waitingForAcknowledgement?: boolean },
|
||||||
|
): boolean {
|
||||||
|
let advancedToTimeout = false;
|
||||||
|
while (this.stateValue.phase === 'running') {
|
||||||
|
const socket = this.stateValue.socketsById.get(socketId);
|
||||||
|
if (socket === undefined || socket.tcpState === undefined) return advancedToTimeout;
|
||||||
|
const waitingOnState = options.waitingStates?.includes(socket.tcpState) === true;
|
||||||
|
const waitingOnAck =
|
||||||
|
options.waitingForAcknowledgement === true && this.stateValue.tcpSessionsById.get(socketId)?.lastSentSegment !== undefined;
|
||||||
|
if (!waitingOnState && !waitingOnAck) return advancedToTimeout;
|
||||||
|
const deadlines = this.stateValue.pendingEvents
|
||||||
|
.filter((event) => event.kind.kind === 'tcpTimeout' && event.kind.sessionId === socketId)
|
||||||
|
.map((event) => event.deadlineMilliseconds);
|
||||||
|
if (deadlines.length === 0) return advancedToTimeout;
|
||||||
|
advancedToTimeout = true;
|
||||||
|
this.advance(Math.min(...deadlines));
|
||||||
|
}
|
||||||
|
return advancedToTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTcpState(socketId: Id, newState: TcpSocketState): void {
|
||||||
|
const socket = this.stateValue.socketsById.get(socketId);
|
||||||
|
const oldState = socket?.tcpState;
|
||||||
|
if (socket === undefined || oldState === undefined || oldState === newState) return;
|
||||||
|
socket.tcpState = newState;
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
if (session !== undefined) session.state = newState;
|
||||||
|
this.recordTcpStateTransition(socketId, oldState, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordTcpStateTransition(socketId: Id, oldState: TcpSocketState, newState: TcpSocketState): void {
|
||||||
|
const socket = this.stateValue.socketsById.get(socketId);
|
||||||
|
if (socket === undefined) return;
|
||||||
|
this.recordTrace({
|
||||||
|
nodeId: socket.nodeId,
|
||||||
|
direction: 'local',
|
||||||
|
layer: 'transport',
|
||||||
|
operation: 'accepted',
|
||||||
|
beforeHeaders: [{ name: 'state', value: oldState }],
|
||||||
|
afterHeaders: [{ name: 'state', value: newState }],
|
||||||
|
detail: `TCP ${oldState} -> ${newState}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetTcpSendAttempts(socketId: Id): void {
|
||||||
|
const session = this.stateValue.tcpSessionsById.get(socketId);
|
||||||
|
if (session !== undefined) session.sendAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private reserveFreeTcpPort(nodeId: Id): number | undefined {
|
||||||
|
const span = TCP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE - TCP_EPHEMERAL_PORT_LOWER_BOUND;
|
||||||
|
for (let attempt = 0; attempt < span; attempt++) {
|
||||||
|
const port = TCP_EPHEMERAL_PORT_LOWER_BOUND + this.tcpPortRandom.nextInt(span);
|
||||||
|
if (!this.isTcpPortReserved(nodeId, port)) return port;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Swift: (value mod 2^32) · 1,000,000, truncated to 32 bits. */
|
||||||
|
private allocateTcpInitialSequenceNumber(): number {
|
||||||
|
const value = BigInt(this.stateValue.nextTcpInitialSequenceValue);
|
||||||
|
this.stateValue.nextTcpInitialSequenceValue += 1;
|
||||||
|
return Number(BigInt.asUintN(32, (value % 4_294_967_296n) * 1_000_000n));
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeTcpSession(socketId: Id, socketState: TcpSocketState): TcpSession {
|
||||||
|
const initialSequence = this.allocateTcpInitialSequenceNumber();
|
||||||
|
return {
|
||||||
|
id: socketId,
|
||||||
|
socketId,
|
||||||
|
state: socketState,
|
||||||
|
sendAttempts: 0,
|
||||||
|
nextSendSequenceNumber: initialSequence,
|
||||||
|
lastSentSequenceNumber: initialSequence,
|
||||||
|
remoteSequenceNumber: 0,
|
||||||
|
lastAcknowledgedSequenceNumber: undefined,
|
||||||
|
lastSentSegment: undefined,
|
||||||
|
receiveAssembly: new Uint8Array(),
|
||||||
|
receivedMessages: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Later slices hook in here ────────────────────────────────────────────
|
// ── Later slices hook in here ────────────────────────────────────────────
|
||||||
|
|
||||||
private initializeRipState(): void {
|
private initializeRipState(): void {
|
||||||
@@ -1783,7 +2467,8 @@ export class NetworkRuntimeEngine {
|
|||||||
interfaceId: networkInterface.portId,
|
interfaceId: networkInterface.portId,
|
||||||
packet,
|
packet,
|
||||||
});
|
});
|
||||||
throw new NotPortedError('TCP delivery');
|
this.deliverTcp(packet, payload.segment, nodeId, networkInterface);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2179,6 +2864,47 @@ export function ipv4Headers(packet: IPv4Packet): HeaderField[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tcpSegmentRequiresAcknowledgement(segment: TcpSegment): boolean {
|
||||||
|
return (
|
||||||
|
(segment.flags & (TCP_FLAG.synchronize | TCP_FLAG.finish | TCP_FLAG.push)) !== 0 || segment.payload.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The sequence number after the segment: SYN and FIN count one, and each payload byte. */
|
||||||
|
export function nextTcpSequenceNumber(segment: TcpSegment): number {
|
||||||
|
let next = segment.sequenceNumber;
|
||||||
|
if ((segment.flags & (TCP_FLAG.synchronize | TCP_FLAG.finish)) !== 0) next = uint32Add(next, 1);
|
||||||
|
return uint32Add(next, segment.payload.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function concatBytes(first: Uint8Array, second: Uint8Array): Uint8Array {
|
||||||
|
const result = new Uint8Array(first.length + second.length);
|
||||||
|
result.set(first);
|
||||||
|
result.set(second, first.length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tcpFlagDescription(flags: number): string {
|
||||||
|
const names: string[] = [];
|
||||||
|
if ((flags & TCP_FLAG.finish) !== 0) names.push('FIN');
|
||||||
|
if ((flags & TCP_FLAG.synchronize) !== 0) names.push('SYN');
|
||||||
|
if ((flags & TCP_FLAG.reset) !== 0) names.push('RST');
|
||||||
|
if ((flags & TCP_FLAG.push) !== 0) names.push('PSH');
|
||||||
|
if ((flags & TCP_FLAG.acknowledgement) !== 0) names.push('ACK');
|
||||||
|
return names.join('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tcpHeaders(segment: TcpSegment): HeaderField[] {
|
||||||
|
return [
|
||||||
|
{ name: 'sourcePort', value: String(segment.sourcePort) },
|
||||||
|
{ name: 'destinationPort', value: String(segment.destinationPort) },
|
||||||
|
{ name: 'sequenceNumber', value: String(segment.sequenceNumber) },
|
||||||
|
{ name: 'acknowledgementNumber', value: String(segment.acknowledgementNumber) },
|
||||||
|
{ name: 'flags', value: tcpFlagDescription(segment.flags) },
|
||||||
|
{ name: 'payloadLength', value: String(segment.payload.length) },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function udpHeaders(datagram: UdpDatagram): HeaderField[] {
|
export function udpHeaders(datagram: UdpDatagram): HeaderField[] {
|
||||||
return [
|
return [
|
||||||
{ name: 'sourcePort', value: String(datagram.sourcePort) },
|
{ name: 'sourcePort', value: String(datagram.sourcePort) },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Ported from filius-ipad ios/FiliusPadTests/TopologyNetworkRuntimeEngineTests.swift:
|
// Ported from filius-ipad ios/FiliusPadTests/TopologyNetworkRuntimeEngineTests.swift:
|
||||||
// the event queue, the virtual clock, Java-compatible randomness, restart,
|
// the event queue, the virtual clock, Java-compatible randomness, restart,
|
||||||
// Ethernet/switch/ARP/IPv4/ICMP, routing errors, the firewall, packet loss
|
// Ethernet/switch/ARP/IPv4/ICMP, routing errors, the firewall, packet loss
|
||||||
// and trace retention, UDP sockets. The remaining Swift tests (TCP, DHCP,
|
// and trace retention, UDP and TCP sockets. The remaining Swift tests (DHCP,
|
||||||
// DNS, RIP, NAT, remote link, packet viewer) follow with their protocol
|
// DNS, RIP, NAT, remote link, packet viewer) follow with their protocol
|
||||||
// slices.
|
// slices.
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
stableMacAddress,
|
stableMacAddress,
|
||||||
switchConfiguration,
|
switchConfiguration,
|
||||||
TCP_FLAG,
|
TCP_FLAG,
|
||||||
|
TCP_ROUND_TRIP_TIME_MILLISECONDS,
|
||||||
|
tcpFlagDescription,
|
||||||
topologySnapshot,
|
topologySnapshot,
|
||||||
traceRetentionPolicy,
|
traceRetentionPolicy,
|
||||||
UNSPECIFIED_MAC_ADDRESS,
|
UNSPECIFIED_MAC_ADDRESS,
|
||||||
@@ -209,6 +211,25 @@ describe('event queue and virtual clock', () => {
|
|||||||
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
|
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
|
||||||
expect(engine.bindUdpSocket(nodeId, { localPort: 7 })).toBe(uuid(1));
|
expect(engine.bindUdpSocket(nodeId, { localPort: 7 })).toBe(uuid(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forgets TCP sessions and accepted sockets on stop', () => {
|
||||||
|
const [clientId, clientPortId, serverId, serverPortId] = [0x21, 0x22, 0x23, 0x24].map(uuid) as [string, string, string, string];
|
||||||
|
const engine = startedEngine({
|
||||||
|
nodes: [pc(clientId, clientPortId), pc(serverId, serverPortId)],
|
||||||
|
links: [link(clientId, clientPortId, serverId, serverPortId)],
|
||||||
|
deviceConfigurations: new Map([
|
||||||
|
[clientId, device('10.3.0.1')],
|
||||||
|
[serverId, device('10.3.0.2')],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
engine.openTcpServerSocket(serverId, 80);
|
||||||
|
expect(engine.connectTcp(engine.openTcpClientSocket(clientId, '10.3.0.2', 80)!)).toBe(true);
|
||||||
|
expect(engine.state.tcpSessionsById.size).toBe(3);
|
||||||
|
|
||||||
|
engine.handle({ type: 'stop' });
|
||||||
|
expect(engine.state.tcpSessionsById.size).toBe(0);
|
||||||
|
expect(engine.state.tcpAcceptedSocketIdsByListenerId.size).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UDP sockets', () => {
|
describe('UDP sockets', () => {
|
||||||
@@ -738,3 +759,111 @@ describe('packet loss and trace retention', () => {
|
|||||||
expect(engine.state.nextTraceIdentity).toBe(7);
|
expect(engine.state.nextTraceIdentity).toBe(7);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('TCP sockets', () => {
|
||||||
|
/** Two PCs on one cable, started. */
|
||||||
|
const pair = (first: number, second: number, prefix: string) => {
|
||||||
|
const [clientId, clientPortId, serverId, serverPortId] = [first, first + 0x10, second, second + 0x10].map(uuid) as [
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
];
|
||||||
|
const engine = startedEngine({
|
||||||
|
nodes: [pc(clientId, clientPortId), pc(serverId, serverPortId)],
|
||||||
|
links: [link(clientId, clientPortId, serverId, serverPortId)],
|
||||||
|
deviceConfigurations: new Map([
|
||||||
|
[clientId, device(`${prefix}.1`)],
|
||||||
|
[serverId, device(`${prefix}.2`)],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
return { engine, clientId, serverId };
|
||||||
|
};
|
||||||
|
|
||||||
|
it('makes an established client and an accepted server socket with the three-way handshake', () => {
|
||||||
|
const { engine, clientId, serverId } = pair(0x1701, 0x1702, '10.17.0');
|
||||||
|
const listenerId = engine.openTcpServerSocket(serverId, 8080)!;
|
||||||
|
const clientSocketId = engine.openTcpClientSocket(clientId, '10.17.0.2', 8080)!;
|
||||||
|
|
||||||
|
expect(engine.connectTcp(clientSocketId)).toBe(true);
|
||||||
|
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
||||||
|
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('ESTABLISHED');
|
||||||
|
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('ESTABLISHED');
|
||||||
|
expect(engine.state.packetTraces.some((trace) => trace.detail === 'TCP SYN_SENT -> ESTABLISHED')).toBe(true);
|
||||||
|
expect(engine.state.packetTraces.some((trace) => trace.detail === 'TCP SYN_RCVD -> ESTABLISHED')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends segments of Java's MSS and reassembles one message up to the push", () => {
|
||||||
|
const { engine, clientId, serverId } = pair(0x1721, 0x1722, '10.18.0');
|
||||||
|
const listenerId = engine.openTcpServerSocket(serverId, 8081)!;
|
||||||
|
const clientSocketId = engine.openTcpClientSocket(clientId, '10.18.0.2', 8081)!;
|
||||||
|
expect(engine.connectTcp(clientSocketId)).toBe(true);
|
||||||
|
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
||||||
|
const payload = Uint8Array.from({ length: 3_001 }, (_, index) => index % 251);
|
||||||
|
|
||||||
|
expect(engine.sendTcp(clientSocketId, payload)).toBe(true);
|
||||||
|
expect(engine.receiveTcp(acceptedSocketId)).toEqual(payload);
|
||||||
|
const payloadLengths = engine.state.packetTraces
|
||||||
|
.filter((trace) => trace.detail?.startsWith('TCP data') === true)
|
||||||
|
.map((trace) => Number(trace.afterHeaders.find((header) => header.name === 'payloadLength')?.value));
|
||||||
|
expect(payloadLengths).toEqual([1_460, 1_460, 81]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retransmits a connection attempt at most three times and releases the port', () => {
|
||||||
|
const clientId = uuid(0x1741);
|
||||||
|
const engine = startedEngine({ nodes: [pc(clientId, uuid(0x1751))], links: [], deviceConfigurations: new Map([[clientId, device('10.19.0.1')]]) });
|
||||||
|
const socketId = engine.openTcpClientSocket(clientId, '10.19.0.99', 80)!;
|
||||||
|
const localPort = engine.state.socketsById.get(socketId)!.localPort;
|
||||||
|
|
||||||
|
expect(engine.connectTcp(socketId)).toBe(false);
|
||||||
|
expect(engine.state.tcpSessionsById.get(socketId)?.sendAttempts).toBe(3);
|
||||||
|
expect(engine.state.socketsById.get(socketId)?.tcpState).toBe('CLOSED');
|
||||||
|
expect(engine.isTcpPortReserved(clientId, localPort)).toBe(false);
|
||||||
|
expect(engine.state.packetTraces.filter((trace) => trace.detail?.includes('TCP SYN retransmission') === true)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
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)!;
|
||||||
|
const segment = { sourcePort: 80, destinationPort: 7000, sequenceNumber: 5, acknowledgementNumber: 9, flags: TCP_FLAG.synchronize | TCP_FLAG.acknowledgement, payload: new Uint8Array() };
|
||||||
|
engine.sendIPv4Packet(clientId, {
|
||||||
|
identity: engine.allocatePacketIdentity(),
|
||||||
|
senderIPAddress: '10.21.0.1',
|
||||||
|
receiverIPAddress: '10.21.0.2',
|
||||||
|
timeToLive: 64,
|
||||||
|
protocol: 'tcp',
|
||||||
|
payload: { kind: 'tcp', segment },
|
||||||
|
});
|
||||||
|
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 1_000 });
|
||||||
|
|
||||||
|
expect(engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)).toEqual([]);
|
||||||
|
expect(engine.state.packetTraces.some((trace) => trace.detail === 'TCP destination port closed')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('names the flags of a segment in Swift\'s order', () => {
|
||||||
|
expect(tcpFlagDescription(TCP_FLAG.acknowledgement | TCP_FLAG.push | TCP_FLAG.reset | TCP_FLAG.synchronize | TCP_FLAG.finish)).toBe(
|
||||||
|
'FIN+SYN+RST+PSH+ACK',
|
||||||
|
);
|
||||||
|
expect(tcpFlagDescription(TCP_FLAG.synchronize | TCP_FLAG.acknowledgement)).toBe('SYN+ACK');
|
||||||
|
expect(tcpFlagDescription(0)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closes with Java's FIN states, and TIME_WAIT releases both ends", () => {
|
||||||
|
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)!;
|
||||||
|
expect(engine.connectTcp(clientSocketId)).toBe(true);
|
||||||
|
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);
|
||||||
|
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
|
||||||
|
expect(engine.state.socketsById.get(serverSocketId)?.tcpState).toBe('CLOSED');
|
||||||
|
|
||||||
|
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + TCP_ROUND_TRIP_TIME_MILLISECONDS });
|
||||||
|
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('CLOSED');
|
||||||
|
expect(engine.state.socketsById.get(listenerId)?.tcpState).toBe('LISTEN');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ interface Scenario {
|
|||||||
socket?: string;
|
socket?: string;
|
||||||
payload?: string;
|
payload?: string;
|
||||||
timeout?: number;
|
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;
|
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':
|
case 'closeSocket':
|
||||||
engine.closeSocket(step.socket!);
|
engine.closeSocket(step.socket!);
|
||||||
results.push(!engine.state.socketsById.has(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,
|
remoteIp: socket.remoteIPAddress ?? null,
|
||||||
remotePort: socket.remotePort ?? null,
|
remotePort: socket.remotePort ?? null,
|
||||||
queued: state.udpReceiveQueuesBySocketId.get(socket.id)?.length ?? 0,
|
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) => ({
|
firewallDecisions: state.firewallDecisions.map((decision) => ({
|
||||||
packet: decision.packetIdentity,
|
packet: decision.packetIdentity,
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"seed": 33,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000101",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000111",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000102",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000112",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000103",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000113",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"kind": "networkSwitch",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000121",
|
||||||
|
"label": "sw1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000122",
|
||||||
|
"label": "sw2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000123",
|
||||||
|
"label": "sw3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000124",
|
||||||
|
"label": "sw4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F001",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000101",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000111",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000121"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F002",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000102",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000112",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000122"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F003",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000103",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000113",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000123"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devices": {
|
||||||
|
"00000000-0000-0000-0000-000000000101": {
|
||||||
|
"ip": "192.168.0.10",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"00000000-0000-0000-0000-000000000102": {
|
||||||
|
"ip": "192.168.0.20",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"00000000-0000-0000-0000-000000000103": {
|
||||||
|
"ip": "192.168.0.30",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"00000000-0000-0000-0000-000000000104": {
|
||||||
|
"ssid": "lab",
|
||||||
|
"retention": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "openTcpServer",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000101",
|
||||||
|
"port": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpClient",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000102",
|
||||||
|
"target": "192.168.0.10",
|
||||||
|
"port": 7,
|
||||||
|
"localIp": "192.168.0.20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "connectTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "sendTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002",
|
||||||
|
"payload": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "receiveTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "packetLoss",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "closeSocket",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "packetLoss",
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "closeSocket",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "advance",
|
||||||
|
"to": 30000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpClient",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000102",
|
||||||
|
"target": "192.168.0.10",
|
||||||
|
"port": 7,
|
||||||
|
"localIp": "10.0.0.1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
|||||||
|
{
|
||||||
|
"seed": 31,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000101",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000111",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000102",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000112",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000103",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000113",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"kind": "networkSwitch",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000121",
|
||||||
|
"label": "sw1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000122",
|
||||||
|
"label": "sw2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000123",
|
||||||
|
"label": "sw3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000124",
|
||||||
|
"label": "sw4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F001",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000101",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000111",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000121"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F002",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000102",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000112",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000122"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F003",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000103",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000113",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000123"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devices": {
|
||||||
|
"00000000-0000-0000-0000-000000000101": {
|
||||||
|
"ip": "192.168.0.10",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"00000000-0000-0000-0000-000000000102": {
|
||||||
|
"ip": "192.168.0.20",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"00000000-0000-0000-0000-000000000103": {
|
||||||
|
"ip": "192.168.0.30",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"00000000-0000-0000-0000-000000000104": {
|
||||||
|
"ssid": "lab",
|
||||||
|
"retention": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "openTcpServer",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000101",
|
||||||
|
"port": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpServer",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000101",
|
||||||
|
"port": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpClient",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000102",
|
||||||
|
"target": "192.168.0.10",
|
||||||
|
"port": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "connectTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "acceptedTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "peerTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "sendTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002",
|
||||||
|
"payload": "GET /"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "receiveTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "sendTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003",
|
||||||
|
"payload": "x",
|
||||||
|
"repeatCount": 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "receiveTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "receiveTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "closeSocket",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "advance",
|
||||||
|
"to": 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "closeSocket",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "advance",
|
||||||
|
"to": 4000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "advance",
|
||||||
|
"to": 8000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "cleanTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "cleanTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "connectTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpClient",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000103",
|
||||||
|
"target": "192.168.0.10",
|
||||||
|
"port": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "sendTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000004",
|
||||||
|
"payload": "ohne Verbindung"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "connectTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpClient",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000103",
|
||||||
|
"target": "192.168.0.99",
|
||||||
|
"port": 80,
|
||||||
|
"localPort": 5000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "connectTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000005"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "closeSocket",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000001"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
|||||||
|
{
|
||||||
|
"seed": 32,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000201",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000211",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000202",
|
||||||
|
"kind": "router",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000221",
|
||||||
|
"label": "rt1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000222",
|
||||||
|
"label": "rt2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000203",
|
||||||
|
"kind": "pc",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000213",
|
||||||
|
"label": "eth0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F004",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000201",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000211",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000202",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000221"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-00000000F005",
|
||||||
|
"sourceNode": "00000000-0000-0000-0000-000000000202",
|
||||||
|
"sourcePort": "00000000-0000-0000-0000-000000000222",
|
||||||
|
"targetNode": "00000000-0000-0000-0000-000000000203",
|
||||||
|
"targetPort": "00000000-0000-0000-0000-000000000213"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"devices": {
|
||||||
|
"00000000-0000-0000-0000-000000000201": {
|
||||||
|
"ip": "10.0.0.2",
|
||||||
|
"mask": "255.255.255.0",
|
||||||
|
"gateway": "10.0.0.1"
|
||||||
|
},
|
||||||
|
"00000000-0000-0000-0000-000000000202": {
|
||||||
|
"ip": "10.0.0.1",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"00000000-0000-0000-0000-000000000203": {
|
||||||
|
"ip": "20.0.0.2",
|
||||||
|
"mask": "255.255.255.0",
|
||||||
|
"gateway": "20.0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"node": "00000000-0000-0000-0000-000000000202",
|
||||||
|
"port": "00000000-0000-0000-0000-000000000221",
|
||||||
|
"ip": "10.0.0.1",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "00000000-0000-0000-0000-000000000202",
|
||||||
|
"port": "00000000-0000-0000-0000-000000000222",
|
||||||
|
"ip": "20.0.0.1",
|
||||||
|
"mask": "255.255.255.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"op": "openTcpServer",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000203",
|
||||||
|
"port": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpServer",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000203",
|
||||||
|
"port": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpClient",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000201",
|
||||||
|
"target": "20.0.0.2",
|
||||||
|
"port": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "connectTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "sendTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003",
|
||||||
|
"payload": "durch den Router"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "receiveTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpClient",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000201",
|
||||||
|
"target": "20.0.0.2",
|
||||||
|
"port": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "connectTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000005"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "packetLoss",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "sendTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003",
|
||||||
|
"payload": "verloren"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "packetLoss",
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "sendTcp",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000003",
|
||||||
|
"payload": "zu spaet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "closeSocket",
|
||||||
|
"socket": "00000000-0000-0000-0000-000000000001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "openTcpServer",
|
||||||
|
"node": "00000000-0000-0000-0000-000000000203",
|
||||||
|
"port": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"firewalls": {
|
||||||
|
"00000000-0000-0000-0000-000000000202": {
|
||||||
|
"active": true,
|
||||||
|
"defaultPolicy": "accept",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"protocol": "tcp",
|
||||||
|
"port": 81,
|
||||||
|
"action": "drop"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -84,30 +84,39 @@
|
|||||||
"localIp" : null,
|
"localIp" : null,
|
||||||
"localPort" : 51180,
|
"localPort" : 51180,
|
||||||
"node" : "00000000-0000-0000-0000-000000000102",
|
"node" : "00000000-0000-0000-0000-000000000102",
|
||||||
|
"parent" : null,
|
||||||
"protocol" : "udp",
|
"protocol" : "udp",
|
||||||
"queued" : 0,
|
"queued" : 0,
|
||||||
"remoteIp" : "192.168.0.10",
|
"remoteIp" : "192.168.0.10",
|
||||||
"remotePort" : 5000
|
"remotePort" : 5000,
|
||||||
|
"session" : null,
|
||||||
|
"tcpState" : null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : "00000000-0000-0000-0000-000000000003",
|
"id" : "00000000-0000-0000-0000-000000000003",
|
||||||
"localIp" : "192.168.0.30",
|
"localIp" : "192.168.0.30",
|
||||||
"localPort" : 7000,
|
"localPort" : 7000,
|
||||||
"node" : "00000000-0000-0000-0000-000000000103",
|
"node" : "00000000-0000-0000-0000-000000000103",
|
||||||
|
"parent" : null,
|
||||||
"protocol" : "udp",
|
"protocol" : "udp",
|
||||||
"queued" : 0,
|
"queued" : 0,
|
||||||
"remoteIp" : null,
|
"remoteIp" : null,
|
||||||
"remotePort" : null
|
"remotePort" : null,
|
||||||
|
"session" : null,
|
||||||
|
"tcpState" : null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : "00000000-0000-0000-0000-000000000004",
|
"id" : "00000000-0000-0000-0000-000000000004",
|
||||||
"localIp" : null,
|
"localIp" : null,
|
||||||
"localPort" : 5000,
|
"localPort" : 5000,
|
||||||
"node" : "00000000-0000-0000-0000-000000000101",
|
"node" : "00000000-0000-0000-0000-000000000101",
|
||||||
|
"parent" : null,
|
||||||
"protocol" : "udp",
|
"protocol" : "udp",
|
||||||
"queued" : 0,
|
"queued" : 0,
|
||||||
"remoteIp" : null,
|
"remoteIp" : null,
|
||||||
"remotePort" : null
|
"remotePort" : null,
|
||||||
|
"session" : null,
|
||||||
|
"tcpState" : null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"switchTables" : {
|
"switchTables" : {
|
||||||
|
|||||||
@@ -96,40 +96,52 @@
|
|||||||
"localIp" : null,
|
"localIp" : null,
|
||||||
"localPort" : 5000,
|
"localPort" : 5000,
|
||||||
"node" : "00000000-0000-0000-0000-000000000203",
|
"node" : "00000000-0000-0000-0000-000000000203",
|
||||||
|
"parent" : null,
|
||||||
"protocol" : "udp",
|
"protocol" : "udp",
|
||||||
"queued" : 0,
|
"queued" : 0,
|
||||||
"remoteIp" : "10.0.0.2",
|
"remoteIp" : "10.0.0.2",
|
||||||
"remotePort" : 54899
|
"remotePort" : 54899,
|
||||||
|
"session" : null,
|
||||||
|
"tcpState" : null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : "00000000-0000-0000-0000-000000000002",
|
"id" : "00000000-0000-0000-0000-000000000002",
|
||||||
"localIp" : null,
|
"localIp" : null,
|
||||||
"localPort" : 5001,
|
"localPort" : 5001,
|
||||||
"node" : "00000000-0000-0000-0000-000000000203",
|
"node" : "00000000-0000-0000-0000-000000000203",
|
||||||
|
"parent" : null,
|
||||||
"protocol" : "udp",
|
"protocol" : "udp",
|
||||||
"queued" : 0,
|
"queued" : 0,
|
||||||
"remoteIp" : null,
|
"remoteIp" : null,
|
||||||
"remotePort" : null
|
"remotePort" : null,
|
||||||
|
"session" : null,
|
||||||
|
"tcpState" : null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : "00000000-0000-0000-0000-000000000003",
|
"id" : "00000000-0000-0000-0000-000000000003",
|
||||||
"localIp" : null,
|
"localIp" : null,
|
||||||
"localPort" : 54899,
|
"localPort" : 54899,
|
||||||
"node" : "00000000-0000-0000-0000-000000000201",
|
"node" : "00000000-0000-0000-0000-000000000201",
|
||||||
|
"parent" : null,
|
||||||
"protocol" : "udp",
|
"protocol" : "udp",
|
||||||
"queued" : 0,
|
"queued" : 0,
|
||||||
"remoteIp" : "20.0.0.2",
|
"remoteIp" : "20.0.0.2",
|
||||||
"remotePort" : 5000
|
"remotePort" : 5000,
|
||||||
|
"session" : null,
|
||||||
|
"tcpState" : null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : "00000000-0000-0000-0000-000000000004",
|
"id" : "00000000-0000-0000-0000-000000000004",
|
||||||
"localIp" : null,
|
"localIp" : null,
|
||||||
"localPort" : 5000,
|
"localPort" : 5000,
|
||||||
"node" : "00000000-0000-0000-0000-000000000202",
|
"node" : "00000000-0000-0000-0000-000000000202",
|
||||||
|
"parent" : null,
|
||||||
"protocol" : "udp",
|
"protocol" : "udp",
|
||||||
"queued" : 0,
|
"queued" : 0,
|
||||||
"remoteIp" : "10.0.0.2",
|
"remoteIp" : "10.0.0.2",
|
||||||
"remotePort" : 54899
|
"remotePort" : 54899,
|
||||||
|
"session" : null,
|
||||||
|
"tcpState" : null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"switchTables" : {
|
"switchTables" : {
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ npm test # vergleicht mit den geschriebenen *.swift.json
|
|||||||
`sat`, `speed`; UDP (seit F2): `bindUdp` (Knoten, optional Port, lokale
|
`sat`, `speed`; UDP (seit F2): `bindUdp` (Knoten, optional Port, lokale
|
||||||
Adresse, Gegenstelle; Ergebnis die Socket-ID), `sendUdp` (Socket, Text,
|
Adresse, Gegenstelle; Ergebnis die Socket-ID), `sendUdp` (Socket, Text,
|
||||||
optional Ziel und Port), `receiveUdp` (Socket, optional Wartezeit),
|
optional Ziel und Port), `receiveUdp` (Socket, optional Wartezeit),
|
||||||
`closeSocket`. Das Ergebnis enthält die Sockets mit der Länge ihrer
|
`closeSocket`; TCP: `openTcpServer`, `openTcpClient`, `connectTcp`
|
||||||
Warteschlange.
|
(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
|
## Korpus
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ struct ScenarioStep: Decodable {
|
|||||||
// UDP: a socket is named by the id bindUdp returned.
|
// UDP: a socket is named by the id bindUdp returned.
|
||||||
let port: UInt16?; let localIp: String?; let remoteIp: String?; let remotePort: UInt16?
|
let port: UInt16?; let localIp: String?; let remoteIp: String?; let remotePort: UInt16?
|
||||||
let socket: String?; let payload: String?; let timeout: UInt64?
|
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 {
|
struct Scenario: Decodable {
|
||||||
let seed: UInt64
|
let seed: UInt64
|
||||||
@@ -245,6 +247,28 @@ func run(_ scenario: Scenario) -> JSON {
|
|||||||
"payload": String(decoding: item.datagram.payload, as: UTF8.self), "at": item.receivedAtMilliseconds,
|
"payload": String(decoding: item.datagram.payload, as: UTF8.self), "at": item.receivedAtMilliseconds,
|
||||||
]
|
]
|
||||||
} ?? NSNull())
|
} ?? 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":
|
case "closeSocket":
|
||||||
engine.closeSocket(socketID: uuid(step.socket!))
|
engine.closeSocket(socketID: uuid(step.socket!))
|
||||||
results.append(engine.state.socketsByID[uuid(step.socket!)] == nil)
|
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,
|
"localIp": socket.localIPAddress ?? NSNull(), "localPort": socket.localPort,
|
||||||
"remoteIp": socket.remoteIPAddress ?? NSNull(), "remotePort": socket.remotePort.map { $0 as Any } ?? NSNull(),
|
"remoteIp": socket.remoteIPAddress ?? NSNull(), "remotePort": socket.remotePort.map { $0 as Any } ?? NSNull(),
|
||||||
"queued": state.udpReceiveQueuesBySocketID[socket.id]?.count ?? 0,
|
"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
|
"firewallDecisions": state.firewallDecisions.map { decision -> JSON in
|
||||||
|
|||||||
Reference in New Issue
Block a user