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:
@@ -30,6 +30,7 @@ import { ICON_URLS } from '../canvas/icons.js';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import { NameField, type Change } from './NameField.js';
|
||||
import { HomeRouterFields, RouterFields } from './RouterSheets.js';
|
||||
import { FirewallPanel } from './FirewallPanel.js';
|
||||
import { DhcpServerPanel, PortForwardingPanel } from './ServicePanels.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
import './sheet.css';
|
||||
@@ -61,7 +62,7 @@ export interface DeviceSheetProps {
|
||||
}
|
||||
|
||||
/** A page of the sheet that Java opens as a dialog. */
|
||||
export type SheetPage = 'dhcp' | 'forwarding';
|
||||
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall';
|
||||
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight }: DeviceSheetProps) {
|
||||
const t = useTranslate();
|
||||
@@ -91,7 +92,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
‹
|
||||
</button>
|
||||
)}
|
||||
<h2>{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : title}</h2>
|
||||
<h2>{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : open === 'firewall' ? t('sheet.firewall') : title}</h2>
|
||||
<button type="button" className="sheet-close" onClick={onClose} aria-label={t('sheet.close')} title={t('sheet.close')}>
|
||||
×
|
||||
</button>
|
||||
@@ -105,7 +106,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
) : sheet.kind === 'remoteLink' ? (
|
||||
<ModemFields sheet={sheet} change={change} />
|
||||
) : sheet.kind === 'router' ? (
|
||||
<RouterFields sheet={sheet} change={change} onHighlight={onHighlight} />
|
||||
<RouterFields sheet={sheet} change={change} onHighlight={onHighlight} onOpen={onOpen} />
|
||||
) : sheet.kind === 'gateway' ? (
|
||||
<HomeRouterFields sheet={sheet} change={change} onHighlight={onHighlight} onOpen={onOpen} />
|
||||
) : (
|
||||
@@ -116,6 +117,10 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
<div key={`${id}-dhcp`}>
|
||||
<DhcpServerPanel server={sheet.dhcpServer} change={change} />
|
||||
</div>
|
||||
) : open === 'firewall' && (sheet.kind === 'router' || sheet.kind === 'gateway') ? (
|
||||
<div key={`${id}-firewall`}>
|
||||
<FirewallPanel firewall={sheet.firewall} change={change} />
|
||||
</div>
|
||||
) : open === 'forwarding' && sheet.kind === 'gateway' ? (
|
||||
<div key={`${id}-forwarding`}>
|
||||
<PortForwardingPanel sheet={sheet} change={change} />
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// A router's or home router's firewall as a page of the device sheet, after
|
||||
// Java's dialog (JFirewallDialog): a general tab with the three switches and
|
||||
// their explanations, a tab with the default action and the rules — a rule
|
||||
// chosen to edit its cells, moved up or down, a new rule at the end as
|
||||
// Java's (TCP, all ports, drop), a rule removed. Java's first tab says
|
||||
// "network interfaces" though it holds the general switches; here it says so.
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
addFirewallRule,
|
||||
FIREWALL_RULE_COLUMNS,
|
||||
firewallCellRule,
|
||||
moveFirewallRule,
|
||||
removeFirewallRule,
|
||||
setFirewallDefaultPolicy,
|
||||
setFirewallFlag,
|
||||
setFirewallRuleCell,
|
||||
setFirewallRuleChoice,
|
||||
checkField,
|
||||
type FirewallSheet,
|
||||
} from '@mdg-filius/doc';
|
||||
import type { FirewallAction, FirewallProtocol, FirewallRule } from '@mdg-filius/core';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Change } from './NameField.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
import { SheetTabs } from './SheetTabs.js';
|
||||
|
||||
/** Java's protocol names in the rule table (ruleAsVector): "*" for all. */
|
||||
const PROTOCOL_NAMES: Readonly<Record<FirewallProtocol, string>> = { all: '*', tcp: 'TCP', udp: 'UDP', icmp: 'ICMP' };
|
||||
/** Java's list offers all, TCP and UDP; ICMP comes only from a file, and is offered where a rule has it. */
|
||||
const OFFERED: readonly FirewallProtocol[] = ['all', 'tcp', 'udp'];
|
||||
|
||||
const cellText = (rule: FirewallRule, column: (typeof FIREWALL_RULE_COLUMNS)[number]) => (column === 'port' ? (rule.port < 0 ? '' : String(rule.port)) : rule[column]);
|
||||
|
||||
/** A text with Java's line breaks as paragraphs. */
|
||||
function Note({ text }: { readonly text: string }) {
|
||||
return (
|
||||
<>
|
||||
{text.split('\n').map((line, index) => (
|
||||
<p key={index} className="sheet-note">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FirewallPanel({ firewall, change }: { readonly firewall: FirewallSheet; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
const [chosen, setChosen] = useState<number | null>(null);
|
||||
const rule = chosen === null ? undefined : firewall.rules[chosen];
|
||||
const flag = (name: 'isActive' | 'dropIcmp' | 'filterSynSegmentsOnly') => (on: boolean) => change.run(() => setFirewallFlag(change.doc, change.id, name, on, change.origin));
|
||||
const move = (step: -1 | 1) => {
|
||||
if (chosen !== null && change.run(() => moveFirewallRule(change.doc, change.id, chosen, step, change.origin))) setChosen(chosen + step);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SheetTabs
|
||||
tabs={[
|
||||
{ key: 'general', label: t('sheet.general') },
|
||||
{ key: 'rules', label: t('firewall.rules') },
|
||||
]}
|
||||
selected={tab}
|
||||
onSelect={setTab}
|
||||
/>
|
||||
<div role="tabpanel" key={tab}>
|
||||
{tab === 'general' ? (
|
||||
<>
|
||||
<SheetCheck name="firewallActive" label={t('firewall.active')} checked={firewall.isActive} onChange={flag('isActive')} />
|
||||
<Note text={t('firewall.activeHint')} />
|
||||
<SheetCheck name="dropIcmp" label={t('firewall.dropIcmp')} checked={firewall.dropIcmp} onChange={flag('dropIcmp')} />
|
||||
<Note text={t('firewall.dropIcmpHint')} />
|
||||
<SheetCheck name="filterSyn" label={t('firewall.filterSyn')} checked={firewall.filterSynSegmentsOnly} onChange={flag('filterSynSegmentsOnly')} />
|
||||
<Note text={t('firewall.filterSynHint')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Note text={t('firewall.intro')} />
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.defaultPolicy')}</span>
|
||||
<select
|
||||
name="defaultPolicy"
|
||||
value={firewall.defaultPolicy}
|
||||
onChange={(event) => change.run(() => setFirewallDefaultPolicy(change.doc, change.id, event.target.value as FirewallAction, change.origin))}
|
||||
>
|
||||
<option value="accept">{t('firewall.accept')}</option>
|
||||
<option value="drop">{t('firewall.drop')}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="route-table-frame">
|
||||
<table className="route-table" data-rows="firewall">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{t('firewall.number')}</th>
|
||||
{FIREWALL_RULE_COLUMNS.slice(0, 4).map((column) => (
|
||||
<th key={column} scope="col">
|
||||
{t(`firewall.${column}`)}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col">{t('firewall.protocol')}</th>
|
||||
<th scope="col">{t('firewall.port')}</th>
|
||||
<th scope="col">{t('firewall.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{firewall.rules.map((each, index) => (
|
||||
<tr key={index} data-manual={String(index)} aria-selected={chosen === index} onClick={() => setChosen(index)}>
|
||||
<td>{index + 1}</td>
|
||||
{FIREWALL_RULE_COLUMNS.slice(0, 4).map((column) => (
|
||||
// A cell its rule refuses (from a file) is marked, not mended.
|
||||
<td key={column} data-invalid={'invalid' in checkField(firewallCellRule(column), cellText(each, column)) ? 'true' : undefined}>
|
||||
{cellText(each, column)}
|
||||
</td>
|
||||
))}
|
||||
<td>{PROTOCOL_NAMES[each.protocol]}</td>
|
||||
<td data-invalid={'invalid' in checkField('portOrEmpty', cellText(each, 'port')) ? 'true' : undefined}>{cellText(each, 'port')}</td>
|
||||
<td>{t(each.action === 'accept' ? 'firewall.accept' : 'firewall.drop')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" disabled={chosen === null || chosen === 0} onClick={() => move(-1)}>
|
||||
{t('firewall.up')}
|
||||
</button>
|
||||
<button type="button" disabled={chosen === null || chosen === firewall.rules.length - 1} onClick={() => move(1)}>
|
||||
{t('firewall.down')}
|
||||
</button>
|
||||
<button type="button" onClick={() => setChosen(change.run(() => addFirewallRule(change.doc, change.id, change.origin)) ?? null)}>
|
||||
{t('firewall.add')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={chosen === null}
|
||||
onClick={() => {
|
||||
if (chosen === null) return;
|
||||
change.run(() => removeFirewallRule(change.doc, change.id, chosen, change.origin));
|
||||
// Java keeps the row chosen, or the last one when the last went.
|
||||
const left = firewall.rules.length - 1;
|
||||
setChosen(left === 0 ? null : Math.min(chosen, left - 1));
|
||||
}}
|
||||
>
|
||||
{t('firewall.delete')}
|
||||
</button>
|
||||
</div>
|
||||
{rule === undefined ? null : (
|
||||
<fieldset className="sheet-group" key={`rule-${String(chosen)}`}>
|
||||
{FIREWALL_RULE_COLUMNS.map((column) => (
|
||||
<SheetField
|
||||
key={column}
|
||||
name={`firewall-${column}`}
|
||||
label={t(`firewall.${column}`)}
|
||||
value={cellText(rule, column)}
|
||||
rule={firewallCellRule(column)}
|
||||
inputMode={column === 'port' ? 'numeric' : 'decimal'}
|
||||
onCommit={(text) => change.run(() => setFirewallRuleCell(change.doc, change.id, chosen!, column, text, change.origin))}
|
||||
/>
|
||||
))}
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.protocol')}</span>
|
||||
<select
|
||||
name="firewall-protocol"
|
||||
value={rule.protocol}
|
||||
onChange={(event) => change.run(() => setFirewallRuleChoice(change.doc, change.id, chosen!, { protocol: event.target.value as FirewallProtocol }, change.origin))}
|
||||
>
|
||||
{(OFFERED.includes(rule.protocol) ? OFFERED : [...OFFERED, rule.protocol]).map((protocol) => (
|
||||
<option key={protocol} value={protocol}>
|
||||
{PROTOCOL_NAMES[protocol]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.action')}</span>
|
||||
<select
|
||||
name="firewall-action"
|
||||
value={rule.action}
|
||||
onChange={(event) => change.run(() => setFirewallRuleChoice(change.doc, change.id, chosen!, { action: event.target.value as FirewallAction }, change.origin))}
|
||||
>
|
||||
<option value="accept">{t('firewall.accept')}</option>
|
||||
<option value="drop">{t('firewall.drop')}</option>
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +65,17 @@ function GatewayField({ value, change }: { readonly value: string; readonly chan
|
||||
);
|
||||
}
|
||||
|
||||
export function RouterFields({ sheet, change, onHighlight }: { readonly sheet: RouterSheet; readonly change: Change; readonly onHighlight: (link: string | null) => void }) {
|
||||
export function RouterFields({
|
||||
sheet,
|
||||
change,
|
||||
onHighlight,
|
||||
onOpen,
|
||||
}: {
|
||||
readonly sheet: RouterSheet;
|
||||
readonly change: Change;
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
readonly onOpen: (page: 'firewall') => void;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
// Java greys the table out while RIP runs; an open table goes back to the general tab.
|
||||
@@ -89,6 +99,11 @@ export function RouterFields({ sheet, change, onHighlight }: { readonly sheet: R
|
||||
<GatewayField value={sheet.gateway} change={change} />
|
||||
<SheetCheck name="rip" label={t('sheet.rip')} checked={sheet.rip} onChange={(on) => change.run(() => setRip(change.doc, change.id, on, change.origin))} />
|
||||
<SheetCheck name="ipForwarding" label={t('sheet.ipForwarding')} checked={sheet.ipForwarding} disabled onChange={() => undefined} />
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" onClick={() => onOpen('firewall')}>
|
||||
{t('sheet.firewall')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : card !== undefined ? (
|
||||
<InterfaceFields card={card} change={change} />
|
||||
@@ -109,7 +124,7 @@ export function HomeRouterFields({
|
||||
readonly sheet: HomeRouterSheet;
|
||||
readonly change: Change;
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
readonly onOpen: (page: 'dhcp' | 'forwarding') => void;
|
||||
readonly onOpen: (page: 'dhcp' | 'forwarding' | 'firewall') => void;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
@@ -131,6 +146,9 @@ export function HomeRouterFields({
|
||||
<GatewayField value={sheet.gateway} change={change} />
|
||||
<SheetCheck name="ipForwarding" label={t('sheet.ipForwarding')} checked={sheet.ipForwarding} disabled onChange={() => undefined} />
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" onClick={() => onOpen('firewall')}>
|
||||
{t('sheet.firewall')}
|
||||
</button>
|
||||
<button type="button" onClick={() => onOpen('forwarding')}>
|
||||
{t('sheet.portForwarding')}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user