add chain and table addition to frontend
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 10s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 10s
This commit is contained in:
250
frontend/src/components/FireWallAddTableModal.tsx
Normal file
250
frontend/src/components/FireWallAddTableModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user