add chain and table addition to frontend
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 10s

This commit is contained in:
2026-02-23 20:21:16 +01:00
parent dfa9a4a343
commit 6636f72f11
5 changed files with 1100 additions and 222 deletions

View File

@@ -0,0 +1,250 @@
// src/components/FirewallManager.tsx
import { Button, Card, Col, Form, Input, Modal, Row, Select, Space, Spin, Typography, message } from 'antd';
import { ReactElement, useEffect, useState } from 'react';
import { execFirewallRaw, fetchRuleset } from '../api/apiClient';
const { Paragraph, Text } = Typography;
const { Option } = Select;
type ExecResult = {
rc: number;
stdout?: string | null;
stderr?: string | null;
};
type CmdResult = {
cmd: string;
out?: ExecResult;
err?: string;
};
type Props = {
open: boolean;
onClose: () => void;
startOnPreview?: boolean;
};
const FAMILY_DESCRIPTIONS: Record<string, string> = {
ip: 'IPv4 packets.',
ip6: 'IPv6 packets.',
inet: 'Both IPv4 and IPv6.',
arp: 'ARP packets.',
bridge: 'Packets passing through a bridge.',
netdev: 'Direct filtering on a network device (ingress).',
};
export default function FirewallAddTableModal({ open, onClose, startOnPreview = false }: Props): ReactElement {
const [form] = Form.useForm();
const [localLoadingRuleset, setLocalLoadingRuleset] = useState(false);
const [rulesetEmpty, setRulesetEmpty] = useState<boolean>(false);
const [step, setStep] = useState<number>(startOnPreview ? 1 : 0);
const [running, setRunning] = useState(false);
const [results, setResults] = useState<CmdResult[]>([]);
const [selectedFamily, setSelectedFamily] = useState<string>('bridge');
// initialize and refresh when modal opens
useEffect(() => {
form.setFieldsValue({
family: 'bridge',
tableName: 'filter',
});
setSelectedFamily('bridge');
if (open) {
refreshRuleset();
setResults([]);
setRunning(false);
setStep(startOnPreview ? 1 : 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
async function refreshRuleset() {
setLocalLoadingRuleset(true);
try {
const res = await fetchRuleset();
const ruleset = (res as any).ruleset;
if (ruleset && Array.isArray(ruleset.tables)) {
setRulesetEmpty(ruleset.tables.length === 0);
} else {
setRulesetEmpty(false);
}
} catch (err: any) {
message.warning('Could not load ruleset: ' + (err?.message ?? String(err)));
} finally {
setLocalLoadingRuleset(false);
}
}
// Build commands: only create table
function buildCommands(values: any): string[] {
const family = values.family ?? 'bridge';
const table = values.tableName ?? 'filter';
return [`add table ${family} ${table}`];
}
// Execute commands sequentially
async function executeCommands(cmds: string[]) {
setRunning(true);
setResults([]);
const acc: CmdResult[] = [];
for (const cmd of cmds) {
try {
const out = (await execFirewallRaw(cmd)) as ExecResult;
const success = out && (out.rc === 0 || out.rc === -1);
if (success) acc.push({ cmd, out });
else acc.push({ cmd, out, err: `rc: ${out.rc} stderr: ${out.stderr ?? ''}` });
} catch (err: any) {
acc.push({ cmd, err: err?.message ?? String(err) });
}
}
setResults(acc);
setRunning(false);
// refresh ruleset after running
try {
await refreshRuleset();
} catch {
// ignore — refreshRuleset handles messaging
}
const hadError = acc.some((r) => r.err);
if (!hadError) {
message.success('Table created and ruleset refreshed');
onClose();
} else {
message.error('Some commands returned errors — see results in the modal');
setStep(1);
}
}
function renderFooter() {
return (
<Space>
<Button
onClick={() => {
if (step === 0) onClose();
else setStep(0);
}}
>
{step === 0 ? 'Cancel' : 'Back'}
</Button>
{step === 0 ? (
<Space>
<Button type="primary" onClick={() => setStep(1)}>
Preview
</Button>
</Space>
) : (
<Button
type="primary"
loading={running}
onClick={() => executeCommands(buildCommands(form.getFieldsValue()))}
>
Execute
</Button>
)}
</Space>
);
}
// show spinner while checking ruleset
if (localLoadingRuleset) {
return (
<Modal title="Create Table" open={open} onCancel={onClose} footer={null} width={700}>
<div style={{ textAlign: 'center', padding: 28 }}>
<Spin />
</div>
</Modal>
);
}
return (
<Modal title="Create Table" open={open} onCancel={onClose} width={700} footer={renderFooter()} destroyOnClose>
{/* Step 0: minimal form */}
{step === 0 && (
<div>
<Form
form={form}
layout="vertical"
onValuesChange={(changed) => {
if (changed.family) setSelectedFamily(changed.family);
}}
>
<Row gutter={12}>
<Col span={8}>
<Form.Item name="family" label="Family" rules={[{ required: true }]}>
<Select defaultValue="bridge" value={selectedFamily}>
<Option value="ip">ip</Option>
<Option value="ip6">ip6</Option>
<Option value="inet">inet</Option>
<Option value="arp">arp</Option>
<Option value="bridge">bridge</Option>
<Option value="netdev">netdev</Option>
</Select>
</Form.Item>
<Paragraph type="secondary" style={{ marginTop: 8 }}>
{FAMILY_DESCRIPTIONS[selectedFamily]}
</Paragraph>
</Col>
<Col span={8}>
<Form.Item name="tableName" label="Table Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
</Row>
</Form>
</div>
)}
{/* Step 1: preview + results */}
{step === 1 && (
<>
<Card title="Command preview" style={{ marginBottom: 12 }}>
<Paragraph>
The command below will be executed on the server (server will prefix with <Text code>nft</Text>):
</Paragraph>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fafafa', padding: 12 }}>
{buildCommands(form.getFieldsValue()).join('\n')}
</pre>
</Card>
{results.length > 0 && (
<Card title="Execution results" style={{ marginTop: 12 }}>
{results.map((r, i) => (
<div key={i} style={{ marginBottom: 12 }}>
<Text strong>{r.cmd}</Text>
{r.err ? (
<Paragraph type="danger">{r.err}</Paragraph>
) : (
<>
<Paragraph>
<Text type="secondary">rc:</Text> {r.out?.rc}
</Paragraph>
{r.out?.stdout ? (
<>
<Text type="secondary">stdout:</Text>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fff', padding: 8 }}>{r.out.stdout}</pre>
</>
) : null}
{r.out?.stderr ? (
<>
<Text type="secondary">stderr:</Text>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fff', padding: 8 }}>{r.out.stderr}</pre>
</>
) : null}
</>
)}
</div>
))}
</Card>
)}
</>
)}
</Modal>
);
}