feat: F4.6g3 a computer's firewall window, measured against Java's with traffic from two networks

The firewall-pc scenario and FirewallWindowRun use Java's window as a person
does and send a ping, a TCP connection and a DNS question from the server's
network and from behind the router after each step. The core adds and
removes port rules (personalFirewall.ts) and writes both into the firewall's
log, which now carries each line's time; the room takes the window's
actions and sends its view and the log as lines; the editor shows Java's
window with its two tabs. Not adopted from Java: any integer as a port,
a port after a space refused without a word, a log one can type into.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 21:18:05 +00:00
co-authored by Claude Opus 5.5
parent 065b42430d
commit 2b85e7432c
25 changed files with 2092 additions and 19 deletions
+129
View File
@@ -0,0 +1,129 @@
// 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']);
});
});