// A computer's firewall window in the room, on FILIUS' example // email_komplett with a firewall installed on its web server: its view (the // three boxes, the rules as the table shows them), its actions, its log as // the window's lines (also over a stop), and what a run leaves in the // project. The firewall's steps against Java's window are // packages/core/test/firewallWindow.test.ts. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import * as Y from 'yjs'; import { DIRECTLY_CONNECTED_SOURCE_MARKER, firewallRule } from '@mdg-filius/core'; import { addFirewallRule, readProject, setSoftware, writeProject } from '@mdg-filius/doc'; import { openProjectArchive } from '@mdg-filius/fls'; import type { FirewallView, HostMessage } from '@mdg-filius/protocol'; import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js'; const here = dirname(fileURLToPath(import.meta.url)); const corpus = join(here, '../../../corpus/java-examples'); function setUp(name = 'Web-Server', fromFile = false) { let ids = 0; const doc = new Y.Doc(); writeProject( doc, openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'email_komplett.fls'))), () => `00000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`), ); const idOf = (displayName: string) => readProject(doc).graph.nodes.find((node) => node.displayName === displayName)!.id; setSoftware(doc, idOf('Web-Server'), { install: ['firewall'], remove: [] }, 'de', 'me'); // A rule as a file may hold it and the window cannot make: TCP, every port, dropped. if (fromFile) addFirewallRule(doc, idOf('Web-Server'), 'me'); const id = idOf(name); let now = 0; const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 }); let view: FirewallView | null | undefined; const errors: string[] = []; const lines: unknown[] = []; const client = room.connect((message: HostMessage) => { if (message.kind === 'app' && message.app === 'firewall') { lines.length = message.from; lines.push(...message.lines); } if (message.kind === 'view' && message.app === 'firewall') view = message.view as FirewallView | null; if (message.kind === 'error') errors.push(message.code); }); const send = (fields: object) => room.receive(client, { v: 1, ...fields }); send({ kind: 'sub', topics: [`app:${id}/firewall`] }); send({ kind: 'start' }); const run = (milliseconds: number) => { for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) { now += TICK_MILLISECONDS; room.tick(); } }; run(TICK_MILLISECONDS); const act = (action: string, args: object = {}) => send({ kind: 'input', node: id, app: 'firewall', action, args, reqId: action }); return { doc, id, view: () => view, errors, lines, act, run, send, now: () => now }; } describe("a computer's firewall window in the room", () => { it('shows the boxes and the rules, takes its actions, and sends its log as lines', () => { const { view, act, errors, lines, send, run, now } = setUp(); // Active as Java installs it. expect(view()).toEqual({ active: true, icmpAllowed: true, filterUdp: true, rules: [] }); run(1000); act('addRule', { port: 80, protocol: 'tcp', sameNetworkOnly: false }); // A line carries the time of the simulation it was written at. expect(lines).toEqual([{ kind: 'portRuleAdded', port: 80, protocol: 'tcp', timeMilliseconds: now() }]); act('addRule', { port: 53, protocol: 'all', sameNetworkOnly: true }); expect(view()!.rules).toEqual([ { protocol: 'tcp', port: 80, sender: 'all', text: '*/* -> */*; TCP:80 => ACCEPT' }, { protocol: 'all', port: 53, sender: 'sameNetwork', text: '141.99.50.128/255.255.255.128 -> */*; *:53 => ACCEPT' }, ]); act('setIcmpAllowed', { allowed: false }); act('setFilterUdp', { enabled: false }); act('setActive', { active: false }); expect(view()).toMatchObject({ active: false, icmpAllowed: false, filterUdp: false }); act('removeRule', { row: 0 }); expect(view()!.rules.map((rule) => rule.port)).toEqual([53]); expect(errors).toEqual([]); expect(lines).toMatchObject([{ kind: 'portRuleAdded', port: 80, protocol: 'tcp' }, { kind: 'portRuleAdded', port: 53, protocol: 'all' }, { kind: 'portRuleRemoved' }]); // The log outlives a stop and a new start, as the engine keeps it. send({ kind: 'stop' }); send({ kind: 'start' }); act('addRule', { port: 25, protocol: 'tcp', sameNetworkOnly: false }); expect(lines.map((line) => (line as { kind: string }).kind)).toEqual(['portRuleAdded', 'portRuleAdded', 'portRuleRemoved', 'portRuleAdded']); }); it('shows a rule the window cannot make as what it is', () => { const { view } = setUp('Web-Server', true); expect(view()!.rules).toEqual([{ protocol: 'tcp', port: -1, sender: null, text: '*/* -> */*; TCP:* => DROP' }]); }); it('leaves in the project what the run made of the firewall', () => { const { doc, id, act, send } = setUp(); act('addRule', { port: 80, protocol: 'udp', sameNetworkOnly: true }); act('setIcmpAllowed', { allowed: false }); send({ kind: 'stop' }); expect(readProject(doc).firewallConfigurationsByNodeId.get(id)).toMatchObject({ isActive: true, dropIcmp: true, filterUdp: true, rules: [firewallRule({ port: 80, protocol: 'udp', action: 'accept', sourceIPAddress: DIRECTLY_CONNECTED_SOURCE_MARKER })], }); }); it('refuses what the window never hands over', () => { const { act, errors } = setUp(); act('setActive', { active: 'no' }); act('setIcmpAllowed', {}); act('setFilterUdp', { enabled: 1 }); act('addRule', { port: 0, protocol: 'tcp', sameNetworkOnly: false }); act('addRule', { port: 65_536, protocol: 'tcp', sameNetworkOnly: false }); act('addRule', { port: 80, protocol: 'icmp', sameNetworkOnly: false }); act('addRule', { port: 80, protocol: 'tcp' }); act('removeRule', { row: 0 }); act('removeRule', { row: 'x' }); act('restart'); expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction']); }); it('has no window on a computer without a firewall', () => { const { view, act, errors } = setUp('DNS-Server'); expect(view()).toBeNull(); act('setActive', { active: false }); expect(errors).toEqual(['unknownApp']); }); });