|
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
|
|
type RuntimeOutput,
|
|
|
|
|
type ScheduledEventKind,
|
|
|
|
|
} from '../src/index.js';
|
|
|
|
|
import { device, link, pc, startedEngine, uuid } from './fixtures.js';
|
|
|
|
|
import { device, letFramesArrive, link, pc, startedEngine, uuid } from './fixtures.js';
|
|
|
|
|
|
|
|
|
|
const marker = (label: string): ScheduledEventKind => ({ kind: 'parityMarker', label });
|
|
|
|
|
|
|
|
|
@@ -274,6 +274,29 @@ describe('event queue and virtual clock', () => {
|
|
|
|
|
expect(engine.state.packetTraces.find((trace) => trace.layer === 'physical' && trace.operation === 'sent')?.timeMilliseconds).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('forgets the send threads on stop', () => {
|
|
|
|
|
const [firstId, firstPortId, secondId, secondPortId] = [0x29, 0x2a, 0x2b, 0x2c].map(uuid) as [string, string, string, string];
|
|
|
|
|
const input = {
|
|
|
|
|
nodes: [pc(firstId, firstPortId), pc(secondId, secondPortId)],
|
|
|
|
|
links: [link(firstId, firstPortId, secondId, secondPortId)],
|
|
|
|
|
deviceConfigurations: new Map([
|
|
|
|
|
[firstId, device('10.6.0.1')],
|
|
|
|
|
[secondId, device('10.6.0.2')],
|
|
|
|
|
]),
|
|
|
|
|
};
|
|
|
|
|
const engine = startedEngine(input);
|
|
|
|
|
// The UDP thread waits for ARP on .99 when the simulation stops.
|
|
|
|
|
engine.sendUdp(engine.bindUdpSocket(firstId, { remoteIPAddress: '10.6.0.99', remotePort: 9 })!, new TextEncoder().encode('lost'));
|
|
|
|
|
|
|
|
|
|
engine.handle({ type: 'stop' });
|
|
|
|
|
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
|
|
|
|
|
const receiver = engine.bindUdpSocket(secondId, { localPort: 9 })!;
|
|
|
|
|
engine.sendUdp(engine.bindUdpSocket(firstId, { remoteIPAddress: '10.6.0.2', remotePort: 9 })!, new TextEncoder().encode('found'));
|
|
|
|
|
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds).toBe(0);
|
|
|
|
|
expect(new TextDecoder().decode(engine.receiveUdp(receiver, 1_000)?.datagram.payload)).toBe('found');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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({
|
|
|
|
@@ -319,7 +342,7 @@ describe('UDP sockets', () => {
|
|
|
|
|
expect(engine.bindUdpSocket(targetNodeId, { localPort: 9000 })).toBeUndefined();
|
|
|
|
|
|
|
|
|
|
engine.sendUdp(sender!, new TextEncoder().encode('hello'));
|
|
|
|
|
const received = engine.receiveUdp(receiver!);
|
|
|
|
|
const received = engine.receiveUdp(receiver!, 1_000);
|
|
|
|
|
expect(text(received?.datagram.payload)).toBe('hello');
|
|
|
|
|
expect(received?.senderIPAddress).toBe('192.168.5.10');
|
|
|
|
|
|
|
|
|
@@ -327,6 +350,36 @@ describe('UDP sockets', () => {
|
|
|
|
|
expect(engine.bindUdpSocket(targetNodeId, { localPort: 9000 })).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns from a send at once, the datagram arrives as the clock advances (swiftSends: when it has arrived)', () => {
|
|
|
|
|
const arrival = (swiftSends: boolean) => {
|
|
|
|
|
const [sourceNodeId, sourcePortId, targetNodeId, targetPortId] = [0x15c1, 0x15c2, 0x15c3, 0x15c4].map(uuid) as [string, string, string, string];
|
|
|
|
|
const engine = new NetworkRuntimeEngine(14, undefined, { swiftSends });
|
|
|
|
|
engine.handle({
|
|
|
|
|
type: 'start',
|
|
|
|
|
snapshot: topologySnapshot({
|
|
|
|
|
nodes: [pc(sourceNodeId, sourcePortId), pc(targetNodeId, targetPortId)],
|
|
|
|
|
links: [link(sourceNodeId, sourcePortId, targetNodeId, targetPortId)],
|
|
|
|
|
deviceConfigurations: new Map([
|
|
|
|
|
[sourceNodeId, device('192.168.6.10')],
|
|
|
|
|
[targetNodeId, device('192.168.6.20')],
|
|
|
|
|
]),
|
|
|
|
|
}),
|
|
|
|
|
seed: 14,
|
|
|
|
|
initialTimeMilliseconds: 0,
|
|
|
|
|
});
|
|
|
|
|
const receiver = engine.bindUdpSocket(targetNodeId, { localPort: 9000 })!;
|
|
|
|
|
const sender = engine.bindUdpSocket(sourceNodeId, { remoteIPAddress: '192.168.6.20', remotePort: 9000 })!;
|
|
|
|
|
engine.sendUdp(sender, new TextEncoder().encode('hello'));
|
|
|
|
|
const returnedAt = engine.state.currentTimeMilliseconds;
|
|
|
|
|
const waiting = engine.state.udpReceiveQueuesBySocketId.get(receiver)?.length ?? 0;
|
|
|
|
|
// ARP there and back, then the datagram: three cables of 55 ms.
|
|
|
|
|
return { returnedAt, waiting, receivedAt: engine.receiveUdp(receiver, 1_000)?.receivedAtMilliseconds };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(arrival(false)).toEqual({ returnedAt: 0, waiting: 0, receivedAt: 165 });
|
|
|
|
|
expect(arrival(true)).toEqual({ returnedAt: 165, waiting: 1, receivedAt: 165 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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')]]) };
|
|
|
|
@@ -366,13 +419,13 @@ describe('UDP sockets', () => {
|
|
|
|
|
|
|
|
|
|
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.receiveUdp(receiverA, 1_000)?.senderIPAddress).toBe('10.0.0.1');
|
|
|
|
|
expect(engine.receiveUdp(receiverB, 1_000)?.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');
|
|
|
|
|
expect(text(engine.receiveUdp(sender, 1_000)?.datagram.payload)).toBe('reply');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('advances the virtual clock for a receive timeout and returns nothing', () => {
|
|
|
|
@@ -621,12 +674,15 @@ describe('switch', () => {
|
|
|
|
|
engine.state.packetTraces.filter((trace) => trace.nodeId === switchId && trace.detail === detail);
|
|
|
|
|
|
|
|
|
|
engine.sendEthernetFrame(hostAId, hostAPortId, frame(macA, '02:00:00:00:00:FE'));
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
expect(switchTraces('switch unknown unicast flooding')).toHaveLength(2);
|
|
|
|
|
|
|
|
|
|
engine.sendEthernetFrame(hostBId, hostBPortId, frame(macB, macA));
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
expect(switchTraces('switch learned unicast forwarding')).toHaveLength(1);
|
|
|
|
|
|
|
|
|
|
engine.sendEthernetFrame(hostAId, hostAPortId, frame(macA, macA));
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
expect(switchTraces('switch filtered destination on incoming port').length).toBeGreaterThan(0);
|
|
|
|
|
expect(engine.switchSatEntries(switchId)).toHaveLength(2);
|
|
|
|
|
|
|
|
|
@@ -777,6 +833,7 @@ describe('packet loss and trace retention', () => {
|
|
|
|
|
|
|
|
|
|
engine.setGlobalPacketLossEnabled(true);
|
|
|
|
|
engine.sendEthernetFrame(firstNodeId, firstPortId, frame);
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
|
|
|
|
|
expect(engine.state.globalPacketLossEnabled).toBe(true);
|
|
|
|
|
expect(engine.state.arpCachesByNodeId.get(secondNodeId)?.size ?? 0).toBe(0);
|
|
|
|
@@ -792,6 +849,7 @@ describe('packet loss and trace retention', () => {
|
|
|
|
|
|
|
|
|
|
engine.setGlobalPacketLossEnabled(false);
|
|
|
|
|
engine.sendEthernetFrame(firstNodeId, firstPortId, frame);
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
|
|
|
|
|
expect(engine.state.globalPacketLossEnabled).toBe(false);
|
|
|
|
|
expect(engine.state.arpCachesByNodeId.get(secondNodeId)?.get('10.0.0.1')?.macAddress).toBe(
|
|
|
|
@@ -864,6 +922,9 @@ describe('TCP sockets', () => {
|
|
|
|
|
expect(engine.connectTcp(clientSocketId)).toBe(true);
|
|
|
|
|
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
|
|
|
|
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('ESTABLISHED');
|
|
|
|
|
// connect returns with the SYN+ACK; the client's ACK is still on its way.
|
|
|
|
|
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('SYN_RCVD');
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
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);
|
|
|
|
@@ -885,6 +946,42 @@ describe('TCP sockets', () => {
|
|
|
|
|
expect(payloadLengths).toEqual([1_460, 1_460, 81]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("sends TCP in a thread of its own: UDP waiting for ARP does not hold a connection up", () => {
|
|
|
|
|
const { engine, clientId, serverId } = pair(0x1731, 0x1732, '10.24.0');
|
|
|
|
|
engine.openTcpServerSocket(serverId, 80);
|
|
|
|
|
engine.sendUdp(engine.bindUdpSocket(clientId, { remoteIPAddress: '10.24.0.99', remotePort: 9 })!, new TextEncoder().encode('nobody'));
|
|
|
|
|
const clientSocketId = engine.openTcpClientSocket(clientId, '10.24.0.2', 80)!;
|
|
|
|
|
|
|
|
|
|
expect(engine.connectTcp(clientSocketId)).toBe(true);
|
|
|
|
|
// ARP for the server (behind the one for .99 on the cable), SYN and SYN+ACK: four cables of 55 ms.
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds).toBe(55 + 4 * 55);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("holds the TCP thread while ARP waits: a connection's SYN waits behind a SYN+ACK to no one", () => {
|
|
|
|
|
const { engine, clientId, serverId } = pair(0x17e1, 0x17e2, '10.25.0');
|
|
|
|
|
engine.openTcpServerSocket(serverId, 80);
|
|
|
|
|
engine.openTcpServerSocket(clientId, 81);
|
|
|
|
|
// A SYN to the client's port 81 from .99, which no card has: the client's answer waits for ARP.
|
|
|
|
|
engine.sendIPv4Packet(serverId, {
|
|
|
|
|
identity: engine.allocatePacketIdentity(),
|
|
|
|
|
senderIPAddress: '10.25.0.99',
|
|
|
|
|
receiverIPAddress: '10.25.0.1',
|
|
|
|
|
timeToLive: 64,
|
|
|
|
|
protocol: 'tcp',
|
|
|
|
|
payload: {
|
|
|
|
|
kind: 'tcp',
|
|
|
|
|
segment: { sourcePort: 5000, destinationPort: 81, sequenceNumber: 1, acknowledgementNumber: 0, flags: TCP_FLAG.synchronize, payload: new Uint8Array() },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
engine.handle({ type: 'advance', toMilliseconds: 200 });
|
|
|
|
|
const clientSocketId = engine.openTcpClientSocket(clientId, '10.25.0.2', 80)!;
|
|
|
|
|
|
|
|
|
|
expect(engine.connectTcp(clientSocketId)).toBe(true);
|
|
|
|
|
// The SYN arrived after ARP there and back (165 ms); ARP for .99 gives up two
|
|
|
|
|
// round trips later, then the client's SYN and the server's SYN+ACK.
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds).toBe(165 + 2 * 2_750 + 2 * 55);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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')]]) });
|
|
|
|
@@ -932,6 +1029,7 @@ describe('TCP sockets', () => {
|
|
|
|
|
const serverSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
|
|
|
|
|
|
|
|
|
|
engine.closeTcpSocket(clientSocketId);
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
// Java's socket thread leaves listen on the FIN and closes without the application.
|
|
|
|
|
const serverStates = engine.state.packetTraces
|
|
|
|
|
.filter((trace) => trace.nodeId === serverId && trace.detail?.startsWith('TCP ') === true && trace.detail.includes(' -> '))
|
|
|
|
@@ -971,8 +1069,10 @@ describe('TCP sockets', () => {
|
|
|
|
|
engine.openTcpServerSocket(serverId, 80);
|
|
|
|
|
const client = engine.openTcpClientSocket(clientId, '10.23.0.2', 80)!;
|
|
|
|
|
expect(engine.connectTcp(client)).toBe(true);
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
const closedAt = engine.state.currentTimeMilliseconds;
|
|
|
|
|
engine.closeTcpSocket(client);
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
// The server answers the FIN with its ACK and, closing by itself, its FIN at once.
|
|
|
|
|
const physical = (nodeId: string, operation: string) =>
|
|
|
|
|
engine.state.packetTraces
|
|
|
|
@@ -1013,6 +1113,7 @@ describe('TCP sockets', () => {
|
|
|
|
|
const clientSocketId = engine.openTcpClientSocket(clientId, '10.21.0.2', 9090)!;
|
|
|
|
|
expect(engine.connectTcp(clientSocketId)).toBe(true);
|
|
|
|
|
engine.closeTcpSocket(clientSocketId);
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
|
|
|
|
|
|
|
|
|
|
const localPort = engine.state.socketsById.get(clientSocketId)!.localPort;
|
|
|
|
@@ -1059,6 +1160,7 @@ describe('TCP sockets', () => {
|
|
|
|
|
// the listener's successor 3 (made when the first is accepted), the second client 4.
|
|
|
|
|
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
|
|
|
|
|
expect(engine.connectTcp(engine.openTcpClientSocket(firstId, '10.22.0.3', 80)!)).toBe(true);
|
|
|
|
|
letFramesArrive(engine);
|
|
|
|
|
expect(engine.connectTcp(engine.openTcpClientSocket(secondId, '10.22.0.3', 80)!)).toBe(true);
|
|
|
|
|
|
|
|
|
|
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)!;
|
|
|
|
@@ -1423,7 +1525,7 @@ describe('DNS', () => {
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
return { engine, client: hosts[0]!.id };
|
|
|
|
|
return { engine, client: hosts[0]!.id, server: hosts[1]!.id };
|
|
|
|
|
};
|
|
|
|
|
const queries = (engine: NetworkRuntimeEngine) =>
|
|
|
|
|
engine.state.packetTraces.filter((trace) => trace.detail?.startsWith('DNS query') === true).map((trace) => trace.detail);
|
|
|
|
@@ -1516,14 +1618,55 @@ describe('DNS', () => {
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds - called).toBe(4 * 110);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("answers behind a datagram of its own whose ARP fails: Java's UDP thread sends one after the other", () => {
|
|
|
|
|
const { engine, client, server } = lan({ '10.30.0.2': ['web.filius.de A 10.30.0.9'] });
|
|
|
|
|
const socket = engine.bindUdpSocket(server, { remoteIPAddress: '10.30.0.99', remotePort: 9 })!;
|
|
|
|
|
const called = engine.state.currentTimeMilliseconds;
|
|
|
|
|
engine.sendUdp(socket, new TextEncoder().encode('nobody'));
|
|
|
|
|
expect(engine.resolveDnsAddress(client, 'web.filius.de')).toEqual({ type: 'resolved', address: '10.30.0.9' });
|
|
|
|
|
// The answer leaves when ARP has given up on .99 (two round trips), and takes two cables through the switch.
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds - called).toBe(2 * 2_750 + 110);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lets the senders that wait take their turns in the order they came, before the thread takes the next', () => {
|
|
|
|
|
const { engine, client, server } = lan({ '10.30.0.2': ['web.filius.de A 10.30.0.9'] });
|
|
|
|
|
const receiver = engine.bindUdpSocket(client, { localPort: 9 })!;
|
|
|
|
|
// The server's UDP thread waits for ARP on .99 from 0 to 5500 ms.
|
|
|
|
|
engine.sendUdp(engine.bindUdpSocket(server, { remoteIPAddress: '10.30.0.99', remotePort: 9 })!, new TextEncoder().encode('nobody'));
|
|
|
|
|
// A query from .98, which no card has, reaches the server at 330 ms; its answer waits its turn.
|
|
|
|
|
const query = encodeDnsMessage({
|
|
|
|
|
id: 1,
|
|
|
|
|
queryResponse: 0,
|
|
|
|
|
responseCode: 0,
|
|
|
|
|
questions: [{ name: 'web.filius.de.', type: 'A', questionClass: 'IN' }],
|
|
|
|
|
answers: [],
|
|
|
|
|
authority: [],
|
|
|
|
|
additional: [],
|
|
|
|
|
});
|
|
|
|
|
engine.sendIPv4Packet(client, {
|
|
|
|
|
identity: engine.allocatePacketIdentity(),
|
|
|
|
|
senderIPAddress: '10.30.0.98',
|
|
|
|
|
receiverIPAddress: '10.30.0.2',
|
|
|
|
|
timeToLive: 64,
|
|
|
|
|
protocol: 'udp',
|
|
|
|
|
payload: { kind: 'udp', datagram: { sourcePort: 5000, destinationPort: 53, payload: new TextEncoder().encode(query) } },
|
|
|
|
|
});
|
|
|
|
|
engine.handle({ type: 'advance', toMilliseconds: 400 });
|
|
|
|
|
|
|
|
|
|
// As with a fair lock (Java's is not): the send returns when the datagram
|
|
|
|
|
// in hand is out; the answer to .98 goes before it and waits for ARP in turn.
|
|
|
|
|
engine.sendUdp(engine.bindUdpSocket(server, { remoteIPAddress: '10.30.0.1', remotePort: 9 })!, new TextEncoder().encode('third'));
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds).toBe(2 * 2_750);
|
|
|
|
|
expect(engine.receiveUdp(receiver, 10_000)?.receivedAtMilliseconds).toBe(4 * 2_750 + 110);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('gives up after five round trips when no server answers, and closes its socket', () => {
|
|
|
|
|
const { engine, client } = lan({ '10.30.0.2': [] });
|
|
|
|
|
const called = engine.state.currentTimeMilliseconds;
|
|
|
|
|
// 10.30.0.3 is there, but no DNS server runs on it.
|
|
|
|
|
expect(engine.resolveDns(client, 'web.filius.de', 'A', '10.30.0.3')).toEqual({ type: 'timedOut' });
|
|
|
|
|
// Java's five round trips; before them the engine's send still blocks for
|
|
|
|
|
// ARP and the query's way to .3 (Java waits from handing it down — the ARP step).
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds - called).toBe(3 * 110 + 5 * 2_750);
|
|
|
|
|
// Java's five round trips, from handing the query down: the send returns at once.
|
|
|
|
|
expect(engine.state.currentTimeMilliseconds - called).toBe(5 * 2_750);
|
|
|
|
|
expect([...engine.state.socketsById.values()].filter((socket) => socket.nodeId === client)).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|