feat(editor): F4.5d the firewall in the device sheet
Measured first: SheetRun drives FILIUS' firewall dialog down to its table editor (Swing's own end of an edit). Java's table writes a cell into the rule before it checks it, so that the firewall holds "abc", a mask of 255.0.255.0 or port 0 while the table shows the old value, and reads a port that is no number as all ports. Here nothing the check refuses reaches the rule. - doc: the three switches, the default action, rules (new as Java's, cells by Java's checks: masks of ones then zeros, the directly connected marker as a source only, an empty port for all ports; protocol, action, moved up or down, removed) - editor: the firewall as a page of the router's and home router's sheet, with Java's explanations; the general tab named so (Java: "network interfaces") Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,9 +18,23 @@
|
||||
// " 10.0.0.1"; a phone's keyboard puts a space after a word). A name and an
|
||||
// SSID are taken as typed, spaces included, as Java takes them.
|
||||
|
||||
import { IP_ADDRESS_PATTERN, javaIpCheck } from '@mdg-filius/core';
|
||||
import { DIRECTLY_CONNECTED_SOURCE_MARKER, IP_ADDRESS_PATTERN, javaIpCheck, parseIPv4 } from '@mdg-filius/core';
|
||||
|
||||
export type FieldRule = 'name' | 'ipAddress' | 'ipAddressOrEmpty' | 'subnetMask' | 'ssid' | 'seconds' | 'port' | 'macAddress';
|
||||
export type FieldRule =
|
||||
| 'name'
|
||||
| 'ipAddress'
|
||||
| 'ipAddressOrEmpty'
|
||||
| 'subnetMask'
|
||||
| 'ssid'
|
||||
| 'seconds'
|
||||
| 'port'
|
||||
| 'macAddress'
|
||||
/** A firewall rule's source: empty, an address, or Java's marker for "a directly connected network". */
|
||||
| 'firewallSource'
|
||||
/** A firewall rule's mask: empty, or ones then zeros (EingabenUeberpruefung.isValidSubnetmask). */
|
||||
| 'firewallMask'
|
||||
/** A firewall rule's port: empty for all ports, or musterPort. */
|
||||
| 'portOrEmpty';
|
||||
|
||||
/** What a field's text comes to: the value the device takes, or that it takes none. */
|
||||
export type CheckedField = { readonly value: string } | { readonly invalid: true };
|
||||
@@ -71,6 +85,18 @@ export function checkField(rule: FieldRule, text: string): CheckedField {
|
||||
}
|
||||
case 'port':
|
||||
return PORT_PATTERN.test(text.trim()) ? { value: text.trim() } : INVALID;
|
||||
case 'firewallSource':
|
||||
if (text.trim() === DIRECTLY_CONNECTED_SOURCE_MARKER) return { value: DIRECTLY_CONNECTED_SOURCE_MARKER };
|
||||
return checkField('ipAddressOrEmpty', text);
|
||||
case 'firewallMask': {
|
||||
if (text.trim() === '') return { value: '' };
|
||||
const checked = address(text.trim(), SUBNET_MASK_PATTERN);
|
||||
if ('invalid' in checked) return checked;
|
||||
// Ones, then zeros, at least one one (Java: the 32 bits match "^11*0*$").
|
||||
return /^1+0*$/.test(parseIPv4(checked.value)!.toString(2).padStart(32, '0')) ? checked : INVALID;
|
||||
}
|
||||
case 'portOrEmpty':
|
||||
return text.trim() === '' ? { value: '' } : checkField('port', text);
|
||||
case 'macAddress':
|
||||
// Upper case, as the model keeps MAC addresses (Java keeps them as typed and compares ignoring case).
|
||||
return MAC_ADDRESS_PATTERN.test(text.trim()) ? { value: text.trim().toUpperCase() } : INVALID;
|
||||
|
||||
@@ -24,14 +24,22 @@
|
||||
// second static assignment of a MAC address without a word, and that a home
|
||||
// router keeps any port forwarding row, valid or not (its firmware then
|
||||
// forwards only the valid ones): here a value passes its rule or is not
|
||||
// taken, and the sheet says why.
|
||||
// taken, and the sheet says why. Nor that Java's firewall table writes a
|
||||
// cell into the rule before it checks it, so that a rule holds "abc" or a
|
||||
// mask of 255.0.255.0 while the table shows the old value, and reads a port
|
||||
// that is no number as "all ports".
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import {
|
||||
DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS,
|
||||
defaultSwitchConfiguration,
|
||||
ALL_PORTS,
|
||||
dhcpServerConfiguration,
|
||||
firewallConfiguration,
|
||||
firewallRule,
|
||||
forwardsIPv4ByDefault,
|
||||
type FirewallAction,
|
||||
type FirewallProtocol,
|
||||
type DhcpStaticAssignment,
|
||||
type GatewayPortForwardingRow,
|
||||
type Id,
|
||||
@@ -42,7 +50,7 @@ import { modemPair } from '@mdg-filius/fls';
|
||||
import { connect, type ConnectError } from './commands.js';
|
||||
import { checkField, type CheckedField, type FieldRule } from './fields.js';
|
||||
import { inOrder, ROOTS } from './projectDoc.js';
|
||||
import { toY } from './values.js';
|
||||
import { fromY, toY, type PlainValue } from './values.js';
|
||||
|
||||
const nodes = (doc: Y.Doc) => doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
|
||||
const links = (doc: Y.Doc) => doc.getMap<Y.Map<unknown>>(ROOTS.links);
|
||||
@@ -481,3 +489,98 @@ export function removePortForwarding(doc: Y.Doc, id: Id, index: number, origin:
|
||||
if (list.length === 0) settingsOf(node).delete('portForwarding');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** A router's or home router's firewall, made as Java's constructor leaves it when the device has none in the document. */
|
||||
function firewallOf(node: Y.Map<unknown>): Y.Map<unknown> {
|
||||
let firewall = settingsOf(node).get('firewall') as Y.Map<unknown> | undefined;
|
||||
if (firewall === undefined) {
|
||||
firewall = toY({ ...firewallConfiguration(), rules: [] }) as Y.Map<unknown>;
|
||||
settingsOf(node).set('firewall', firewall);
|
||||
}
|
||||
return firewall;
|
||||
}
|
||||
|
||||
const rulesOf = (node: Y.Map<unknown>) => {
|
||||
const firewall = firewallOf(node);
|
||||
let rules = firewall.get('rules') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
if (rules === undefined) {
|
||||
rules = new Y.Array<Y.Map<unknown>>();
|
||||
firewall.set('rules', rules);
|
||||
}
|
||||
return rules;
|
||||
};
|
||||
|
||||
/** The firewall switched on, dropping ICMP, or dropping only SYN segments (JFirewallDialog's general tab). */
|
||||
export function setFirewallFlag(doc: Y.Doc, id: Id, flag: 'isActive' | 'dropIcmp' | 'filterSynSegmentsOnly', on: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node !== undefined) doc.transact(() => firewallOf(node).set(flag, on), origin);
|
||||
}
|
||||
|
||||
/** What the firewall does when no rule matches. */
|
||||
export function setFirewallDefaultPolicy(doc: Y.Doc, id: Id, policy: FirewallAction, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node !== undefined) doc.transact(() => firewallOf(node).set('defaultPolicy', policy), origin);
|
||||
}
|
||||
|
||||
/** A new rule at the end, as Java's makes it (Firewall.addRule: every field empty, TCP, all ports, drop); its place. */
|
||||
export function addFirewallRule(doc: Y.Doc, id: Id, origin: unknown): number | undefined {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return undefined;
|
||||
let place = 0;
|
||||
doc.transact(() => {
|
||||
const rules = rulesOf(node);
|
||||
rules.push([toY({ ...firewallRule() }) as Y.Map<unknown>]);
|
||||
place = rules.length - 1;
|
||||
}, origin);
|
||||
return place;
|
||||
}
|
||||
|
||||
export type FirewallRuleColumn = 'sourceIPAddress' | 'sourceSubnetMask' | 'destinationIPAddress' | 'destinationSubnetMask' | 'port';
|
||||
export const FIREWALL_RULE_COLUMNS: readonly FirewallRuleColumn[] = ['sourceIPAddress', 'sourceSubnetMask', 'destinationIPAddress', 'destinationSubnetMask', 'port'];
|
||||
|
||||
/** Each text column's rule (FirewallRule.validateExistingAttributes; the marker only as a source). */
|
||||
export const firewallCellRule = (column: FirewallRuleColumn): FieldRule =>
|
||||
column === 'sourceIPAddress' ? 'firewallSource' : column === 'destinationIPAddress' ? 'ipAddressOrEmpty' : column === 'port' ? 'portOrEmpty' : 'firewallMask';
|
||||
|
||||
const ruleAt = (node: Y.Map<unknown>, index: number) => ((settingsOf(node).get('firewall') as Y.Map<unknown> | undefined)?.get('rules') as Y.Array<Y.Map<unknown>> | undefined)?.get(index);
|
||||
|
||||
/** A text cell of a rule; an empty port means all ports. */
|
||||
export function setFirewallRuleCell(doc: Y.Doc, id: Id, index: number, column: FirewallRuleColumn, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField(firewallCellRule(column), text),
|
||||
(node, value) => ruleAt(node, index)?.set(column, column === 'port' ? (value === '' ? ALL_PORTS : Number(value)) : value),
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
/** A rule's protocol or action (Java's lists: *, TCP, UDP; accept, drop). */
|
||||
export function setFirewallRuleChoice(doc: Y.Doc, id: Id, index: number, choice: { readonly protocol: FirewallProtocol } | { readonly action: FirewallAction }, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
const rule = node === undefined ? undefined : ruleAt(node, index);
|
||||
if (rule === undefined) return;
|
||||
doc.transact(() => ('protocol' in choice ? rule.set('protocol', choice.protocol) : rule.set('action', choice.action)), origin);
|
||||
}
|
||||
|
||||
/** Moves a rule one place up (-1) or down (+1); the first goes no higher, the last no lower (Firewall.moveUp, moveDown). */
|
||||
export function moveFirewallRule(doc: Y.Doc, id: Id, index: number, step: -1 | 1, origin: unknown): boolean {
|
||||
const node = nodes(doc).get(id);
|
||||
const rules = node === undefined ? undefined : ((settingsOf(node).get('firewall') as Y.Map<unknown> | undefined)?.get('rules') as Y.Array<Y.Map<unknown>> | undefined);
|
||||
const target = index + step;
|
||||
if (rules === undefined || index < 0 || index >= rules.length || target < 0 || target >= rules.length) return false;
|
||||
doc.transact(() => {
|
||||
// A Yjs type cannot be inserted twice: the rule moves as a copy of its fields.
|
||||
const moved = fromY(rules.get(index)) as PlainValue;
|
||||
rules.delete(index, 1);
|
||||
rules.insert(target, [toY(moved) as Y.Map<unknown>]);
|
||||
}, origin);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function removeFirewallRule(doc: Y.Doc, id: Id, index: number, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
const rules = node === undefined ? undefined : ((settingsOf(node).get('firewall') as Y.Map<unknown> | undefined)?.get('rules') as Y.Array<Y.Map<unknown>> | undefined);
|
||||
if (rules === undefined || index < 0 || index >= rules.length) return;
|
||||
doc.transact(() => rules.delete(index, 1), origin);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
defaultSwitchConfiguration,
|
||||
DEFAULT_SWITCH_RETENTION_MILLISECONDS,
|
||||
dhcpServerConfiguration,
|
||||
firewallConfiguration,
|
||||
forwardsIPv4ByDefault,
|
||||
type FirewallConfiguration,
|
||||
parsePortForwardingRow,
|
||||
routeTableRows,
|
||||
type DhcpServerConfiguration,
|
||||
@@ -90,6 +92,14 @@ export interface InterfaceSheet {
|
||||
readonly connected: { readonly device: string; readonly link: string } | null;
|
||||
}
|
||||
|
||||
/** A router's or home router's firewall as Java's dialog shows it (JFirewallDialog). */
|
||||
export type FirewallSheet = Pick<FirewallConfiguration, 'isActive' | 'dropIcmp' | 'filterSynSegmentsOnly' | 'defaultPolicy' | 'rules'>;
|
||||
|
||||
function firewallSheet(node: Y.Map<unknown>): FirewallSheet {
|
||||
const { isActive, dropIcmp, filterSynSegmentsOnly, defaultPolicy, rules } = firewallConfiguration((fromY(settingsOf(node).get('firewall')) ?? {}) as Partial<FirewallConfiguration>);
|
||||
return { isActive, dropIcmp, filterSynSegmentsOnly, defaultPolicy, rules };
|
||||
}
|
||||
|
||||
export interface RouterSheet {
|
||||
readonly kind: 'router';
|
||||
readonly name: string;
|
||||
@@ -100,6 +110,7 @@ export interface RouterSheet {
|
||||
readonly interfaces: readonly InterfaceSheet[];
|
||||
/** The manual rows, in their order. */
|
||||
readonly routes: readonly ManualRoute[];
|
||||
readonly firewall: FirewallSheet;
|
||||
/** The whole table as Java shows it (Weiterleitungstabelle.holeTabelle): own addresses and networks, localhost, the manual rows, the gateway. */
|
||||
readonly table: readonly RouteRow[];
|
||||
}
|
||||
@@ -114,6 +125,7 @@ export interface HomeRouterSheet {
|
||||
readonly wan: InterfaceSheet;
|
||||
readonly lan: InterfaceSheet;
|
||||
readonly dhcpServer: DhcpServerSheet;
|
||||
readonly firewall: FirewallSheet;
|
||||
/** The port forwarding rows, each with whether Java's firmware forwards it (GatewayFirmware.validateData). */
|
||||
readonly portForwarding: readonly (GatewayPortForwardingRow & { readonly forwarded: boolean })[];
|
||||
}
|
||||
@@ -237,6 +249,7 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
interfaces,
|
||||
routes,
|
||||
table: routeTableRows(interfaces, routes, gateway),
|
||||
firewall: firewallSheet(node),
|
||||
};
|
||||
}
|
||||
case 'gateway': {
|
||||
@@ -252,6 +265,7 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
lan: lan!,
|
||||
// Java's home router hands out its LAN address as the gateway, with the LAN card's mask.
|
||||
dhcpServer: dhcpServerSheet(node, { subnetMask: lan!.subnetMask, gateway: lan!.ipAddress, dnsServer: '' }, true),
|
||||
firewall: firewallSheet(node),
|
||||
portForwarding: rows.map((row) => ({ ...row, forwarded: parsePortForwardingRow(row) !== undefined })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1209,7 +1209,7 @@
|
||||
}
|
||||
],
|
||||
"flows": {
|
||||
"useMacAsNameThenOff": "26:0A:1B:EE:FE:4B",
|
||||
"useMacAsNameThenOff": "5E:C4:F7:30:10:4A",
|
||||
"useIpAsNameThenOff": "PC-A",
|
||||
"dhcpClientStopsServer": true,
|
||||
"dhcpClient": true,
|
||||
@@ -1674,6 +1674,615 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"firewall": {
|
||||
"newRule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"newRuleShown": [
|
||||
"1",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
],
|
||||
"newFirewall": [
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"cells": [
|
||||
{
|
||||
"column": 1,
|
||||
"input": "10.0.0.0",
|
||||
"rule": {
|
||||
"srcIP": "10.0.0.0",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"10.0.0.0",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 1,
|
||||
"input": "abc",
|
||||
"rule": {
|
||||
"srcIP": "abc",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": false,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 1,
|
||||
"input": ".0.0.1",
|
||||
"rule": {
|
||||
"srcIP": ".0.0.1",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
".0.0.1",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 1,
|
||||
"input": "999.999.999.999",
|
||||
"rule": {
|
||||
"srcIP": "999.999.999.999",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"999.999.999.999",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 1,
|
||||
"input": "010.0.0.0",
|
||||
"rule": {
|
||||
"srcIP": "010.0.0.0",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"010.0.0.0",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 1,
|
||||
"input": " ",
|
||||
"rule": {
|
||||
"srcIP": " ",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
" ",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"input": "255.255.0.0",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "255.255.0.0",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"255.255.0.0",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"input": "255.0.255.0",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "255.0.255.0",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": false,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"input": "0.0.0.0",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "0.0.0.0",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": false,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"input": "255.255.255.255",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "255.255.255.255",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"255.255.255.255",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"input": "0255.255.0.0",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "0255.255.0.0",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"0255.255.0.0",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 3,
|
||||
"input": "192.168.0.1",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "192.168.0.1",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"192.168.0.1",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 3,
|
||||
"input": "999.999.999.999",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "999.999.999.999",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": false,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 4,
|
||||
"input": "255.255.255.0",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "255.255.255.0",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"255.255.255.0",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"input": "80",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": 80,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"80",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"input": "abc",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"input": "0",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": 0,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": false,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"input": "080",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": 80,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"80",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"input": "",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"input": "65536",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": 65536,
|
||||
"protocol": 6,
|
||||
"action": 0
|
||||
},
|
||||
"valid": false,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 5,
|
||||
"input": "UDP",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 17,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"UDP",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 5,
|
||||
"input": "*",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": -1,
|
||||
"action": 0
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"*",
|
||||
"",
|
||||
"verwerfen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": 7,
|
||||
"input": "#0",
|
||||
"rule": {
|
||||
"srcIP": "",
|
||||
"srcMask": "",
|
||||
"destIP": "",
|
||||
"destMask": "",
|
||||
"port": -1,
|
||||
"protocol": 6,
|
||||
"action": 1
|
||||
},
|
||||
"valid": true,
|
||||
"shown": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TCP",
|
||||
"",
|
||||
"akzeptieren"
|
||||
]
|
||||
}
|
||||
],
|
||||
"moves": [
|
||||
[
|
||||
"up",
|
||||
1,
|
||||
false,
|
||||
[
|
||||
"1.1.1.1",
|
||||
"2.2.2.2",
|
||||
"3.3.3.3"
|
||||
]
|
||||
],
|
||||
[
|
||||
"up",
|
||||
3,
|
||||
true,
|
||||
[
|
||||
"1.1.1.1",
|
||||
"3.3.3.3",
|
||||
"2.2.2.2"
|
||||
]
|
||||
],
|
||||
[
|
||||
"down",
|
||||
3,
|
||||
false,
|
||||
[
|
||||
"1.1.1.1",
|
||||
"3.3.3.3",
|
||||
"2.2.2.2"
|
||||
]
|
||||
],
|
||||
[
|
||||
"down",
|
||||
1,
|
||||
true,
|
||||
[
|
||||
"3.3.3.3",
|
||||
"1.1.1.1",
|
||||
"2.2.2.2"
|
||||
]
|
||||
],
|
||||
[
|
||||
"delete",
|
||||
2,
|
||||
null,
|
||||
[
|
||||
"3.3.3.3",
|
||||
"2.2.2.2"
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
"edits": {
|
||||
"invalidCell": {
|
||||
"manual": [
|
||||
|
||||
@@ -31,6 +31,14 @@ import {
|
||||
setSwitchRetention,
|
||||
setSwitchSsid,
|
||||
addRoute,
|
||||
addFirewallRule,
|
||||
moveFirewallRule,
|
||||
removeFirewallRule,
|
||||
setFirewallDefaultPolicy,
|
||||
setFirewallFlag,
|
||||
setFirewallRuleCell,
|
||||
setFirewallRuleChoice,
|
||||
type FirewallRuleColumn,
|
||||
addDhcpStaticAssignment,
|
||||
addPortForwarding,
|
||||
removeDhcpStaticAssignment,
|
||||
@@ -90,8 +98,24 @@ const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)
|
||||
readonly addable: readonly boolean[];
|
||||
};
|
||||
readonly forwarding: { readonly rows: readonly (readonly [string, string, string, string])[]; readonly valid: readonly boolean[] };
|
||||
readonly firewall: {
|
||||
readonly newRule: JavaFirewallRule;
|
||||
readonly newFirewall: readonly [boolean, number, boolean, boolean];
|
||||
readonly cells: readonly { readonly column: number; readonly input: string; readonly rule: JavaFirewallRule; readonly valid: boolean }[];
|
||||
readonly moves: readonly (readonly [string, number, boolean | null, readonly string[]])[];
|
||||
};
|
||||
};
|
||||
|
||||
interface JavaFirewallRule {
|
||||
readonly srcIP: string;
|
||||
readonly srcMask: string;
|
||||
readonly destIP: string;
|
||||
readonly destMask: string;
|
||||
readonly port: number;
|
||||
readonly protocol: number;
|
||||
readonly action: number;
|
||||
}
|
||||
|
||||
const RULES: Readonly<Record<string, FieldRule>> = {
|
||||
'host name': 'name',
|
||||
'host ip': 'ipAddress',
|
||||
@@ -921,3 +945,147 @@ describe("a home router's port forwarding", () => {
|
||||
expect(reopened.portForwardingRowsByNodeId.get(again)).toEqual([JAVA_ROWS()[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a router's firewall", () => {
|
||||
const JAVA_PROTOCOLS: Readonly<Record<number, string>> = { [-1]: 'all', 1: 'icmp', 6: 'tcp', 17: 'udp' };
|
||||
const COLUMNS: Readonly<Record<number, FirewallRuleColumn>> = { 1: 'sourceIPAddress', 2: 'sourceSubnetMask', 3: 'destinationIPAddress', 4: 'destinationSubnetMask', 6: 'port' };
|
||||
const ruleOfJava = (rule: JavaFirewallRule) => ({
|
||||
sourceIPAddress: rule.srcIP,
|
||||
sourceSubnetMask: rule.srcMask,
|
||||
destinationIPAddress: rule.destIP,
|
||||
destinationSubnetMask: rule.destMask,
|
||||
port: rule.port,
|
||||
protocol: JAVA_PROTOCOLS[rule.protocol],
|
||||
action: rule.action === 1 ? 'accept' : 'drop',
|
||||
});
|
||||
|
||||
function firewalled() {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router');
|
||||
const rules = () => router(doc, id).firewall.rules;
|
||||
return { doc, id, rules };
|
||||
}
|
||||
|
||||
it("starts as Java's, and makes a new rule as Java's does", () => {
|
||||
const { doc, id, rules } = firewalled();
|
||||
const [isActive, policy, dropIcmp, filterSyn] = java.firewall.newFirewall;
|
||||
expect(router(doc, id).firewall).toEqual({ isActive, defaultPolicy: policy === 1 ? 'accept' : 'drop', dropIcmp, filterSynSegmentsOnly: filterSyn, rules: [] });
|
||||
expect(addFirewallRule(doc, id, 'me')).toBe(0);
|
||||
expect(rules()).toEqual([ruleOfJava(java.firewall.newRule)]);
|
||||
expect(addFirewallRule(doc, 'nobody', 'me')).toBeUndefined();
|
||||
});
|
||||
|
||||
it("takes a cell by Java's checks, never a value they refuse (Java's table keeps it in the rule)", () => {
|
||||
// Java: what its rule holds after the cell is taken, and whether the rule then passes its own check.
|
||||
expect(java.firewall.cells.filter((each) => each.column in COLUMNS).map((each) => [each.column, each.input, ruleOfJava(each.rule)[COLUMNS[each.column]!], each.valid])).toEqual([
|
||||
[1, '10.0.0.0', '10.0.0.0', true],
|
||||
[1, 'abc', 'abc', false],
|
||||
[1, '.0.0.1', '.0.0.1', true],
|
||||
[1, '999.999.999.999', '999.999.999.999', true],
|
||||
[1, '010.0.0.0', '010.0.0.0', true],
|
||||
[1, ' ', ' ', true],
|
||||
[2, '255.255.0.0', '255.255.0.0', true],
|
||||
[2, '255.0.255.0', '255.0.255.0', false],
|
||||
[2, '0.0.0.0', '0.0.0.0', false],
|
||||
[2, '255.255.255.255', '255.255.255.255', true],
|
||||
[2, '0255.255.0.0', '0255.255.0.0', true],
|
||||
[3, '192.168.0.1', '192.168.0.1', true],
|
||||
[3, '999.999.999.999', '999.999.999.999', false],
|
||||
[4, '255.255.255.0', '255.255.255.0', true],
|
||||
[6, '80', 80, true],
|
||||
[6, 'abc', -1, true],
|
||||
[6, '0', 0, false],
|
||||
[6, '080', 80, true],
|
||||
[6, '', -1, true],
|
||||
[6, '65536', 65_536, false],
|
||||
]);
|
||||
// Here: what the rule takes, or that it takes nothing. Not adopted: ".0.0.1" (no address), "abc" as all ports, "080" (musterPort).
|
||||
const ours = java.firewall.cells
|
||||
.filter((each) => each.column in COLUMNS)
|
||||
.map((each) => {
|
||||
const { doc, id, rules } = firewalled();
|
||||
addFirewallRule(doc, id, 'me');
|
||||
const result = setFirewallRuleCell(doc, id, 0, COLUMNS[each.column]!, each.input, 'me');
|
||||
return 'invalid' in result ? 'refused' : rules()[0]![COLUMNS[each.column]!];
|
||||
});
|
||||
expect(ours).toEqual([
|
||||
'10.0.0.0',
|
||||
'refused',
|
||||
'refused',
|
||||
'999.999.999.999',
|
||||
'10.0.0.0',
|
||||
'',
|
||||
'255.255.0.0',
|
||||
'refused',
|
||||
'refused',
|
||||
'255.255.255.255',
|
||||
'255.255.0.0',
|
||||
'192.168.0.1',
|
||||
'refused',
|
||||
'255.255.255.0',
|
||||
80,
|
||||
'refused',
|
||||
'refused',
|
||||
'refused',
|
||||
-1,
|
||||
'refused',
|
||||
]);
|
||||
});
|
||||
|
||||
it("chooses a rule's protocol and action as Java's lists do", () => {
|
||||
const choices = java.firewall.cells.filter((each) => !(each.column in COLUMNS)).map((each) => ruleOfJava(each.rule));
|
||||
expect(choices.map((each) => [each.protocol, each.action])).toEqual([
|
||||
['udp', 'drop'],
|
||||
['all', 'drop'],
|
||||
['tcp', 'accept'],
|
||||
]);
|
||||
const { doc, id, rules } = firewalled();
|
||||
addFirewallRule(doc, id, 'me');
|
||||
setFirewallRuleChoice(doc, id, 0, { protocol: 'udp' }, 'me');
|
||||
setFirewallRuleChoice(doc, id, 0, { action: 'accept' }, 'me');
|
||||
expect(rules()[0]).toMatchObject({ protocol: 'udp', action: 'accept' });
|
||||
setFirewallRuleChoice(doc, id, 5, { protocol: 'all' }, 'me');
|
||||
expect(rules()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("moves and removes rules as Java's buttons do, the first going no higher and the last no lower", () => {
|
||||
const { doc, id, rules } = firewalled();
|
||||
for (const source of ['1.1.1.1', '2.2.2.2', '3.3.3.3']) setFirewallRuleCell(doc, id, addFirewallRule(doc, id, 'me')!, 'sourceIPAddress', source, 'me');
|
||||
// Java's buttons pass the row's number (1 for the first); here its place.
|
||||
const ours = java.firewall.moves.map(([step, number]) => {
|
||||
const answer = step === 'delete' ? (removeFirewallRule(doc, id, number - 1, 'me'), null) : moveFirewallRule(doc, id, number - 1, step === 'up' ? -1 : 1, 'me');
|
||||
return [step, number, answer, rules().map((rule) => rule.sourceIPAddress)];
|
||||
});
|
||||
expect(ours).toEqual(java.firewall.moves);
|
||||
expect(moveFirewallRule(doc, id, 7, 1, 'me')).toBe(false);
|
||||
removeFirewallRule(doc, id, 7, 'me');
|
||||
expect(rules()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('switches its flags and its default, and keeps all through a saved file', () => {
|
||||
const { doc, id } = firewalled();
|
||||
const homeId = add(doc, 'gateway');
|
||||
for (const device of [id, homeId]) {
|
||||
setFirewallFlag(doc, device, 'isActive', true, 'me');
|
||||
setFirewallFlag(doc, device, 'dropIcmp', true, 'me');
|
||||
setFirewallFlag(doc, device, 'filterSynSegmentsOnly', false, 'me');
|
||||
setFirewallDefaultPolicy(doc, device, 'accept', 'me');
|
||||
addFirewallRule(doc, device, 'me');
|
||||
setFirewallRuleCell(doc, device, 0, 'destinationIPAddress', '10.0.0.0', 'me');
|
||||
setFirewallRuleCell(doc, device, 0, 'destinationSubnetMask', '255.0.0.0', 'me');
|
||||
setFirewallRuleCell(doc, device, 0, 'port', '80', 'me');
|
||||
}
|
||||
expect(home(doc, homeId).firewall).toMatchObject({ isActive: true, dropIcmp: true, filterSynSegmentsOnly: false, defaultPolicy: 'accept' });
|
||||
let counter = 0;
|
||||
const reopened = openProjectArchive(saveProjectArchive(readProject(doc)).archive, () => `60000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
for (const node of reopened.graph.nodes) {
|
||||
expect(reopened.firewallConfigurationsByNodeId.get(node.id)).toMatchObject({
|
||||
isActive: true,
|
||||
dropIcmp: true,
|
||||
filterSynSegmentsOnly: false,
|
||||
defaultPolicy: 'accept',
|
||||
rules: [{ sourceIPAddress: '', destinationIPAddress: '10.0.0.0', destinationSubnetMask: '255.0.0.0', port: 80, protocol: 'tcp', action: 'drop' }],
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user