feat(core): UDP sockets in the simulation core
engine.ts ports the UDP part of Swift's "UDP sockets and Router RIP": a socket bound to a given port or to a free one drawn from 49152–65534 with Java's random numbers on Swift's seed; sending, also as a limited broadcast on every interface (or the bound one) with TTL 1; delivery into the queue of every socket on the destination port, which then answers to the last sender; receiving, with a timeout that advances the virtual clock; closing. Stopping forgets sockets and queues. RIP stays not ported; Swift's DHCP handling of a datagram has nothing to do until DHCP is in the snapshot. The oracle gets the steps bindUdp, sendUdp, receiveUdp and closeSocket on both sides and reports the sockets. Two new scenarios — a LAN, and two networks through a router with a UDP firewall rule and a broadcast that does not cross it — match the Swift engine trace for trace, the random ports included. Swift's four UDP engine tests are ported, with a test for stopping. 11 mutation probes were all caught (one only after the stop test was added). 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/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/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, 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). Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, TCP, DHCP, DNS, RIP, Remote Link, WLAN |
|
||||
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
|
||||
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
|
||||
| `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/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/oracle.test.ts`, `test/scenarios/` | — | neu: 5 Szenarien, Ergebnis mit dem Swift-Kern abgeglichen (`tools/swift-oracle`) |
|
||||
| `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/routeTable.test.ts` | `TopologyJavaRouteTableTests.swift` (8 von 12 Tests) | übertragen; Geometrie- und Reducer-Tests folgen mit Editor und Dokument |
|
||||
|
||||
## packages/fls
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
// Calls that block in Java (resolving an address, waiting for an echo reply)
|
||||
// advance the virtual clock inside the call, exactly as the Swift port does.
|
||||
//
|
||||
// "UDP sockets and Router RIP" (lines ~4472–5187, the
|
||||
// UDP sockets; RIP not yet)
|
||||
//
|
||||
// Not ported yet — each place throws NotPortedError instead of guessing:
|
||||
// gateway NAT, UDP and TCP delivery, RIP routing, remote links.
|
||||
// gateway NAT, TCP delivery, RIP routing, remote links. DHCP is not in the
|
||||
// snapshot yet, so Swift's DHCP handling of a datagram has nothing to do.
|
||||
|
||||
import { compareIds, type Id } from '../ids.js';
|
||||
import { isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js';
|
||||
@@ -47,6 +51,7 @@ import {
|
||||
type IcmpMessage,
|
||||
type IcmpMessageKind,
|
||||
type IPv4Packet,
|
||||
type UdpDatagram,
|
||||
} from './packets.js';
|
||||
import { JavaCompatibleRandom } from './random.js';
|
||||
import { routeTableRows, type RouteRow } from './routeTable.js';
|
||||
@@ -225,6 +230,38 @@ export interface FirewallDecision {
|
||||
|
||||
export type RuntimePhase = 'stopped' | 'running';
|
||||
|
||||
export type SocketProtocol = 'udp' | 'tcp';
|
||||
|
||||
/** Swift's TopologyRuntimeSocketRecord (UDP so far). */
|
||||
export interface SocketRecord {
|
||||
readonly id: Id;
|
||||
readonly nodeId: Id;
|
||||
readonly protocol: SocketProtocol;
|
||||
readonly localIPAddress: string | undefined;
|
||||
/** 0…65535 */
|
||||
readonly localPort: number;
|
||||
remoteIPAddress: string | undefined;
|
||||
remotePort: number | undefined;
|
||||
}
|
||||
|
||||
/** A datagram waiting in a UDP socket's queue. */
|
||||
export interface UdpReceivedDatagram {
|
||||
readonly senderIPAddress: string;
|
||||
readonly receiverIPAddress: string;
|
||||
readonly datagram: UdpDatagram;
|
||||
readonly receivedAtMilliseconds: number;
|
||||
}
|
||||
|
||||
export const UDP_EPHEMERAL_PORT_LOWER_BOUND = 49_152;
|
||||
export const UDP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE = 65_535;
|
||||
export const RIP_DESTINATION_PORT = 520;
|
||||
export const RIP_SOURCE_PORT = 521;
|
||||
|
||||
/** Swift's deterministicSocketID: the sequence number as the UUID's last twelve hex digits. */
|
||||
export function deterministicSocketId(sequence: number): Id {
|
||||
return `00000000-0000-0000-0000-${sequence.toString(16).toUpperCase().padStart(12, '0')}`;
|
||||
}
|
||||
|
||||
export interface RuntimeState {
|
||||
phase: RuntimePhase;
|
||||
currentTimeMilliseconds: number;
|
||||
@@ -242,6 +279,9 @@ export interface RuntimeState {
|
||||
deliveredIPv4PacketsByNodeId: Map<Id, DeliveredIPv4Packet[]>;
|
||||
icmpObservationsByNodeId: Map<Id, IcmpObservation[]>;
|
||||
firewallDecisions: FirewallDecision[];
|
||||
socketsById: Map<Id, SocketRecord>;
|
||||
udpReceiveQueuesBySocketId: Map<Id, UdpReceivedDatagram[]>;
|
||||
nextSocketSequenceNumber: number;
|
||||
nextEventSequenceNumber: number;
|
||||
nextFrameIdentity: number;
|
||||
nextPacketIdentity: number;
|
||||
@@ -266,6 +306,9 @@ function initialState(seed: bigint): RuntimeState {
|
||||
deliveredIPv4PacketsByNodeId: new Map(),
|
||||
icmpObservationsByNodeId: new Map(),
|
||||
firewallDecisions: [],
|
||||
socketsById: new Map(),
|
||||
udpReceiveQueuesBySocketId: new Map(),
|
||||
nextSocketSequenceNumber: 1,
|
||||
nextEventSequenceNumber: 0,
|
||||
nextFrameIdentity: 1,
|
||||
nextPacketIdentity: 1,
|
||||
@@ -398,6 +441,8 @@ export class NetworkRuntimeEngine {
|
||||
state.switchSeenFrameIdentitiesByNodeId.clear();
|
||||
state.deliveredIPv4PacketsByNodeId.clear();
|
||||
state.icmpObservationsByNodeId.clear();
|
||||
state.socketsById.clear();
|
||||
state.udpReceiveQueuesBySocketId.clear();
|
||||
state.firewallDecisions = [];
|
||||
return [this.output({ type: 'stopped' })];
|
||||
}
|
||||
@@ -603,6 +648,209 @@ export class NetworkRuntimeEngine {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── UDP sockets ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A UDP socket on `nodeId`: on `localPort`, else on a free port drawn at
|
||||
* random from the ephemeral range; undefined while stopped, when the port
|
||||
* is taken or the node has no interface with `localIPAddress`.
|
||||
*/
|
||||
bindUdpSocket(
|
||||
nodeId: Id,
|
||||
options: { localPort?: number; localIPAddress?: string; remoteIPAddress?: string; remotePort?: number } = {},
|
||||
): Id | undefined {
|
||||
const state = this.stateValue;
|
||||
if (state.phase !== 'running') return undefined;
|
||||
let reservedPort: number;
|
||||
if (options.localPort !== undefined) {
|
||||
if (this.isUdpPortReserved(nodeId, options.localPort)) return undefined;
|
||||
reservedPort = options.localPort;
|
||||
} else {
|
||||
const port = this.reserveFreeUdpPort(nodeId);
|
||||
if (port === undefined) return undefined;
|
||||
reservedPort = port;
|
||||
}
|
||||
if (
|
||||
options.localIPAddress !== undefined &&
|
||||
!this.networkInterfaces(nodeId).some((networkInterface) => networkInterface.ipAddress === options.localIPAddress)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const id = deterministicSocketId(state.nextSocketSequenceNumber);
|
||||
state.nextSocketSequenceNumber += 1;
|
||||
state.socketsById.set(id, {
|
||||
id,
|
||||
nodeId,
|
||||
protocol: 'udp',
|
||||
localIPAddress: options.localIPAddress,
|
||||
localPort: reservedPort,
|
||||
remoteIPAddress: options.remoteIPAddress,
|
||||
remotePort: options.remotePort,
|
||||
});
|
||||
state.udpReceiveQueuesBySocketId.set(id, []);
|
||||
return id;
|
||||
}
|
||||
|
||||
private isUdpPortReserved(nodeId: Id, port: number): boolean {
|
||||
// Swift also counts a gateway's NAT mappings; gateways do not run here yet.
|
||||
for (const socket of this.stateValue.socketsById.values()) {
|
||||
if (socket.nodeId === nodeId && socket.protocol === 'udp' && socket.localPort === port) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private reserveFreeUdpPort(nodeId: Id): number | undefined {
|
||||
const span = UDP_EPHEMERAL_PORT_UPPER_BOUND_EXCLUSIVE - UDP_EPHEMERAL_PORT_LOWER_BOUND;
|
||||
for (let attempt = 0; attempt < span; attempt++) {
|
||||
const port = UDP_EPHEMERAL_PORT_LOWER_BOUND + this.udpPortRandom.nextInt(span);
|
||||
if (!this.isUdpPortReserved(nodeId, port)) return port;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
closeSocket(socketId: Id): void {
|
||||
if (this.stateValue.socketsById.get(socketId)?.protocol === 'tcp') throw new NotPortedError('TCP sockets');
|
||||
this.stateValue.socketsById.delete(socketId);
|
||||
this.stateValue.udpReceiveQueuesBySocketId.delete(socketId);
|
||||
}
|
||||
|
||||
/** Forgets a socket without closing it (Swift's discardSocket; TCP children once TCP is ported). */
|
||||
discardSocket(socketId: Id): void {
|
||||
this.stateValue.socketsById.delete(socketId);
|
||||
this.stateValue.udpReceiveQueuesBySocketId.delete(socketId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The oldest datagram in the socket's queue; with a timeout, the clock
|
||||
* advances up to it when the queue is empty, as a blocking receive in Java.
|
||||
*/
|
||||
receiveUdp(socketId: Id, timeoutMilliseconds = 0): UdpReceivedDatagram | undefined {
|
||||
const next = () => this.stateValue.udpReceiveQueuesBySocketId.get(socketId)?.shift();
|
||||
const datagram = next();
|
||||
if (datagram !== undefined) return datagram;
|
||||
if (timeoutMilliseconds <= 0 || this.stateValue.phase !== 'running') return undefined;
|
||||
const deadline = this.stateValue.currentTimeMilliseconds + timeoutMilliseconds;
|
||||
if (!Number.isSafeInteger(deadline)) return undefined;
|
||||
this.advance(deadline);
|
||||
return next();
|
||||
}
|
||||
|
||||
/** Sends from a UDP socket to its remote end, or to the given one. */
|
||||
sendUdp(
|
||||
socketId: Id,
|
||||
payload: Uint8Array,
|
||||
destination: { ipAddress?: string; port?: number } = {},
|
||||
): IPv4DeliveryResult | undefined {
|
||||
const socket = this.stateValue.socketsById.get(socketId);
|
||||
if (socket === undefined || socket.protocol !== 'udp') return undefined;
|
||||
const destinationIPAddress = destination.ipAddress ?? socket.remoteIPAddress;
|
||||
const destinationPort = destination.port ?? socket.remotePort;
|
||||
if (destinationIPAddress === undefined || destinationPort === undefined) return undefined;
|
||||
const interfaces = this.networkInterfaces(socket.nodeId);
|
||||
if (interfaces.length === 0) return undefined;
|
||||
const boundInterface =
|
||||
socket.localIPAddress === undefined
|
||||
? undefined
|
||||
: interfaces.find((networkInterface) => networkInterface.ipAddress === socket.localIPAddress);
|
||||
const packetIdentity = this.allocatePacketIdentity();
|
||||
const datagram: UdpDatagram = { sourcePort: socket.localPort, destinationPort, payload };
|
||||
const traceSent = (networkInterface: NetworkInterface, identity: number) =>
|
||||
this.recordTrace({
|
||||
packetIdentity: identity,
|
||||
nodeId: socket.nodeId,
|
||||
interfaceId: networkInterface.portId,
|
||||
direction: 'outbound',
|
||||
layer: 'transport',
|
||||
operation: 'sent',
|
||||
afterHeaders: udpHeaders(datagram),
|
||||
});
|
||||
|
||||
if (destinationIPAddress === LIMITED_BROADCAST_IP_ADDRESS) {
|
||||
for (const networkInterface of boundInterface === undefined ? interfaces : [boundInterface]) {
|
||||
const packet: IPv4Packet = {
|
||||
identity: packetIdentity,
|
||||
senderIPAddress: networkInterface.ipAddress,
|
||||
receiverIPAddress: destinationIPAddress,
|
||||
timeToLive: 1,
|
||||
protocol: 'udp',
|
||||
payload: { kind: 'udp', datagram },
|
||||
};
|
||||
traceSent(networkInterface, packet.identity);
|
||||
this.sendLimitedBroadcastPacket(socket.nodeId, networkInterface, packet);
|
||||
}
|
||||
return { type: 'delivered', packetIdentity, nodeId: socket.nodeId };
|
||||
}
|
||||
|
||||
let networkInterface: NetworkInterface;
|
||||
if (boundInterface !== undefined) {
|
||||
networkInterface = boundInterface;
|
||||
} else {
|
||||
const route = this.routeForPacket(socket.nodeId, destinationIPAddress);
|
||||
networkInterface = route.type === 'egress' ? route.interface : interfaces[0]!;
|
||||
}
|
||||
const packet: IPv4Packet = {
|
||||
identity: packetIdentity,
|
||||
senderIPAddress: networkInterface.ipAddress,
|
||||
receiverIPAddress: destinationIPAddress,
|
||||
timeToLive: 64,
|
||||
protocol: 'udp',
|
||||
payload: { kind: 'udp', datagram },
|
||||
};
|
||||
traceSent(networkInterface, packet.identity);
|
||||
return this.sendIPv4Packet(socket.nodeId, packet, networkInterface.portId);
|
||||
}
|
||||
|
||||
private sendLimitedBroadcastPacket(fromNodeId: Id, networkInterface: NetworkInterface, packet: IPv4Packet): void {
|
||||
const broadcastPacket: IPv4Packet = {
|
||||
...packet,
|
||||
senderIPAddress: networkInterface.ipAddress,
|
||||
receiverIPAddress: LIMITED_BROADCAST_IP_ADDRESS,
|
||||
timeToLive: 1,
|
||||
};
|
||||
const frame: EthernetFrame = {
|
||||
identity: this.allocateFrameIdentity(),
|
||||
sourceMacAddress: networkInterface.macAddress,
|
||||
destinationMacAddress: ETHERNET_BROADCAST_MAC_ADDRESS,
|
||||
payload: { kind: 'ipv4', packet: broadcastPacket },
|
||||
};
|
||||
this.recordIPv4Send(broadcastPacket, frame, fromNodeId, networkInterface.portId, 'sent');
|
||||
this.sendEthernetFrame(fromNodeId, networkInterface.portId, frame);
|
||||
}
|
||||
|
||||
/** Into the queue of every UDP socket on the destination port, which then answers to the sender. */
|
||||
private deliverUdp(packet: IPv4Packet, datagram: UdpDatagram, nodeId: Id, networkInterface: NetworkInterface): void {
|
||||
if (
|
||||
datagram.destinationPort === RIP_DESTINATION_PORT &&
|
||||
datagram.sourcePort === RIP_SOURCE_PORT &&
|
||||
this.isRipEnabledRouter(nodeId)
|
||||
) {
|
||||
throw new NotPortedError('RIP advertisements');
|
||||
}
|
||||
const state = this.stateValue;
|
||||
const matching = [...state.socketsById.values()]
|
||||
.filter((socket) => socket.nodeId === nodeId && socket.protocol === 'udp' && socket.localPort === datagram.destinationPort)
|
||||
.sort((lhs, rhs) => compareIds(lhs.id, rhs.id));
|
||||
for (const socket of matching) {
|
||||
getOrCreate(state.udpReceiveQueuesBySocketId, socket.id, () => []).push({
|
||||
senderIPAddress: packet.senderIPAddress,
|
||||
receiverIPAddress: packet.receiverIPAddress,
|
||||
datagram,
|
||||
receivedAtMilliseconds: state.currentTimeMilliseconds,
|
||||
});
|
||||
state.socketsById.set(socket.id, { ...socket, remoteIPAddress: packet.senderIPAddress, remotePort: datagram.sourcePort });
|
||||
}
|
||||
this.recordTrace({
|
||||
packetIdentity: packet.identity,
|
||||
nodeId,
|
||||
interfaceId: networkInterface.portId,
|
||||
direction: 'local',
|
||||
layer: 'transport',
|
||||
operation: matching.length === 0 ? 'dropped' : 'accepted',
|
||||
beforeHeaders: udpHeaders(datagram),
|
||||
detail: matching.length === 0 ? 'UDP destination port unbound' : 'UDP queued',
|
||||
});
|
||||
}
|
||||
|
||||
// ── Later slices hook in here ────────────────────────────────────────────
|
||||
|
||||
private initializeRipState(): void {
|
||||
@@ -1527,7 +1775,8 @@ export class NetworkRuntimeEngine {
|
||||
interfaceId: networkInterface.portId,
|
||||
packet,
|
||||
});
|
||||
throw new NotPortedError('UDP delivery');
|
||||
this.deliverUdp(packet, payload.datagram, nodeId, networkInterface);
|
||||
return;
|
||||
case 'tcp':
|
||||
getOrCreate(this.stateValue.deliveredIPv4PacketsByNodeId, nodeId, () => []).push({
|
||||
nodeId,
|
||||
@@ -1930,6 +2179,14 @@ export function ipv4Headers(packet: IPv4Packet): HeaderField[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function udpHeaders(datagram: UdpDatagram): HeaderField[] {
|
||||
return [
|
||||
{ name: 'sourcePort', value: String(datagram.sourcePort) },
|
||||
{ name: 'destinationPort', value: String(datagram.destinationPort) },
|
||||
{ name: 'payloadLength', value: String(datagram.payload.length) },
|
||||
];
|
||||
}
|
||||
|
||||
export function icmpHeaders(message: IcmpMessage): HeaderField[] {
|
||||
return [
|
||||
{ name: 'kind', value: message.kind },
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Ported from filius-ipad ios/FiliusPadTests/TopologyNetworkRuntimeEngineTests.swift:
|
||||
// the event queue, the virtual clock, Java-compatible randomness, restart,
|
||||
// Ethernet/switch/ARP/IPv4/ICMP, routing errors, the firewall, packet loss
|
||||
// and trace retention. The remaining Swift tests (UDP, TCP, DHCP, DNS, RIP,
|
||||
// NAT, remote link, packet viewer) follow with their protocol slices.
|
||||
// and trace retention, UDP sockets. The remaining Swift tests (TCP, DHCP,
|
||||
// DNS, RIP, NAT, remote link, packet viewer) follow with their protocol
|
||||
// slices.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
@@ -191,6 +192,114 @@ describe('event queue and virtual clock', () => {
|
||||
expect(engine.allocateFrameIdentity()).toBe(1);
|
||||
expect(engine.allocatePacketIdentity()).toBe(1);
|
||||
});
|
||||
|
||||
it('forgets UDP sockets on stop, binds none while stopped, and numbers them from one again', () => {
|
||||
const nodeId = uuid(0x13);
|
||||
const portId = uuid(0x14);
|
||||
const input = { nodes: [pc(nodeId, portId)], links: [], deviceConfigurations: new Map([[nodeId, device('10.2.0.1')]]) };
|
||||
const engine = startedEngine(input);
|
||||
expect(engine.bindUdpSocket(nodeId, { localPort: 7 })).toBe(uuid(1));
|
||||
expect(engine.bindUdpSocket(nodeId)).toBe(uuid(2));
|
||||
|
||||
engine.handle({ type: 'stop' });
|
||||
expect(engine.state.socketsById.size).toBe(0);
|
||||
expect(engine.state.udpReceiveQueuesBySocketId.size).toBe(0);
|
||||
expect(engine.bindUdpSocket(nodeId, { localPort: 7 })).toBeUndefined();
|
||||
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
|
||||
expect(engine.bindUdpSocket(nodeId, { localPort: 7 })).toBe(uuid(1));
|
||||
});
|
||||
});
|
||||
|
||||
describe('UDP sockets', () => {
|
||||
const text = (payload: Uint8Array | undefined) => new TextDecoder().decode(payload ?? new Uint8Array());
|
||||
|
||||
it('binds, delivers and releases a port', () => {
|
||||
const [sourceNodeId, sourcePortId, targetNodeId, targetPortId] = [0x15b1, 0x15b2, 0x15b3, 0x15b4].map(uuid) as [string, string, string, string];
|
||||
const engine = startedEngine({
|
||||
nodes: [pc(sourceNodeId, sourcePortId), pc(targetNodeId, targetPortId)],
|
||||
links: [link(sourceNodeId, sourcePortId, targetNodeId, targetPortId)],
|
||||
deviceConfigurations: new Map([
|
||||
[sourceNodeId, device('192.168.5.10')],
|
||||
[targetNodeId, device('192.168.5.20')],
|
||||
]),
|
||||
});
|
||||
const automatic = engine.bindUdpSocket(targetNodeId);
|
||||
const automaticPort = automatic === undefined ? undefined : engine.state.socketsById.get(automatic)?.localPort;
|
||||
expect(automaticPort).toBeGreaterThanOrEqual(49_152);
|
||||
expect(automaticPort).toBeLessThan(65_535);
|
||||
|
||||
const receiver = engine.bindUdpSocket(targetNodeId, { localPort: 9000 });
|
||||
const sender = engine.bindUdpSocket(sourceNodeId, { localPort: 8000, remoteIPAddress: '192.168.5.20', remotePort: 9000 });
|
||||
expect(receiver).toBeDefined();
|
||||
expect(sender).toBeDefined();
|
||||
expect(engine.bindUdpSocket(targetNodeId, { localPort: 9000 })).toBeUndefined();
|
||||
|
||||
engine.sendUdp(sender!, new TextEncoder().encode('hello'));
|
||||
const received = engine.receiveUdp(receiver!);
|
||||
expect(text(received?.datagram.payload)).toBe('hello');
|
||||
expect(received?.senderIPAddress).toBe('192.168.5.10');
|
||||
|
||||
engine.closeSocket(receiver!);
|
||||
expect(engine.bindUdpSocket(targetNodeId, { localPort: 9000 })).toBeDefined();
|
||||
});
|
||||
|
||||
it('draws ephemeral ports without disturbing the seeded RIP jitter', () => {
|
||||
const nodeId = uuid(0x15f1);
|
||||
const input = { nodes: [pc(nodeId, uuid(0x15f2))], links: [], deviceConfigurations: new Map([[nodeId, device('192.168.10.10')]]) };
|
||||
const socketEngine = startedEngine(input);
|
||||
const ripOnlyEngine = startedEngine(input);
|
||||
|
||||
expect(socketEngine.bindUdpSocket(nodeId)).toBeDefined();
|
||||
expect(socketEngine.nextRipJitterMilliseconds()).toBe(ripOnlyEngine.nextRipJitterMilliseconds());
|
||||
});
|
||||
|
||||
it('broadcasts on every interface, and the receiver replies to its last peer', () => {
|
||||
const [routerId, routerPortA, routerPortB, pcAId, pcAPort, pcBId, pcBPort] = [0x15e1, 0x15e2, 0x15e3, 0x15e4, 0x15e5, 0x15e6, 0x15e7].map(
|
||||
uuid,
|
||||
) as [string, string, string, string, string, string, string];
|
||||
const engine = startedEngine(
|
||||
{
|
||||
nodes: [
|
||||
{ id: routerId, kind: 'router', ports: [portSnapshot(routerPortA, 'rt1'), portSnapshot(routerPortB, 'rt2')] },
|
||||
pc(pcAId, pcAPort),
|
||||
pc(pcBId, pcBPort),
|
||||
],
|
||||
links: [link(routerId, routerPortA, pcAId, pcAPort), link(routerId, routerPortB, pcBId, pcBPort)],
|
||||
deviceConfigurations: new Map([
|
||||
[pcAId, device('10.0.0.2')],
|
||||
[pcBId, device('20.0.0.2')],
|
||||
]),
|
||||
interfaceConfigurations: new Map([
|
||||
[interfaceKey(routerId, routerPortA), { ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' }],
|
||||
[interfaceKey(routerId, routerPortB), { ipAddress: '20.0.0.1', subnetMask: '255.255.255.0' }],
|
||||
]),
|
||||
},
|
||||
15,
|
||||
);
|
||||
const sender = engine.bindUdpSocket(routerId, { localPort: 8000 })!;
|
||||
const receiverA = engine.bindUdpSocket(pcAId, { localPort: 9000 })!;
|
||||
const receiverB = engine.bindUdpSocket(pcBId, { localPort: 9000 })!;
|
||||
|
||||
engine.sendUdp(sender, new TextEncoder().encode('broadcast'), { ipAddress: '255.255.255.255', port: 9000 });
|
||||
|
||||
expect(engine.receiveUdp(receiverA)?.senderIPAddress).toBe('10.0.0.1');
|
||||
expect(engine.receiveUdp(receiverB)?.senderIPAddress).toBe('20.0.0.1');
|
||||
expect(engine.state.socketsById.get(receiverA)?.remoteIPAddress).toBe('10.0.0.1');
|
||||
expect(engine.state.socketsById.get(receiverA)?.remotePort).toBe(8000);
|
||||
|
||||
engine.sendUdp(receiverA, new TextEncoder().encode('reply'));
|
||||
expect(text(engine.receiveUdp(sender)?.datagram.payload)).toBe('reply');
|
||||
});
|
||||
|
||||
it('advances the virtual clock for a receive timeout and returns nothing', () => {
|
||||
const nodeId = uuid(0x15d1);
|
||||
const engine = startedEngine({ nodes: [pc(nodeId, uuid(0x15d2))], links: [], deviceConfigurations: new Map([[nodeId, device('10.1.0.1')]]) });
|
||||
const socketId = engine.bindUdpSocket(nodeId, { localPort: 9999 })!;
|
||||
|
||||
expect(engine.receiveUdp(socketId, 250)).toBeUndefined();
|
||||
expect(engine.state.currentTimeMilliseconds).toBe(250);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java-compatible randomness', () => {
|
||||
|
||||
@@ -67,6 +67,14 @@ interface Scenario {
|
||||
to?: number;
|
||||
enabled?: boolean;
|
||||
percent?: number;
|
||||
// UDP: a socket is named by the id bindUdp returned.
|
||||
port?: number;
|
||||
localIp?: string;
|
||||
remoteIp?: string;
|
||||
remotePort?: number;
|
||||
socket?: string;
|
||||
payload?: string;
|
||||
timeout?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -239,6 +247,44 @@ function run(scenario: Scenario): Record<string, unknown> {
|
||||
engine.setSimulationSpeed(step.percent!);
|
||||
results.push(engine.simulationSpeed.percent);
|
||||
break;
|
||||
case 'bindUdp':
|
||||
results.push(
|
||||
engine.bindUdpSocket(step.node!, {
|
||||
...(step.port === undefined ? {} : { localPort: step.port }),
|
||||
...(step.localIp === undefined ? {} : { localIPAddress: step.localIp }),
|
||||
...(step.remoteIp === undefined ? {} : { remoteIPAddress: step.remoteIp }),
|
||||
...(step.remotePort === undefined ? {} : { remotePort: step.remotePort }),
|
||||
}) ?? null,
|
||||
);
|
||||
break;
|
||||
case 'sendUdp': {
|
||||
const result = engine.sendUdp(step.socket!, new TextEncoder().encode(step.payload ?? ''), {
|
||||
...(step.target === undefined ? {} : { ipAddress: step.target }),
|
||||
...(step.port === undefined ? {} : { port: step.port }),
|
||||
});
|
||||
results.push(result === undefined ? null : deliveryJson(result));
|
||||
break;
|
||||
}
|
||||
case 'receiveUdp': {
|
||||
const item = engine.receiveUdp(step.socket!, step.timeout ?? 0);
|
||||
results.push(
|
||||
item === undefined
|
||||
? null
|
||||
: {
|
||||
sender: item.senderIPAddress,
|
||||
receiver: item.receiverIPAddress,
|
||||
sourcePort: item.datagram.sourcePort,
|
||||
destinationPort: item.datagram.destinationPort,
|
||||
payload: new TextDecoder().decode(item.datagram.payload),
|
||||
at: item.receivedAtMilliseconds,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'closeSocket':
|
||||
engine.closeSocket(step.socket!);
|
||||
results.push(!engine.state.socketsById.has(step.socket!));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown step ${step.op}`);
|
||||
}
|
||||
@@ -259,6 +305,18 @@ function run(scenario: Scenario): Record<string, unknown> {
|
||||
Object.fromEntries([...table].map(([mac, port]) => [mac, upper(port)])),
|
||||
),
|
||||
icmpObservations: mapOf(state.icmpObservationsByNodeId, (items) => items.map(observationJson)),
|
||||
sockets: [...state.socketsById.values()]
|
||||
.sort((lhs, rhs) => (upper(lhs.id) < upper(rhs.id) ? -1 : 1))
|
||||
.map((socket) => ({
|
||||
id: upper(socket.id),
|
||||
node: upper(socket.nodeId),
|
||||
protocol: socket.protocol,
|
||||
localIp: socket.localIPAddress ?? null,
|
||||
localPort: socket.localPort,
|
||||
remoteIp: socket.remoteIPAddress ?? null,
|
||||
remotePort: socket.remotePort ?? null,
|
||||
queued: state.udpReceiveQueuesBySocketId.get(socket.id)?.length ?? 0,
|
||||
})),
|
||||
firewallDecisions: state.firewallDecisions.map((decision) => ({
|
||||
packet: decision.packetIdentity,
|
||||
node: upper(decision.nodeId),
|
||||
|
||||
@@ -80,6 +80,9 @@
|
||||
"packet" : 7,
|
||||
"type" : "delivered"
|
||||
}
|
||||
],
|
||||
"sockets" : [
|
||||
|
||||
],
|
||||
"switchTables" : {
|
||||
"00000000-0000-0000-0000-000000000405" : {
|
||||
|
||||
@@ -151,6 +151,9 @@
|
||||
"type" : "delivered"
|
||||
},
|
||||
"02:00:00:00:02:22"
|
||||
],
|
||||
"sockets" : [
|
||||
|
||||
],
|
||||
"switchTables" : {
|
||||
|
||||
|
||||
@@ -204,6 +204,9 @@
|
||||
"packet" : 13,
|
||||
"type" : "delivered"
|
||||
}
|
||||
],
|
||||
"sockets" : [
|
||||
|
||||
],
|
||||
"switchTables" : {
|
||||
|
||||
|
||||
@@ -795,6 +795,9 @@
|
||||
"packet" : 3,
|
||||
"type" : "delivered"
|
||||
}
|
||||
],
|
||||
"sockets" : [
|
||||
|
||||
],
|
||||
"switchTables" : {
|
||||
"00000000-0000-0000-0000-000000000503" : {
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
"packet" : 12,
|
||||
"type" : "delivered"
|
||||
}
|
||||
],
|
||||
"sockets" : [
|
||||
|
||||
],
|
||||
"switchTables" : {
|
||||
"00000000-0000-0000-0000-000000000104" : {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"seed": 21,
|
||||
"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": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000101",
|
||||
"port": 5000
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000102"
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000101",
|
||||
"port": 5000
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000101",
|
||||
"localIp": "10.9.9.9"
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000103",
|
||||
"port": 7000,
|
||||
"localIp": "192.168.0.30"
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000002",
|
||||
"target": "192.168.0.10",
|
||||
"port": 5000,
|
||||
"payload": "hallo"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001",
|
||||
"timeout": 200
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001",
|
||||
"payload": "antwort"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000002",
|
||||
"timeout": 200
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000002"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000002",
|
||||
"timeout": 500
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000002",
|
||||
"target": "192.168.0.30",
|
||||
"port": 6000,
|
||||
"payload": "niemand"
|
||||
},
|
||||
{
|
||||
"op": "advance",
|
||||
"to": 2000
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000003",
|
||||
"target": "255.255.255.255",
|
||||
"port": 5000,
|
||||
"payload": "an alle"
|
||||
},
|
||||
{
|
||||
"op": "advance",
|
||||
"to": 3000
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000003",
|
||||
"payload": "ohne Ziel"
|
||||
},
|
||||
{
|
||||
"op": "closeSocket",
|
||||
"socket": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001",
|
||||
"target": "192.168.0.20",
|
||||
"port": 5000,
|
||||
"payload": "geschlossen"
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000101",
|
||||
"port": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"seed": 22,
|
||||
"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": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000203",
|
||||
"port": 5000
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000203",
|
||||
"port": 5001
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000201"
|
||||
},
|
||||
{
|
||||
"op": "bindUdp",
|
||||
"node": "00000000-0000-0000-0000-000000000202",
|
||||
"port": 5000
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000003",
|
||||
"target": "20.0.0.2",
|
||||
"port": 5000,
|
||||
"payload": "durch den Router"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001",
|
||||
"timeout": 500
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000003",
|
||||
"target": "20.0.0.2",
|
||||
"port": 5001,
|
||||
"payload": "verboten"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000002",
|
||||
"timeout": 500
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000003",
|
||||
"target": "255.255.255.255",
|
||||
"port": 5000,
|
||||
"payload": "Broadcast"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000004",
|
||||
"timeout": 500
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001",
|
||||
"timeout": 500
|
||||
},
|
||||
{
|
||||
"op": "sendUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000001",
|
||||
"payload": "zurueck"
|
||||
},
|
||||
{
|
||||
"op": "receiveUdp",
|
||||
"socket": "00000000-0000-0000-0000-000000000003",
|
||||
"timeout": 500
|
||||
}
|
||||
],
|
||||
"firewalls": {
|
||||
"00000000-0000-0000-0000-000000000202": {
|
||||
"active": true,
|
||||
"defaultPolicy": "accept",
|
||||
"filterUdp": true,
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "udp",
|
||||
"port": 5001,
|
||||
"action": "drop"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,12 @@ npm test # vergleicht mit den geschriebenen *.swift.json
|
||||
die Tests ohne Docker und ohne Swift laufen.
|
||||
* Vergleich: `packages/core/test/oracle.test.ts`. Er meldet das **erste**
|
||||
abweichende Spurereignis mit Nummer.
|
||||
* Schritte: `ping`, `traceroute`, `resolveMac`, `advance`, `packetLoss`,
|
||||
`sat`, `speed`; UDP (seit F2): `bindUdp` (Knoten, optional Port, lokale
|
||||
Adresse, Gegenstelle; Ergebnis die Socket-ID), `sendUdp` (Socket, Text,
|
||||
optional Ziel und Port), `receiveUdp` (Socket, optional Wartezeit),
|
||||
`closeSocket`. Das Ergebnis enthält die Sockets mit der Länge ihrer
|
||||
Warteschlange.
|
||||
|
||||
## Korpus
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ struct ScenarioStep: Decodable {
|
||||
let from: String?; let node: String?; let target: String?
|
||||
let ttl: UInt8?; let identifier: UInt16?; let sequence: UInt16?; let maxTtl: UInt8?
|
||||
let to: UInt64?; let enabled: Bool?; let percent: Int?
|
||||
// UDP: a socket is named by the id bindUdp returned.
|
||||
let port: UInt16?; let localIp: String?; let remoteIp: String?; let remotePort: UInt16?
|
||||
let socket: String?; let payload: String?; let timeout: UInt64?
|
||||
}
|
||||
struct Scenario: Decodable {
|
||||
let seed: UInt64
|
||||
@@ -222,6 +225,29 @@ func run(_ scenario: Scenario) -> JSON {
|
||||
case "speed":
|
||||
engine.setSimulationSpeed(percent: step.percent!)
|
||||
results.append(engine.simulationSpeed.percent)
|
||||
case "bindUdp":
|
||||
results.append(engine.bindUDPSocket(
|
||||
nodeID: uuid(step.node!), localPort: step.port, localIPAddress: step.localIp,
|
||||
remoteIPAddress: step.remoteIp, remotePort: step.remotePort
|
||||
)?.uuidString ?? NSNull())
|
||||
case "sendUdp":
|
||||
results.append(engine.sendUDP(
|
||||
socketID: uuid(step.socket!), payload: Data((step.payload ?? "").utf8),
|
||||
destinationIPAddress: step.target, destinationPort: step.port
|
||||
).map(delivery) ?? NSNull())
|
||||
case "receiveUdp":
|
||||
let received = step.timeout.map { engine.receiveUDP(socketID: uuid(step.socket!), timeoutMilliseconds: $0) }
|
||||
?? engine.receiveUDP(socketID: uuid(step.socket!))
|
||||
results.append(received.map { item -> JSON in
|
||||
[
|
||||
"sender": item.senderIPAddress, "receiver": item.receiverIPAddress,
|
||||
"sourcePort": item.datagram.sourcePort, "destinationPort": item.datagram.destinationPort,
|
||||
"payload": String(decoding: item.datagram.payload, as: UTF8.self), "at": item.receivedAtMilliseconds,
|
||||
]
|
||||
} ?? NSNull())
|
||||
case "closeSocket":
|
||||
engine.closeSocket(socketID: uuid(step.socket!))
|
||||
results.append(engine.state.socketsByID[uuid(step.socket!)] == nil)
|
||||
default:
|
||||
fatalError("unknown step \(step.op)")
|
||||
}
|
||||
@@ -248,6 +274,14 @@ func run(_ scenario: Scenario) -> JSON {
|
||||
"arp": arp,
|
||||
"switchTables": switchTables,
|
||||
"icmpObservations": observations,
|
||||
"sockets": state.socketsByID.values.sorted { $0.id.uuidString < $1.id.uuidString }.map { socket -> JSON in
|
||||
[
|
||||
"id": socket.id.uuidString, "node": socket.nodeID.uuidString, "protocol": socket.protocolKind.rawValue,
|
||||
"localIp": socket.localIPAddress ?? NSNull(), "localPort": socket.localPort,
|
||||
"remoteIp": socket.remoteIPAddress ?? NSNull(), "remotePort": socket.remotePort.map { $0 as Any } ?? NSNull(),
|
||||
"queued": state.udpReceiveQueuesBySocketID[socket.id]?.count ?? 0,
|
||||
]
|
||||
},
|
||||
"firewallDecisions": state.firewallDecisions.map { decision -> JSON in
|
||||
var json: JSON = [
|
||||
"packet": decision.packetIdentity, "node": decision.nodeID.uuidString, "accepted": decision.accepted,
|
||||
|
||||
Reference in New Issue
Block a user