// 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, UDP and TCP sockets. The remaining Swift tests (DHCP, // DNS, RIP, NAT, remote link, packet viewer) follow with their protocol // slices. import { describe, expect, it } from 'vitest'; import { EMPTY_TOPOLOGY_SNAPSHOT, ETHERNET_BROADCAST_MAC_ADDRESS, firewallConfiguration, firewallRule, DIRECTLY_CONNECTED_SOURCE_MARKER, forwardingClone, icmpMessage, interfaceKey, JavaCompatibleRandom, NetworkRuntimeEngine, portSnapshot, stableMacAddress, switchConfiguration, TCP_FLAG, TCP_ROUND_TRIP_TIME_MILLISECONDS, tcpFlagDescription, topologySnapshot, traceRetentionPolicy, UNSPECIFIED_MAC_ADDRESS, type EthernetFrame, type EthernetPayload, type FirewallConfiguration, type IPv4Packet, type NodeSnapshot, type RuntimeOutput, type ScheduledEventKind, } from '../src/index.js'; import { device, link, pc, startedEngine, uuid } from './fixtures.js'; const marker = (label: string): ScheduledEventKind => ({ kind: 'parityMarker', label }); function firedKinds(outputs: RuntimeOutput[]): ScheduledEventKind[] { return outputs.flatMap((output) => (output.kind.type === 'fired' ? [output.kind.event.kind] : [])); } function routedTopology() { const sourceNodeId = uuid(0x1451); const sourcePortId = uuid(0x1461); const routerNodeId = uuid(0x1452); const routerPortA = uuid(0x1462); const routerPortB = uuid(0x1463); const targetNodeId = uuid(0x1453); const targetPortId = uuid(0x1464); const router: NodeSnapshot = { id: routerNodeId, kind: 'router', ports: [portSnapshot(routerPortA, 'rt1'), portSnapshot(routerPortB, 'rt2')], }; const snapshot = topologySnapshot({ nodes: [pc(sourceNodeId, sourcePortId), router, pc(targetNodeId, targetPortId)], links: [ link(sourceNodeId, sourcePortId, routerNodeId, routerPortA), link(routerNodeId, routerPortB, targetNodeId, targetPortId), ], deviceConfigurations: new Map([ [sourceNodeId, device('10.0.0.2', { gateway: '10.0.0.1' })], [routerNodeId, device('10.0.0.1')], [targetNodeId, device('20.0.0.2', { gateway: '20.0.0.1' })], ]), interfaceConfigurations: new Map([ [interfaceKey(routerNodeId, routerPortA), { ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' }], [interfaceKey(routerNodeId, routerPortB), { ipAddress: '20.0.0.1', subnetMask: '255.255.255.0' }], ]), }); const engine = new NetworkRuntimeEngine(14); engine.handle({ type: 'start', snapshot, seed: 14, initialTimeMilliseconds: 0 }); return { engine, sourceNodeId, routerNodeId }; } function firewallEngine(configuration: FirewallConfiguration) { const routerId = uuid(0x1802); const portId = uuid(0x1812); const engine = startedEngine( { nodes: [{ id: routerId, kind: 'router', ports: [portSnapshot(portId, 'rt1')] }], links: [], interfaceConfigurations: new Map([ [interfaceKey(routerId, portId), { ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' }], ]), firewallConfigurationsByNodeId: new Map([[routerId, configuration]]), }, 18, ); return { engine, routerId }; } function tcpPacket(identity: number, destinationPort: number, flags: number): IPv4Packet { return { identity, senderIPAddress: '203.0.113.10', receiverIPAddress: '10.0.0.1', timeToLive: 64, protocol: 'tcp', payload: { kind: 'tcp', segment: { sourcePort: 50_000, destinationPort, sequenceNumber: identity, acknowledgementNumber: 0, flags, payload: new Uint8Array(), }, }, }; } describe('event queue and virtual clock', () => { it('orders events by deadline, then by insertion sequence', () => { const engine = new NetworkRuntimeEngine(42); engine.handle({ type: 'start', snapshot: EMPTY_TOPOLOGY_SNAPSHOT, seed: 42, initialTimeMilliseconds: 0 }); engine.handle({ type: 'schedule', deadlineMilliseconds: 100, kind: marker('first-at-100') }); engine.handle({ type: 'schedule', deadlineMilliseconds: 50, kind: marker('at-50') }); engine.handle({ type: 'schedule', deadlineMilliseconds: 100, kind: marker('second-at-100') }); const outputs = engine.handle({ type: 'advance', toMilliseconds: 100 }); expect(firedKinds(outputs)).toEqual([marker('at-50'), marker('first-at-100'), marker('second-at-100')]); expect(engine.state.currentTimeMilliseconds).toBe(100); expect(engine.state.pendingEvents).toEqual([]); }); it('rejects a past time without changing the clock', () => { const engine = new NetworkRuntimeEngine(7); engine.handle({ type: 'start', snapshot: EMPTY_TOPOLOGY_SNAPSHOT, seed: 7, initialTimeMilliseconds: 10 }); engine.handle({ type: 'advance', toMilliseconds: 25 }); const outputs = engine.handle({ type: 'advance', toMilliseconds: 24 }); expect(outputs.map((output) => output.kind)).toEqual([{ type: 'advanceRejectedPastTime' }]); expect(engine.state.currentTimeMilliseconds).toBe(25); }); it('rejects a past deadline without using up a sequence number', () => { const engine = new NetworkRuntimeEngine(8); engine.handle({ type: 'start', snapshot: EMPTY_TOPOLOGY_SNAPSHOT, seed: 8, initialTimeMilliseconds: 25 }); const outputs = engine.handle({ type: 'schedule', deadlineMilliseconds: 24, kind: marker('past') }); expect(outputs.map((output) => output.kind)).toEqual([ { type: 'scheduleRejectedPastTime', deadlineMilliseconds: 24 }, ]); expect(engine.state.pendingEvents).toEqual([]); expect(engine.state.nextEventSequenceNumber).toBe(0); expect(engine.state.currentTimeMilliseconds).toBe(25); }); it('replays a named scenario straight to a stable state', () => { const finalState = NetworkRuntimeEngine.run({ name: 'equal-deadline-order', seed: 1, advanceToMilliseconds: 1_000, scheduledEvents: [ { deadlineMilliseconds: 500, kind: marker('a') }, { deadlineMilliseconds: 500, kind: marker('b') }, ], }); expect(finalState.currentTimeMilliseconds).toBe(1_000); expect(finalState.pendingEvents).toEqual([]); expect(finalState.seed).toBe(1n); }); it('clears transient state on restart and numbers packets from one again', () => { const nodeId = uuid(0x12); const engine = new NetworkRuntimeEngine(12); engine.handle({ type: 'start', snapshot: EMPTY_TOPOLOGY_SNAPSHOT, seed: 12, initialTimeMilliseconds: 0 }); engine.handle({ type: 'schedule', deadlineMilliseconds: 40, kind: marker('stale') }); expect(engine.allocateFrameIdentity()).toBe(1); expect(engine.allocatePacketIdentity()).toBe(1); engine.recordTrace({ packetIdentity: 1, nodeId, direction: 'local', layer: 'application', operation: 'created' }); engine.handle({ type: 'stop' }); engine.handle({ type: 'start', snapshot: EMPTY_TOPOLOGY_SNAPSHOT, seed: 12, initialTimeMilliseconds: 0 }); const state = engine.state; expect(state.pendingEvents).toEqual([]); expect(state.packetTraces).toEqual([]); expect(state.arpCachesByNodeId.size).toBe(0); expect(state.switchForwardingTablesByNodeId.size).toBe(0); expect(state.switchSeenFrameIdentitiesByNodeId.size).toBe(0); expect(state.deliveredIPv4PacketsByNodeId.size).toBe(0); expect(state.icmpObservationsByNodeId.size).toBe(0); expect(state.firewallDecisions).toEqual([]); 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)); }); 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', () => { 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', () => { it('draws the same RIP jitter as java.util.Random', () => { const random = new JavaCompatibleRandom(42); expect(Array.from({ length: 5 }, () => random.nextRipJitterMilliseconds())).toEqual([ 32_475, 25_746, 32_032, 25_679, 28_287, ]); }); it('matches java.util.Random.nextInt for bounded and power-of-two ranges', () => { // Measured on OpenJDK 21 (29.09.2026): new java.util.Random(42), // nextInt(100) three times, then nextInt(16) three times. const random = new JavaCompatibleRandom(42); expect([random.nextInt(100), random.nextInt(100), random.nextInt(100)]).toEqual([30, 63, 48]); expect([random.nextInt(16), random.nextInt(16), random.nextInt(16)]).toEqual([0, 4, 15]); }); }); describe('IPv4 packets', () => { it('keeps the identity and lowers the TTL when forwarding', () => { const packet: IPv4Packet = { identity: 99, senderIPAddress: '192.168.0.10', receiverIPAddress: '192.168.1.20', timeToLive: 2, protocol: 'icmp', payload: { kind: 'icmp', message: icmpMessage('echoRequest', { identifier: 3, sequenceNumber: 4 }) }, }; const forwarded = forwardingClone(packet); const expired = forwardingClone(forwarded); expect(forwarded).toMatchObject({ identity: 99, timeToLive: 1 }); expect(expired).toMatchObject({ identity: 99, timeToLive: 0 }); }); }); describe('Ethernet, ARP and ICMP', () => { it('fills both ARP caches with one request and reply over a direct cable', () => { const sourceNodeId = uuid(0x1401); const sourcePortId = uuid(0x1411); const targetNodeId = uuid(0x1402); const targetPortId = uuid(0x1412); const engine = startedEngine({ nodes: [pc(sourceNodeId, sourcePortId), pc(targetNodeId, targetPortId)], links: [link(sourceNodeId, sourcePortId, targetNodeId, targetPortId)], deviceConfigurations: new Map([ [sourceNodeId, device('192.168.0.10')], [targetNodeId, device('192.168.0.20')], ]), }); const resolved = engine.resolveMacAddress(sourceNodeId, '192.168.0.20'); expect(resolved).toBe(stableMacAddress(targetPortId)); expect(engine.state.arpCachesByNodeId.get(sourceNodeId)?.get('192.168.0.20')?.macAddress).toBe(resolved); expect(engine.state.arpCachesByNodeId.get(targetNodeId)?.get('192.168.0.10')?.macAddress).toBe( stableMacAddress(sourcePortId), ); }); it('pings across a switch with Ethernet, ARP and ICMP packets', () => { const sourceNodeId = uuid(0x1421); const sourcePortId = uuid(0x1431); const switchNodeId = uuid(0x1422); const switchPortA = uuid(0x1432); const switchPortB = uuid(0x1433); const targetNodeId = uuid(0x1423); const targetPortId = uuid(0x1434); const switchNode: NodeSnapshot = { id: switchNodeId, kind: 'networkSwitch', ports: [portSnapshot(switchPortA, 'sw1'), portSnapshot(switchPortB, 'sw2')], }; const engine = startedEngine({ nodes: [pc(sourceNodeId, sourcePortId), switchNode, pc(targetNodeId, targetPortId)], links: [ link(sourceNodeId, sourcePortId, switchNodeId, switchPortA), link(switchNodeId, switchPortB, targetNodeId, targetPortId), ], deviceConfigurations: new Map([ [sourceNodeId, device('192.168.0.10')], [targetNodeId, device('192.168.0.20')], ]), }); const result = engine.sendIcmpEcho(sourceNodeId, '192.168.0.20', { identifier: 14, sequenceNumber: 1 }); expect(result.type).toBe('delivered'); expect(engine.state.icmpObservationsByNodeId.get(sourceNodeId)?.at(-1)?.message.kind).toBe('echoReply'); expect(engine.state.switchForwardingTablesByNodeId.get(switchNodeId)?.size).toBe(2); expect(engine.state.packetTraces.some((trace) => trace.detail === 'ARP request for 192.168.0.20')).toBe(true); expect( engine.state.packetTraces.some((trace) => trace.packetIdentity !== undefined && trace.layer === 'network'), ).toBe(true); }); it('answers an expired TTL at the router with time exceeded and the original packet', () => { const topology = routedTopology(); const result = topology.engine.sendIcmpEcho(topology.sourceNodeId, '20.0.0.2', { timeToLive: 1, identifier: 140, sequenceNumber: 1, }); expect(result).toMatchObject({ type: 'icmpError', kind: 'timeExceeded' }); const observation = topology.engine.state.icmpObservationsByNodeId.get(topology.sourceNodeId)?.at(-1); expect(observation?.message.kind).toBe('timeExceeded'); expect(observation?.message.embeddedOriginalPacket?.timeToLive).toBe(1); expect(observation?.message.embeddedOriginalPacket?.receiverIPAddress).toBe('20.0.0.2'); }); it('keeps the packet identity and lowers the TTL when a router forwards', () => { const topology = routedTopology(); const result = topology.engine.sendIcmpEcho(topology.sourceNodeId, '20.0.0.2', { timeToLive: 8, identifier: 141, sequenceNumber: 1, }); expect(result.type).toBe('delivered'); const forwardingTrace = topology.engine.state.packetTraces.find( (trace) => trace.nodeId === topology.routerNodeId && trace.operation === 'forwarded' && trace.detail === 'TTL decremented', ); expect(forwardingTrace?.packetIdentity).toBeDefined(); expect(forwardingTrace?.beforeHeaders.find((field) => field.name === 'ttl')?.value).toBe('8'); expect(forwardingTrace?.afterHeaders.find((field) => field.name === 'ttl')?.value).toBe('7'); }); it('reports network unreachable when the router has no route', () => { const topology = routedTopology(); const snapshot = topology.engine.state.topologySnapshot; const sourceToRouter = snapshot.links.find( (candidate) => candidate.sourceNodeId === topology.sourceNodeId || candidate.targetNodeId === topology.sourceNodeId, ); const engine = new NetworkRuntimeEngine(14); engine.handle({ type: 'start', snapshot: topologySnapshot({ nodes: snapshot.nodes, links: sourceToRouter === undefined ? [] : [sourceToRouter], deviceConfigurations: snapshot.deviceConfigurations, interfaceConfigurations: snapshot.interfaceConfigurations, manualRoutesByNodeId: snapshot.manualRoutesByNodeId, }), seed: 14, initialTimeMilliseconds: 0, }); const result = engine.sendIcmpEcho(topology.sourceNodeId, '30.0.0.2', { identifier: 142, sequenceNumber: 1 }); expect(result).toMatchObject({ type: 'icmpError', kind: 'destinationNetworkUnreachable' }); }); it('reports host unreachable when a connected target never answers ARP', () => { const sourceNodeId = uuid(0x1481); const sourcePortId = uuid(0x1491); const engine = startedEngine({ nodes: [pc(sourceNodeId, sourcePortId)], links: [], deviceConfigurations: new Map([[sourceNodeId, device('192.168.50.10')]]), }); const result = engine.sendIcmpEcho(sourceNodeId, '192.168.50.99', { identifier: 143, sequenceNumber: 1 }); expect(result).toMatchObject({ type: 'icmpError', kind: 'destinationHostUnreachable' }); }); }); describe('switch', () => { it('learns, floods, forwards, filters and ages its address table', () => { const hostAId = uuid(0x1a01); const hostAPortId = uuid(0x1a11); const hostBId = uuid(0x1a02); const hostBPortId = uuid(0x1a12); const hostCId = uuid(0x1a03); const hostCPortId = uuid(0x1a13); const switchId = uuid(0x1a04); const switchPorts = [1, 2, 3].map((index) => portSnapshot(uuid(0x1a20 + index), `sw${index}`)); const engine = startedEngine( { nodes: [ pc(hostAId, hostAPortId), pc(hostBId, hostBPortId), pc(hostCId, hostCPortId), { id: switchId, kind: 'networkSwitch', ports: switchPorts }, ], links: [ link(hostAId, hostAPortId, switchId, switchPorts[0]!.id), link(hostBId, hostBPortId, switchId, switchPorts[1]!.id), link(hostCId, hostCPortId, switchId, switchPorts[2]!.id), ], switchConfigurationsByNodeId: new Map([[switchId, switchConfiguration('Classroom', 1_000)]]), }, 20, ); const macA = stableMacAddress(hostAPortId); const macB = stableMacAddress(hostBPortId); const payload: EthernetPayload = { kind: 'arp', packet: { operation: 'request', senderMacAddress: macA, senderIPAddress: '192.0.2.1', targetMacAddress: UNSPECIFIED_MAC_ADDRESS, targetIPAddress: '192.0.2.200', }, }; const frame = (sourceMacAddress: string, destinationMacAddress: string): EthernetFrame => ({ identity: engine.allocateFrameIdentity(), sourceMacAddress, destinationMacAddress, payload, }); const switchTraces = (detail: string) => engine.state.packetTraces.filter((trace) => trace.nodeId === switchId && trace.detail === detail); engine.sendEthernetFrame(hostAId, hostAPortId, frame(macA, '02:00:00:00:00:FE')); expect(switchTraces('switch unknown unicast flooding')).toHaveLength(2); engine.sendEthernetFrame(hostBId, hostBPortId, frame(macB, macA)); expect(switchTraces('switch learned unicast forwarding')).toHaveLength(1); engine.sendEthernetFrame(hostAId, hostAPortId, frame(macA, macA)); expect(switchTraces('switch filtered destination on incoming port').length).toBeGreaterThan(0); expect(engine.switchSatEntries(switchId)).toHaveLength(2); const retention = 1_000; const updateTimes = [...engine.state.switchForwardingUpdatedAtMillisecondsByNodeId.get(switchId)!.values()]; const expirationDeadlines = updateTimes.map((time) => time + retention); const firstExpiration = Math.min(...expirationDeadlines); const lastExpiration = Math.max(...expirationDeadlines); engine.handle({ type: 'advance', toMilliseconds: firstExpiration - 1 }); expect(engine.switchSatEntries(switchId)).toHaveLength(2); engine.handle({ type: 'advance', toMilliseconds: lastExpiration }); expect(engine.switchSatEntries(switchId)).toEqual([]); }); }); describe('firewall', () => { it('accepts when inactive, and an active ICMP drop bypasses the ordered rules', () => { const routerId = uuid(0x1801); const portId = uuid(0x1811); const router: NodeSnapshot = { id: routerId, kind: 'router', ports: [portSnapshot(portId, 'rt1')] }; const packet: IPv4Packet = { identity: 18, senderIPAddress: '203.0.113.10', receiverIPAddress: '10.0.0.1', timeToLive: 64, protocol: 'icmp', payload: { kind: 'icmp', message: icmpMessage('echoRequest') }, }; const interfaces = new Map([ [interfaceKey(routerId, portId), { ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' }], ]); const inactive = startedEngine( { nodes: [router], links: [], interfaceConfigurations: interfaces, firewallConfigurationsByNodeId: new Map([[routerId, firewallConfiguration()]]), }, 18, ); expect(inactive.evaluateFirewall(packet, routerId).accepted).toBe(true); const active = startedEngine( { nodes: [router], links: [], interfaceConfigurations: interfaces, firewallConfigurationsByNodeId: new Map([ [ routerId, firewallConfiguration({ isActive: true, defaultPolicy: 'accept', dropIcmp: true, rules: [firewallRule({ protocol: 'icmp', action: 'accept' })], }), ], ]), }, 18, ); expect(active.evaluateFirewall(packet, routerId).accepted).toBe(false); expect(active.state.firewallDecisions.at(-1)?.ruleIndex).toBeUndefined(); expect(active.state.packetTraces.at(-1)?.operation).toBe('dropped'); }); it('lets the first matching TCP rule win, and established traffic bypasses SYN-only rules', () => { const { engine, routerId } = firewallEngine( firewallConfiguration({ isActive: true, defaultPolicy: 'drop', filterSynSegmentsOnly: true, rules: [ firewallRule({ port: 443, protocol: 'tcp', action: 'accept' }), firewallRule({ port: 443, protocol: 'tcp', action: 'drop' }), ], }), ); const synDecision = engine.evaluateFirewall(tcpPacket(181, 443, TCP_FLAG.synchronize), routerId); expect(synDecision.accepted).toBe(true); expect(synDecision.ruleIndex).toBe(0); expect(engine.evaluateFirewall(tcpPacket(182, 22, TCP_FLAG.acknowledgement), routerId).accepted).toBe(true); }); it('matches a UDP response by its explicit source port and the direct-network marker', () => { const { engine, routerId } = firewallEngine( firewallConfiguration({ isActive: true, defaultPolicy: 'drop', rules: [ firewallRule({ sourceIPAddress: DIRECTLY_CONNECTED_SOURCE_MARKER, destinationIPAddress: '8.8.8.8', destinationSubnetMask: '255.255.255.255', port: 53, protocol: 'udp', action: 'accept', }), ], }), ); const udp = (identity: number, sender: string, receiver: string, sourcePort: number, destinationPort: number): IPv4Packet => ({ identity, senderIPAddress: sender, receiverIPAddress: receiver, timeToLive: 64, protocol: 'udp', payload: { kind: 'udp', datagram: { sourcePort, destinationPort, payload: new Uint8Array() } }, }); expect(engine.evaluateFirewall(udp(183, '10.0.0.20', '8.8.8.8', 50_001, 53), routerId).accepted).toBe(true); expect(engine.evaluateFirewall(udp(184, '8.8.8.8', '10.0.0.20', 53, 50_001), routerId).accepted).toBe(true); }); }); describe('packet loss and trace retention', () => { it('drops every physical delivery while global packet loss is on', () => { const firstNodeId = uuid(0x101); const firstPortId = uuid(0x102); const secondNodeId = uuid(0x103); const secondPortId = uuid(0x104); const engine = startedEngine({ nodes: [pc(firstNodeId, firstPortId), pc(secondNodeId, secondPortId)], links: [link(firstNodeId, firstPortId, secondNodeId, secondPortId)], deviceConfigurations: new Map([ [firstNodeId, device('10.0.0.1')], [secondNodeId, device('10.0.0.2')], ]), }); const frame: EthernetFrame = { identity: engine.allocateFrameIdentity(), sourceMacAddress: stableMacAddress(firstPortId), destinationMacAddress: ETHERNET_BROADCAST_MAC_ADDRESS, payload: { kind: 'arp', packet: { operation: 'request', senderMacAddress: stableMacAddress(firstPortId), senderIPAddress: '10.0.0.1', targetMacAddress: UNSPECIFIED_MAC_ADDRESS, targetIPAddress: '10.0.0.2', }, }, }; engine.setGlobalPacketLossEnabled(true); engine.sendEthernetFrame(firstNodeId, firstPortId, frame); expect(engine.state.globalPacketLossEnabled).toBe(true); expect(engine.state.arpCachesByNodeId.get(secondNodeId)?.size ?? 0).toBe(0); expect( engine.state.packetTraces.some( (trace) => trace.frameIdentity === frame.identity && trace.nodeId === secondNodeId && trace.operation === 'dropped' && trace.detail === 'global packet-loss simulation', ), ).toBe(true); engine.setGlobalPacketLossEnabled(false); engine.sendEthernetFrame(firstNodeId, firstPortId, frame); expect(engine.state.globalPacketLossEnabled).toBe(false); expect(engine.state.arpCachesByNodeId.get(secondNodeId)?.get('10.0.0.1')?.macAddress).toBe( stableMacAddress(firstPortId), ); }); it('resets packet loss across stop and restart', () => { const engine = new NetworkRuntimeEngine(42); engine.handle({ type: 'start', snapshot: EMPTY_TOPOLOGY_SNAPSHOT, seed: 42, initialTimeMilliseconds: 0 }); engine.setGlobalPacketLossEnabled(true); expect(engine.state.globalPacketLossEnabled).toBe(true); engine.handle({ type: 'stop' }); expect(engine.state.globalPacketLossEnabled).toBe(false); engine.handle({ type: 'start', snapshot: EMPTY_TOPOLOGY_SNAPSHOT, seed: 42, initialTimeMilliseconds: 0 }); expect(engine.state.globalPacketLossEnabled).toBe(false); }); it('evicts the oldest trace events in configured batches', () => { const nodeId = uuid(0x2001); const interfaceId = uuid(0x2002); const engine = new NetworkRuntimeEngine(20, traceRetentionPolicy(3, 2)); for (let identity = 1; identity <= 6; identity += 1) { engine.recordTrace({ frameIdentity: identity, nodeId, interfaceId, direction: 'outbound', layer: 'dataLink', operation: 'sent', detail: `trace-${identity}`, }); } expect(engine.state.packetTraces.map((trace) => trace.id)).toEqual([5, 6]); expect(engine.state.packetTraces.map((trace) => trace.detail)).toEqual(['trace-5', 'trace-6']); expect(engine.state.discardedPacketTraceCount).toBe(4); 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'); }); });