Some checks failed
Build and Deploy MITM Webserver / build (push) Failing after 3s
277 lines
8.2 KiB
TypeScript
277 lines
8.2 KiB
TypeScript
import { Button, Card, Col, Form, Input, Modal, Row, Select, Space, Spin, Typography, notification } 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?: (created?: boolean) => void; // created === true when a table was created
|
|
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');
|
|
|
|
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) {
|
|
notification.warning({
|
|
message: 'Could not load ruleset',
|
|
description: err?.message ?? String(err),
|
|
duration: 6,
|
|
});
|
|
setRulesetEmpty(false);
|
|
} finally {
|
|
setLocalLoadingRuleset(false);
|
|
}
|
|
}
|
|
|
|
function buildCommands(values: any): string[] {
|
|
const family = (values.family ?? 'bridge').trim();
|
|
const table = (values.tableName ?? 'filter').trim();
|
|
return [`add table ${family} ${table}`];
|
|
}
|
|
|
|
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);
|
|
|
|
try {
|
|
await refreshRuleset();
|
|
} catch {
|
|
}
|
|
|
|
const hadError = acc.some((r) => r.err);
|
|
if (!hadError) {
|
|
notification.success({
|
|
message: 'Table created',
|
|
description: 'Table was created and ruleset has been refreshed locally in the modal.',
|
|
duration: 4,
|
|
});
|
|
onClose?.(true); // signal parent to refresh and close modal
|
|
} else {
|
|
notification.error({
|
|
message: 'Some commands returned errors',
|
|
description: 'See execution results below for details.',
|
|
duration: 6,
|
|
});
|
|
setStep(1);
|
|
}
|
|
}
|
|
|
|
function renderFooter() {
|
|
return (
|
|
<Space>
|
|
<Button
|
|
onClick={() => {
|
|
if (step === 0) onClose?.(false);
|
|
else setStep(0);
|
|
}}
|
|
>
|
|
{step === 0 ? 'Cancel' : 'Back'}
|
|
</Button>
|
|
|
|
{step === 0 ? (
|
|
<Space>
|
|
<Button
|
|
type="primary"
|
|
onClick={() => {
|
|
form
|
|
.validateFields()
|
|
.then(() => setStep(1))
|
|
.catch(() =>
|
|
notification.warning({
|
|
message: 'Validation',
|
|
description: 'Please fill required fields before preview.',
|
|
}),
|
|
);
|
|
}}
|
|
>
|
|
Preview
|
|
</Button>
|
|
</Space>
|
|
) : (
|
|
<Button
|
|
type="primary"
|
|
loading={running}
|
|
onClick={() => executeCommands(buildCommands(form.getFieldsValue()))}
|
|
>
|
|
Execute
|
|
</Button>
|
|
)}
|
|
</Space>
|
|
);
|
|
}
|
|
|
|
if (localLoadingRuleset) {
|
|
return (
|
|
<Modal title="Create Table" open={open} onCancel={() => onClose?.(false)} footer={null} width={700}>
|
|
<div style={{ textAlign: 'center', padding: 28 }}>
|
|
<Spin />
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
title="Create Table"
|
|
open={open}
|
|
onCancel={() => onClose?.(false)}
|
|
width={700}
|
|
footer={renderFooter()}
|
|
destroyOnClose
|
|
>
|
|
|
|
{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 && (
|
|
<>
|
|
<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>
|
|
);
|
|
}
|