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
+46
View File
@@ -48,7 +48,9 @@ import {
DEFAULT_SPEED_PERCENT,
NetworkRuntimeEngine,
captureRows,
firewallRuleText,
interfaceKey,
portRuleSender,
stableGeneratedMacAddress,
type CapturedFrame,
type CaptureRow,
@@ -73,6 +75,7 @@ import {
type EmailServerView,
type DnsServerView,
type EchoServerView,
type FirewallView,
type SimpleClientView,
type TraceFrame,
type WebServerView,
@@ -463,6 +466,8 @@ export class SimulationRoom {
return this.actInSimpleClient(input);
case 'dnsServer':
return this.actInDnsServer(input);
case 'firewall':
return this.actInFirewall(input);
default:
return 'unknownAction';
}
@@ -498,6 +503,36 @@ export class SimulationRoom {
}
}
/** A computer's firewall window: its three boxes, "Hinzufügen" with the port, the protocol and the senders chosen, "Löschen" of the row chosen. */
private actInFirewall(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const configuration = this.engine.personalFirewall(input.node);
if (configuration === undefined) return 'unknownApp';
const { args } = input;
const set = (fields: Partial<typeof configuration>) => this.engine.setFirewallConfiguration(input.node, { ...configuration, ...fields });
switch (input.action) {
case 'setActive':
if (typeof args.active !== 'boolean') return 'badArgs';
set({ isActive: args.active });
return undefined;
case 'setIcmpAllowed':
if (typeof args.allowed !== 'boolean') return 'badArgs';
set({ dropIcmp: !args.allowed });
return undefined;
case 'setFilterUdp':
if (typeof args.enabled !== 'boolean') return 'badArgs';
set({ filterUdp: args.enabled });
return undefined;
case 'addRule':
if (!isPort(args.port) || args.port === 0 || (args.protocol !== 'tcp' && args.protocol !== 'udp' && args.protocol !== 'all') || typeof args.sameNetworkOnly !== 'boolean') return 'badArgs';
this.engine.addFirewallPortRule(input.node, args.port, args.protocol, args.sameNetworkOnly);
return undefined;
case 'removeRule':
return Number.isInteger(args.row) && this.engine.removeFirewallRule(input.node, args.row as number) ? undefined : 'badArgs';
default:
return 'unknownAction';
}
}
/** The start button with the port typed (Java's window sets the port before and after it starts the server). */
private actInEchoServer(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const server = this.engine.echoServer(input.node);
@@ -680,6 +715,14 @@ export class SimulationRoom {
const view: DnsServerView = { active: server.isActive, recursive: server.recursiveResolutionEnabled, rows: { A: rows('A'), MX: rows('MX'), NS: rows('NS') } };
return view;
}
case 'firewall': {
const configuration = this.engine.personalFirewall(node);
if (configuration === undefined) return null;
const first = this.engine.networkInterfaces(node)[0];
const rules = configuration.rules.map((rule) => ({ protocol: rule.protocol, port: rule.port, sender: portRuleSender(rule) ?? null, text: firewallRuleText(rule, first) }));
const view: FirewallView = { active: configuration.isActive, icmpAllowed: !configuration.dropIcmp, filterUdp: configuration.filterUdp, rules };
return view;
}
case 'echoServer': {
const server = this.engine.echoServer(node);
if (server === undefined) return null;
@@ -725,6 +768,9 @@ export class SimulationRoom {
return this.engine.simpleClient(node);
case 'gnutella':
return this.engine.gnutella(node);
case 'firewall':
// Its log outlives a stop, as the engine keeps it.
return this.engine.personalFirewall(node) === undefined ? undefined : { log: { all: this.engine.firewallLog(node) } };
}
}