This commit is contained in:
@@ -111,7 +111,7 @@ export default function FirewallAddChainModal({
|
|||||||
values && typeof values.chainName === 'string' && values.chainName.trim().length > 0
|
values && typeof values.chainName === 'string' && values.chainName.trim().length > 0
|
||||||
? values.chainName.trim()
|
? values.chainName.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
const chainFromForm = form.getFieldValue('chainName');
|
const chainFromForm = form.getFieldValue('hook');
|
||||||
const chain =
|
const chain =
|
||||||
chainFromValues ||
|
chainFromValues ||
|
||||||
(typeof chainFromForm === 'string' && chainFromForm.trim().length > 0 ? chainFromForm.trim() : undefined) ||
|
(typeof chainFromForm === 'string' && chainFromForm.trim().length > 0 ? chainFromForm.trim() : undefined) ||
|
||||||
@@ -287,12 +287,6 @@ export default function FirewallAddChainModal({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={8}>
|
|
||||||
<Form.Item name="chainName" label="Chain Name" rules={[{ required: true }]}>
|
|
||||||
<Input placeholder="e.g. forward" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row gutter={12} style={{ marginTop: 8 }}>
|
<Row gutter={12} style={{ marginTop: 8 }}>
|
||||||
|
|||||||
@@ -18,16 +18,13 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { createRuleJson } from '../api/apiClient';
|
import { execFirewallRaw } from '../api/apiClient';
|
||||||
import { ChainOut, Expr, RuleOut, TableOut } from '../types/firewall';
|
import type { ChainOut, ExecResult, Expr, RuleOut, TableOut } from '../types/firewall';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
const MANUAL_TABLE = '__manual__';
|
|
||||||
const MANUAL_CHAIN = '__manual_chain__';
|
|
||||||
|
|
||||||
type FormValues = Record<string, any>;
|
type FormValues = Record<string, any>;
|
||||||
|
|
||||||
interface RuleBuilderProps {
|
interface RuleBuilderProps {
|
||||||
@@ -39,11 +36,17 @@ interface RuleBuilderProps {
|
|||||||
onRulesChange?: (tables: TableOut[]) => void;
|
onRulesChange?: (tables: TableOut[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Helpers (unchanged) */
|
/* ----------------------
|
||||||
|
Small helper functions
|
||||||
|
---------------------- */
|
||||||
|
|
||||||
function isPresetProtocolChoice(values: FormValues) {
|
function isPresetProtocolChoice(values: FormValues) {
|
||||||
return (values.protocolChoice ?? 'preset') === 'preset';
|
return (values.protocolChoice ?? 'preset') === 'preset';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build NFT expression tokens (keeps your original logic).
|
||||||
|
*/
|
||||||
export function buildExprFromValues(values: FormValues): Expr[] {
|
export function buildExprFromValues(values: FormValues): Expr[] {
|
||||||
const expr: Expr[] = [];
|
const expr: Expr[] = [];
|
||||||
|
|
||||||
@@ -156,6 +159,9 @@ export function buildExprFromValues(values: FormValues): Expr[] {
|
|||||||
return expr;
|
return expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a compact textual representation of an expr.
|
||||||
|
*/
|
||||||
export function textFromExpr(expr: Expr | null | undefined): string {
|
export function textFromExpr(expr: Expr | null | undefined): string {
|
||||||
if (!expr) return '';
|
if (!expr) return '';
|
||||||
if (!Array.isArray(expr)) return String(expr);
|
if (!Array.isArray(expr)) return String(expr);
|
||||||
@@ -230,52 +236,78 @@ export function textFromExpr(expr: Expr | null | undefined): string {
|
|||||||
return tokens.join(' ');
|
return tokens.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Component */
|
/* -------------------------
|
||||||
|
Command generation helper
|
||||||
|
------------------------- */
|
||||||
|
|
||||||
|
function generateCommandFromValues(values: FormValues): { cmd: string; textual: string; position?: number | null } {
|
||||||
|
const expr = buildExprFromValues(values);
|
||||||
|
let textual = textFromExpr(expr).trim();
|
||||||
|
|
||||||
|
if (values.advanced && typeof values.advanced === 'string' && values.advanced.trim() !== '') {
|
||||||
|
textual = `${textual} ${values.advanced.trim()}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.action === 'nfqueue' || values.action === 'queue') {
|
||||||
|
const qnum = values.nfqueue ?? values.queue ?? null;
|
||||||
|
const qstr = qnum != null && String(qnum).trim() !== '' ? String(qnum).trim() : null;
|
||||||
|
const hasQueue = /\bqueue(?:\s+num)?\b/i.test(textual);
|
||||||
|
if (qstr) {
|
||||||
|
if (!hasQueue) textual = `${textual} queue num ${qstr}`.trim();
|
||||||
|
else textual = textual.replace(/\bqueue(?:\s+num)?\s*\d*/i, `queue num ${qstr}`);
|
||||||
|
} else {
|
||||||
|
if (!hasQueue) textual = `${textual} queue num 0`.trim();
|
||||||
|
else textual = textual.replace(/\bqueue(?:\s+num)?\s*\d*/i, `queue num 0`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableSelect = values.tableSelect;
|
||||||
|
const chain = values.chainSelect || 'input';
|
||||||
|
const [family = 'inet', table = 'filter'] = tableSelect ? String(tableSelect).split(':') : ['inet', 'filter'];
|
||||||
|
|
||||||
|
const before = values.insertBeforeHandle;
|
||||||
|
const hasBefore = before != null && String(before) !== '';
|
||||||
|
const verb = hasBefore ? 'insert' : 'add';
|
||||||
|
const positionPart = hasBefore ? ` position ${before}` : '';
|
||||||
|
|
||||||
|
const cmd = `${verb} rule ${family} ${table} ${chain}${positionPart} ${textual}`.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
return { cmd, textual, position: hasBefore ? Number(before) : undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------
|
||||||
|
Component
|
||||||
|
------------------------- */
|
||||||
|
|
||||||
export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps) => {
|
export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [cmdPreview, setCmdPreview] = useState('');
|
const [cmdPreview, setCmdPreview] = useState('');
|
||||||
const [requestJsonPreview, setRequestJsonPreview] = useState<any>(null);
|
|
||||||
|
|
||||||
// state for insert-before options (so we can recompute on both table change and form change)
|
|
||||||
const [insertBeforeOptions, setInsertBeforeOptions] = useState<Array<{ value: any; label: string }>>([]);
|
const [insertBeforeOptions, setInsertBeforeOptions] = useState<Array<{ value: any; label: string }>>([]);
|
||||||
|
|
||||||
// Resolve table selection convenience
|
|
||||||
const resolveTable = useCallback((values: FormValues) => {
|
|
||||||
const tableSelect = values.tableSelect;
|
|
||||||
if (!tableSelect || tableSelect === MANUAL_TABLE) {
|
|
||||||
return {
|
|
||||||
family: values.familyManual || 'inet',
|
|
||||||
table: values.tableManual || 'filter',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const [family = 'inet', table = 'filter'] = String(tableSelect).split(':');
|
|
||||||
return { family, table };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const tableOptions = useMemo(
|
const tableOptions = useMemo(
|
||||||
() => (props.tables || []).map((t) => ({ value: `${t.family}:${t.name}`, label: `${t.family}:${t.name}` })),
|
() => (props.tables || []).map((t) => ({ value: `${t.family}:${t.name}`, label: `${t.family}:${t.name}` })),
|
||||||
[props.tables],
|
[props.tables],
|
||||||
);
|
);
|
||||||
|
|
||||||
// compute insert-before options from current form selection + props.tables
|
const noTables = !(props.tables && props.tables.length > 0);
|
||||||
|
|
||||||
const updateInsertOptions = useCallback(() => {
|
const updateInsertOptions = useCallback(() => {
|
||||||
const ts = form.getFieldValue('tableSelect');
|
const ts = form.getFieldValue('tableSelect');
|
||||||
const cs = form.getFieldValue('chainSelect');
|
const cs = form.getFieldValue('chainSelect');
|
||||||
if (!ts || ts === MANUAL_TABLE || !cs || cs === MANUAL_CHAIN) {
|
if (!ts || !cs) {
|
||||||
setInsertBeforeOptions([]);
|
setInsertBeforeOptions([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [family, table] = String(ts).split(':');
|
const [family, table] = String(ts).split(':');
|
||||||
const tbl = props.tables?.find((t) => t.family === family && t.name === table);
|
const tbl = props.tables?.find((t) => t.family === family && t.name === table);
|
||||||
if (!tbl) {
|
if (!tbl) {
|
||||||
setInsertBeforeOptions([]);
|
setInsertBeforeOptions([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ch = (tbl.chains || []).find((c: ChainOut) => c.name === cs);
|
const ch = (tbl.chains || []).find((c: ChainOut) => c.name === cs);
|
||||||
if (!ch || !Array.isArray(ch.rules)) {
|
if (!ch || !Array.isArray(ch.rules)) {
|
||||||
setInsertBeforeOptions([]);
|
setInsertBeforeOptions([]);
|
||||||
@@ -286,13 +318,22 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
.filter((r: RuleOut) => r && r.handle != null)
|
.filter((r: RuleOut) => r && r.handle != null)
|
||||||
.map((r: RuleOut) => ({
|
.map((r: RuleOut) => ({
|
||||||
value: r.handle,
|
value: r.handle,
|
||||||
label: `#${r.handle} — ${r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || {}).slice(0, 120))}`,
|
label: `#${r.handle} — ${
|
||||||
|
r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || {}).slice(0, 120))
|
||||||
|
}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setInsertBeforeOptions(opts);
|
setInsertBeforeOptions(opts);
|
||||||
}, [form, props.tables]);
|
}, [form, props.tables]);
|
||||||
|
|
||||||
// When the (parent-supplied) tables change, pick sensible defaults and update previews + insert options.
|
const updatePreviewFromValues = useCallback(
|
||||||
|
(values?: FormValues) => {
|
||||||
|
const vals = values ?? form.getFieldsValue();
|
||||||
|
const { cmd } = generateCommandFromValues(vals);
|
||||||
|
setCmdPreview(cmd);
|
||||||
|
},
|
||||||
|
[form],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tableOptions.length > 0) {
|
if (tableOptions.length > 0) {
|
||||||
const first = tableOptions[0].value;
|
const first = tableOptions[0].value;
|
||||||
@@ -301,6 +342,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
protocolChoice: 'preset',
|
protocolChoice: 'preset',
|
||||||
protocolPreset: 'icmp',
|
protocolPreset: 'icmp',
|
||||||
action: 'drop',
|
action: 'drop',
|
||||||
|
nfqueue: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [f, n] = String(first).split(':');
|
const [f, n] = String(first).split(':');
|
||||||
@@ -308,179 +350,117 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
if (tbl && tbl.chains && tbl.chains.length > 0) {
|
if (tbl && tbl.chains && tbl.chains.length > 0) {
|
||||||
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
|
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
|
||||||
} else {
|
} else {
|
||||||
form.setFieldsValue({ chainSelect: MANUAL_CHAIN });
|
form.setFieldsValue({ chainSelect: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
// recompute insert-before options after defaults set
|
|
||||||
// give AntD a tick to flush form fields, then compute options & previews
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateInsertOptions();
|
updateInsertOptions();
|
||||||
updatePreviews();
|
updatePreviewFromValues();
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
tableSelect: MANUAL_TABLE,
|
|
||||||
protocolChoice: 'preset',
|
protocolChoice: 'preset',
|
||||||
protocolPreset: 'icmp',
|
protocolPreset: 'icmp',
|
||||||
action: 'drop',
|
action: 'drop',
|
||||||
chainSelect: MANUAL_CHAIN,
|
nfqueue: 1,
|
||||||
|
tableSelect: undefined,
|
||||||
|
chainSelect: undefined,
|
||||||
});
|
});
|
||||||
setInsertBeforeOptions([]);
|
setInsertBeforeOptions([]);
|
||||||
setTimeout(() => updatePreviews(), 0);
|
setTimeout(() => updatePreviewFromValues(), 0);
|
||||||
}
|
}
|
||||||
// only when parent tables change
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.tables, tableOptions.length, updateInsertOptions]);
|
}, [props.tables, tableOptions.length]);
|
||||||
|
|
||||||
// preview update (reads form values)
|
|
||||||
const updatePreviews = useCallback(() => {
|
|
||||||
const values = form.getFieldsValue();
|
|
||||||
const expr = buildExprFromValues(values);
|
|
||||||
|
|
||||||
const { family, table } = resolveTable(values);
|
|
||||||
|
|
||||||
let chain = 'input';
|
|
||||||
const chainVal = values.chainSelect;
|
|
||||||
if (!chainVal || chainVal === MANUAL_CHAIN) chain = values.chainManual || chain;
|
|
||||||
else chain = chainVal;
|
|
||||||
|
|
||||||
const beforeHandle = values.insertBeforeHandle;
|
|
||||||
const short = textFromExpr(expr);
|
|
||||||
const cmdFriendly = beforeHandle
|
|
||||||
? `add rule ${family} ${table} ${chain} position ${beforeHandle} ${short}`
|
|
||||||
: `add rule ${family} ${table} ${chain} ${short}`;
|
|
||||||
setCmdPreview(cmdFriendly);
|
|
||||||
|
|
||||||
const req: any = { family, table, chain, expr };
|
|
||||||
if (beforeHandle != null && beforeHandle !== '') {
|
|
||||||
// backend expects "position"
|
|
||||||
req.position = Number(beforeHandle);
|
|
||||||
}
|
|
||||||
setRequestJsonPreview(req);
|
|
||||||
}, [form, resolveTable]);
|
|
||||||
|
|
||||||
// run once at mount to compute previews from initial values
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updatePreviews();
|
const current = form.getFieldsValue();
|
||||||
|
if (current.nfqueue == null) form.setFieldsValue({ nfqueue: 1 });
|
||||||
|
updatePreviewFromValues();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// when user changes form fields
|
const onValuesChange = useCallback(
|
||||||
const onValuesChange = useCallback(() => {
|
(_: any, allValues: FormValues) => {
|
||||||
updatePreviews();
|
// If user switches to NFQUEUE and nfqueue is empty, seed it with 1
|
||||||
|
if (allValues.action === 'nfqueue' && (allValues.nfqueue == null || allValues.nfqueue === '')) {
|
||||||
|
form.setFieldsValue({ nfqueue: 1 });
|
||||||
|
allValues.nfqueue = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// keep chain in sync when tableSelect changes
|
const ts = allValues.tableSelect;
|
||||||
const values = form.getFieldsValue();
|
if (ts) {
|
||||||
const ts = values.tableSelect;
|
const [f, n] = String(ts).split(':');
|
||||||
if (ts && ts !== MANUAL_TABLE) {
|
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
|
||||||
const [f, n] = String(ts).split(':');
|
if (tbl) {
|
||||||
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
|
if (tbl.chains && tbl.chains.length > 0) {
|
||||||
if (tbl) {
|
if (!allValues.chainSelect) {
|
||||||
if (tbl.chains && tbl.chains.length > 0) {
|
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
|
||||||
const currentChain = form.getFieldValue('chainSelect');
|
}
|
||||||
if (!currentChain || currentChain === MANUAL_CHAIN) {
|
} else {
|
||||||
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
|
form.setFieldsValue({ chainSelect: undefined });
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
form.setFieldsValue({ chainSelect: MANUAL_CHAIN });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// recompute insert-before options when the user changes table/chain
|
updateInsertOptions();
|
||||||
updateInsertOptions();
|
updatePreviewFromValues(allValues);
|
||||||
}, [form, props.tables, updatePreviews, updateInsertOptions]);
|
},
|
||||||
|
[form, props.tables, updateInsertOptions, updatePreviewFromValues],
|
||||||
|
);
|
||||||
|
|
||||||
// create handler — component will call props.refreshRules if provided; otherwise it will NOT fetch rules itself.
|
|
||||||
const handleCreate = useCallback(
|
const handleCreate = useCallback(
|
||||||
async (values: FormValues) => {
|
async (values: FormValues) => {
|
||||||
const { family, table } = resolveTable(values);
|
const { cmd } = generateCommandFromValues(values);
|
||||||
const chain =
|
|
||||||
!values.chainSelect || values.chainSelect === MANUAL_CHAIN ? values.chainManual || 'input' : values.chainSelect;
|
|
||||||
const expr = buildExprFromValues(values);
|
|
||||||
|
|
||||||
const req: any = { family, table, chain, expr };
|
|
||||||
if (values.insertBeforeHandle != null && values.insertBeforeHandle !== '') {
|
|
||||||
// backend expects "position" (per your request)
|
|
||||||
req.position = Number(values.insertBeforeHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
const textual = textFromExpr(expr);
|
|
||||||
const previewHint = req.position ? ` ${req.position}` : '';
|
|
||||||
const cmd = `add rule ${family} ${table} ${chain}${previewHint} ${textual}`.trim();
|
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: 'Create rule (JSON)',
|
title: 'Run raw nft command',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<Text>
|
<Text>
|
||||||
About to create rule in{' '}
|
About to run nft command in <b>{String(values.tableSelect ?? 'inet:filter')}</b> (see preview).
|
||||||
<b>
|
|
||||||
{family}:{table} {chain}
|
|
||||||
</b>
|
|
||||||
</Text>
|
</Text>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text strong>Generated nft command:</Text>
|
<Text strong>Command:</Text>
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{cmd || '(no textual preview available)'}</pre>
|
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{cmd}</pre>
|
||||||
<Divider />
|
|
||||||
<Text strong>Full JSON NFT request:</Text>
|
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{JSON.stringify(req, null, 2)}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
okText: 'Create',
|
okText: 'Run',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await createRuleJson(req);
|
const out: ExecResult = await execFirewallRaw(cmd);
|
||||||
if (res && res.rc === 0) {
|
const stderrText = out?.stderr ? String(out.stderr).trim() : '';
|
||||||
message.success('Rule created');
|
if (stderrText) {
|
||||||
if (props.refreshRules) {
|
message.error(`Command produced stderr: ${stderrText}`);
|
||||||
await props.refreshRules();
|
} else if (out && (out.rc === 0 || out.rc === -1)) {
|
||||||
} else {
|
message.success('Command executed (rc ' + out.rc + ')');
|
||||||
message.info('Parent did not provide a refresh function; please refresh ruleset externally.');
|
if (props.refreshRules) await props.refreshRules();
|
||||||
}
|
|
||||||
if (props.onCreated) await props.onCreated();
|
if (props.onCreated) await props.onCreated();
|
||||||
} else {
|
} else {
|
||||||
message.error(`Create failed: ${res?.stderr ?? 'unknown error'}`);
|
const info = out
|
||||||
|
? `rc:${out.rc}` +
|
||||||
|
(out.stdout ? ` stdout:${out.stdout}` : '') +
|
||||||
|
(out.stderr ? ` stderr:${out.stderr}` : '')
|
||||||
|
: 'unknown result';
|
||||||
|
message.error('Command failed: ' + info);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const resp = err?.response;
|
message.error('Execution failed: ' + (err?.message ?? String(err)));
|
||||||
if (resp && resp.data) {
|
|
||||||
const data = resp.data;
|
|
||||||
if (typeof data === 'object' && (typeof data.rc === 'number' || 'stderr' in data)) {
|
|
||||||
const rc = Number(data.rc ?? -1);
|
|
||||||
const stderr = data.stderr ?? data;
|
|
||||||
if (rc === 0) {
|
|
||||||
message.warn('Rule may have been created; check ruleset.');
|
|
||||||
if (props.refreshRules) {
|
|
||||||
await props.refreshRules();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errMsg = typeof stderr === 'string' ? stderr : JSON.stringify(stderr);
|
|
||||||
message.error(`Create failed: ${errMsg}`);
|
|
||||||
}
|
|
||||||
} else if (resp.data.detail) {
|
|
||||||
message.error(`Create failed: ${resp.data.detail}`);
|
|
||||||
} else {
|
|
||||||
message.error(`Create failed: ${JSON.stringify(resp.data)}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.error(`Create failed: ${err?.message ?? String(err)}`);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[form, props.refreshRules, resolveTable],
|
[props.refreshRules, props.onCreated],
|
||||||
);
|
);
|
||||||
|
|
||||||
const chainOptions = useMemo(() => {
|
const chainOptions = useMemo(() => {
|
||||||
const ts = form.getFieldValue('tableSelect');
|
const ts = form.getFieldValue('tableSelect');
|
||||||
if (!ts || ts === MANUAL_TABLE) return null;
|
if (!ts) return [];
|
||||||
const [f, n] = String(ts).split(':');
|
const [f, n] = String(ts).split(':');
|
||||||
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
|
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
|
||||||
if (!tbl) return null;
|
if (!tbl) return [];
|
||||||
return tbl.chains.map((c) => (
|
return tbl.chains.map((c) => (
|
||||||
<Option key={c.name} value={c.name}>
|
<Option key={c.name} value={c.name}>
|
||||||
{c.name}
|
{c.name}
|
||||||
@@ -501,8 +481,6 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
console.warn('refresh failed', err);
|
console.warn('refresh failed', err);
|
||||||
message.error('Refresh failed');
|
message.error('Refresh failed');
|
||||||
} finally {
|
} finally {
|
||||||
// parent should update props.tables — update options once props.tables changes
|
|
||||||
// but ensure we recompute in case parent updated in place
|
|
||||||
updateInsertOptions();
|
updateInsertOptions();
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
@@ -513,11 +491,9 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
<Row justify="space-between" align="middle">
|
<Row justify="space-between" align="middle">
|
||||||
<Col>
|
<Col>
|
||||||
<Title level={4} style={{ margin: 0 }}>
|
<Title level={4} style={{ margin: 0 }}>
|
||||||
Add Firewall Rule (JSON)
|
Add Firewall Rule (raw)
|
||||||
</Title>
|
</Title>
|
||||||
<Text type="secondary">
|
<Text type="secondary">Builds a raw nft command and executes it via the raw endpoint.</Text>
|
||||||
Builds an nft JSON <code>expr</code> and sends it to the server. If rendering fails, use the Raw UI.
|
|
||||||
</Text>
|
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col>
|
<Col>
|
||||||
@@ -538,103 +514,66 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
protocolChoice: 'preset',
|
protocolChoice: 'preset',
|
||||||
protocolPreset: 'icmp',
|
protocolPreset: 'icmp',
|
||||||
action: 'drop',
|
action: 'drop',
|
||||||
tableSelect: tableOptions.length > 0 ? tableOptions[0].value : MANUAL_TABLE,
|
nfqueue: 1,
|
||||||
|
tableSelect: tableOptions.length > 0 ? tableOptions[0].value : undefined,
|
||||||
}}
|
}}
|
||||||
onFinish={handleCreate}
|
onFinish={handleCreate}
|
||||||
onValuesChange={onValuesChange}
|
onValuesChange={onValuesChange}
|
||||||
>
|
>
|
||||||
{/* Table selection */}
|
{/* Table & Chain selection */}
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Form.Item name="tableSelect" label="Table (family:name)" rules={[{ required: true }]}>
|
<Form.Item name="tableSelect" label="Table (family:name)" rules={[{ required: true }]}>
|
||||||
<Select>
|
<Select placeholder={noTables ? 'No tables available' : 'Select table'} disabled={noTables}>
|
||||||
{tableOptions.map((t) => (
|
{tableOptions.map((t) => (
|
||||||
<Option key={t.value} value={t.value}>
|
<Option key={t.value} value={t.value}>
|
||||||
{t.label}
|
{t.label}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
<Option key={MANUAL_TABLE} value={MANUAL_TABLE}>
|
|
||||||
Manual / new table...
|
|
||||||
</Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
{form.getFieldValue('tableSelect') === MANUAL_TABLE ? (
|
<Form.Item name="chainSelect" label="Chain (select)" rules={[{ required: true }]}>
|
||||||
<Space style={{ width: '100%' }}>
|
<Select placeholder="Select chain" disabled={noTables || chainOptions.length === 0}>
|
||||||
<Form.Item name="familyManual" label="Family" style={{ flex: 1 }}>
|
{chainOptions}
|
||||||
<Input placeholder="inet, bridge, ip, ip6 ..." />
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="tableManual" label="Table" style={{ flex: 1 }}>
|
|
||||||
<Input placeholder="filter" />
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
) : (
|
|
||||||
<Form.Item label="Table (preview)">
|
|
||||||
<Input value={form.getFieldValue('tableSelect')} readOnly />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Chain selection */}
|
{/* Insert-before */}
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Form.Item name="chainSelect" label="Chain (select)" rules={[{ required: true }]}>
|
<Form.Item
|
||||||
<Select>
|
name="insertBeforeHandle"
|
||||||
{chainOptions ?? (
|
label="Insert before (optional)"
|
||||||
<Option key={MANUAL_CHAIN} value={MANUAL_CHAIN}>
|
help="Pick an existing rule handle to insert the new rule before. If left empty the rule will be appended."
|
||||||
Manual chain...
|
>
|
||||||
|
<Select allowClear placeholder="Append (no insert-before)">
|
||||||
|
{insertBeforeOptions.length === 0 ? (
|
||||||
|
<Option value="__none__" disabled>
|
||||||
|
(no rules available)
|
||||||
</Option>
|
</Option>
|
||||||
|
) : (
|
||||||
|
insertBeforeOptions.map((o) => (
|
||||||
|
<Option key={String(o.value)} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</Option>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12} style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||||
{form.getFieldValue('chainSelect') === MANUAL_CHAIN ||
|
<Text type="secondary">
|
||||||
form.getFieldValue('tableSelect') === MANUAL_TABLE ? (
|
Use when you want the new rule to appear before a known handle. Refresh tables to see latest handles.
|
||||||
<Form.Item name="chainManual" label="Chain (manual)">
|
</Text>
|
||||||
<Input placeholder="input, forward, output" />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
<Form.Item label="Chain (preview)">
|
|
||||||
<Input value={form.getFieldValue('chainSelect') ?? ''} readOnly />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Insert-before control */}
|
|
||||||
{form.getFieldValue('tableSelect') !== MANUAL_TABLE &&
|
|
||||||
form.getFieldValue('chainSelect') &&
|
|
||||||
form.getFieldValue('chainSelect') !== MANUAL_CHAIN && (
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col xs={24} sm={12}>
|
|
||||||
<Form.Item
|
|
||||||
name="insertBeforeHandle"
|
|
||||||
label="Insert before (optional)"
|
|
||||||
help="Pick an existing rule handle to insert the new rule before. If left empty the rule will be appended."
|
|
||||||
>
|
|
||||||
<Select allowClear placeholder="Append (no insert-before)">
|
|
||||||
{insertBeforeOptions.length === 0 ? (
|
|
||||||
<Option value="__none__" disabled>
|
|
||||||
(no rules available)
|
|
||||||
</Option>
|
|
||||||
) : (
|
|
||||||
insertBeforeOptions.map((o) => (
|
|
||||||
<Option key={String(o.value)} value={o.value}>
|
|
||||||
{o.label}
|
|
||||||
</Option>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Protocol / addresses */}
|
{/* Protocol / addresses */}
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} sm={8}>
|
<Col xs={24} sm={8}>
|
||||||
@@ -703,8 +642,20 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* NFQUEUE: required when selected, default 1 */}
|
||||||
{form.getFieldValue('action') === 'nfqueue' && (
|
{form.getFieldValue('action') === 'nfqueue' && (
|
||||||
<Form.Item name="nfqueue" label="Queue number" initialValue={1}>
|
<Form.Item
|
||||||
|
name="nfqueue"
|
||||||
|
label="Queue number"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Queue number is required when NFQUEUE is selected' },
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
message: 'Queue number must be at least 1',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
<InputNumber style={{ width: '100%' }} min={1} max={65535} />
|
<InputNumber style={{ width: '100%' }} min={1} max={65535} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
@@ -717,10 +668,10 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
<Form.Item name="advanced" label="Advanced (optional)">
|
<Form.Item name="advanced" label="Advanced (optional)">
|
||||||
<TextArea
|
<TextArea
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder='Append arbitrary JSON token or text (advanced). Example: {"limit": {"rate":"5/second"}}'
|
placeholder='Append advanced text (will be appended to the command preview). Example: "limit rate 5/second"'
|
||||||
/>
|
/>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Advanced is appended to the expr as a token. Prefer structured fields where possible.
|
Advanced is appended to the textual expr preview. Prefer structured fields where possible.
|
||||||
</Text>
|
</Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -740,39 +691,26 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ width: 320 }}>
|
<div style={{ width: 320 }}>
|
||||||
<Tooltip title="Click to copy expr JSON. Shift+Click copies full request JSON.">
|
<Tooltip title="Copy command to clipboard">
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
style={{ marginRight: 8 }}
|
style={{ marginRight: 8 }}
|
||||||
onClick={(e: any) => {
|
onClick={() => {
|
||||||
if (e.shiftKey && requestJsonPreview) {
|
|
||||||
navigator.clipboard?.writeText(JSON.stringify(requestJsonPreview, null, 2));
|
|
||||||
message.success('Full request JSON copied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!cmdPreview) return;
|
if (!cmdPreview) return;
|
||||||
navigator.clipboard?.writeText(cmdPreview);
|
navigator.clipboard?.writeText(cmdPreview);
|
||||||
message.success('Command preview copied');
|
message.success('Command copied');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Button type="primary" htmlType="submit" loading={loading} icon={<PlusOutlined />}>
|
<Button type="primary" htmlType="submit" loading={loading} icon={<PlusOutlined />} disabled={noTables}>
|
||||||
Create rule
|
Run command
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<Text strong>Full JSON NFT request:</Text>
|
|
||||||
<div style={{ marginTop: 8 }}>
|
|
||||||
<Card size="small" type="inner" style={{ background: '#fafafa' }}>
|
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{JSON.stringify(requestJsonPreview, null, 2)}</pre>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user