diff --git a/frontend/src/components/FireWallAddChainModal.tsx b/frontend/src/components/FireWallAddChainModal.tsx
index 2379d40..6e31719 100644
--- a/frontend/src/components/FireWallAddChainModal.tsx
+++ b/frontend/src/components/FireWallAddChainModal.tsx
@@ -111,7 +111,7 @@ export default function FirewallAddChainModal({
values && typeof values.chainName === 'string' && values.chainName.trim().length > 0
? values.chainName.trim()
: undefined;
- const chainFromForm = form.getFieldValue('chainName');
+ const chainFromForm = form.getFieldValue('hook');
const chain =
chainFromValues ||
(typeof chainFromForm === 'string' && chainFromForm.trim().length > 0 ? chainFromForm.trim() : undefined) ||
@@ -287,12 +287,6 @@ export default function FirewallAddChainModal({
)}
-
-
-
-
-
-
diff --git a/frontend/src/components/FirewallRuleBuilder.tsx b/frontend/src/components/FirewallRuleBuilder.tsx
index 05d5cb9..8ad478a 100644
--- a/frontend/src/components/FirewallRuleBuilder.tsx
+++ b/frontend/src/components/FirewallRuleBuilder.tsx
@@ -18,16 +18,13 @@ import {
Typography,
} from 'antd';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { createRuleJson } from '../api/apiClient';
-import { ChainOut, Expr, RuleOut, TableOut } from '../types/firewall';
+import { execFirewallRaw } from '../api/apiClient';
+import type { ChainOut, ExecResult, Expr, RuleOut, TableOut } from '../types/firewall';
const { Option } = Select;
const { TextArea } = Input;
const { Title, Text } = Typography;
-const MANUAL_TABLE = '__manual__';
-const MANUAL_CHAIN = '__manual_chain__';
-
type FormValues = Record;
interface RuleBuilderProps {
@@ -39,11 +36,17 @@ interface RuleBuilderProps {
onRulesChange?: (tables: TableOut[]) => void;
}
-/* Helpers (unchanged) */
+/* ----------------------
+ Small helper functions
+ ---------------------- */
+
function isPresetProtocolChoice(values: FormValues) {
return (values.protocolChoice ?? 'preset') === 'preset';
}
+/**
+ * Build NFT expression tokens (keeps your original logic).
+ */
export function buildExprFromValues(values: FormValues): Expr[] {
const expr: Expr[] = [];
@@ -156,6 +159,9 @@ export function buildExprFromValues(values: FormValues): Expr[] {
return expr;
}
+/**
+ * Create a compact textual representation of an expr.
+ */
export function textFromExpr(expr: Expr | null | undefined): string {
if (!expr) return '';
if (!Array.isArray(expr)) return String(expr);
@@ -230,52 +236,78 @@ export function textFromExpr(expr: Expr | null | undefined): string {
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 = (props: RuleBuilderProps) => {
const [form] = Form.useForm();
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const [cmdPreview, setCmdPreview] = useState('');
- const [requestJsonPreview, setRequestJsonPreview] = useState(null);
- // state for insert-before options (so we can recompute on both table change and form change)
const [insertBeforeOptions, setInsertBeforeOptions] = useState>([]);
- // 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(
() => (props.tables || []).map((t) => ({ value: `${t.family}:${t.name}`, label: `${t.family}:${t.name}` })),
[props.tables],
);
- // compute insert-before options from current form selection + props.tables
+ const noTables = !(props.tables && props.tables.length > 0);
+
const updateInsertOptions = useCallback(() => {
const ts = form.getFieldValue('tableSelect');
const cs = form.getFieldValue('chainSelect');
- if (!ts || ts === MANUAL_TABLE || !cs || cs === MANUAL_CHAIN) {
+ if (!ts || !cs) {
setInsertBeforeOptions([]);
return;
}
-
const [family, table] = String(ts).split(':');
const tbl = props.tables?.find((t) => t.family === family && t.name === table);
if (!tbl) {
setInsertBeforeOptions([]);
return;
}
-
const ch = (tbl.chains || []).find((c: ChainOut) => c.name === cs);
if (!ch || !Array.isArray(ch.rules)) {
setInsertBeforeOptions([]);
@@ -286,13 +318,22 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
.filter((r: RuleOut) => r && r.handle != null)
.map((r: RuleOut) => ({
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);
}, [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(() => {
if (tableOptions.length > 0) {
const first = tableOptions[0].value;
@@ -301,6 +342,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
protocolChoice: 'preset',
protocolPreset: 'icmp',
action: 'drop',
+ nfqueue: 1,
});
const [f, n] = String(first).split(':');
@@ -308,179 +350,117 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
if (tbl && tbl.chains && tbl.chains.length > 0) {
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
} 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(() => {
updateInsertOptions();
- updatePreviews();
+ updatePreviewFromValues();
}, 0);
} else {
form.setFieldsValue({
- tableSelect: MANUAL_TABLE,
protocolChoice: 'preset',
protocolPreset: 'icmp',
action: 'drop',
- chainSelect: MANUAL_CHAIN,
+ nfqueue: 1,
+ tableSelect: undefined,
+ chainSelect: undefined,
});
setInsertBeforeOptions([]);
- setTimeout(() => updatePreviews(), 0);
+ setTimeout(() => updatePreviewFromValues(), 0);
}
- // only when parent tables change
- }, [props.tables, tableOptions.length, updateInsertOptions]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [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(() => {
- updatePreviews();
+ const current = form.getFieldsValue();
+ if (current.nfqueue == null) form.setFieldsValue({ nfqueue: 1 });
+ updatePreviewFromValues();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- // when user changes form fields
- const onValuesChange = useCallback(() => {
- updatePreviews();
+ const onValuesChange = useCallback(
+ (_: any, allValues: FormValues) => {
+ // 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 values = form.getFieldsValue();
- const ts = values.tableSelect;
- if (ts && ts !== MANUAL_TABLE) {
- const [f, n] = String(ts).split(':');
- const tbl = props.tables?.find((t) => t.family === f && t.name === n);
- if (tbl) {
- if (tbl.chains && tbl.chains.length > 0) {
- const currentChain = form.getFieldValue('chainSelect');
- if (!currentChain || currentChain === MANUAL_CHAIN) {
- form.setFieldsValue({ chainSelect: tbl.chains[0].name });
+ const ts = allValues.tableSelect;
+ if (ts) {
+ const [f, n] = String(ts).split(':');
+ const tbl = props.tables?.find((t) => t.family === f && t.name === n);
+ if (tbl) {
+ if (tbl.chains && tbl.chains.length > 0) {
+ if (!allValues.chainSelect) {
+ form.setFieldsValue({ chainSelect: tbl.chains[0].name });
+ }
+ } else {
+ form.setFieldsValue({ chainSelect: undefined });
}
- } else {
- form.setFieldsValue({ chainSelect: MANUAL_CHAIN });
}
}
- }
- // recompute insert-before options when the user changes table/chain
- updateInsertOptions();
- }, [form, props.tables, updatePreviews, updateInsertOptions]);
+ updateInsertOptions();
+ updatePreviewFromValues(allValues);
+ },
+ [form, props.tables, updateInsertOptions, updatePreviewFromValues],
+ );
- // create handler — component will call props.refreshRules if provided; otherwise it will NOT fetch rules itself.
const handleCreate = useCallback(
async (values: FormValues) => {
- const { family, table } = resolveTable(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();
+ const { cmd } = generateCommandFromValues(values);
Modal.confirm({
- title: 'Create rule (JSON)',
+ title: 'Run raw nft command',
content: (
- About to create rule in{' '}
-
- {family}:{table} {chain}
-
+ About to run nft command in {String(values.tableSelect ?? 'inet:filter')} (see preview).
-
Generated nft command:
-
{cmd || '(no textual preview available)'}
-
-
Full JSON NFT request:
-
{JSON.stringify(req, null, 2)}
+
Command:
+
{cmd}
),
- okText: 'Create',
+ okText: 'Run',
onOk: async () => {
setLoading(true);
try {
- const res = await createRuleJson(req);
- if (res && res.rc === 0) {
- message.success('Rule created');
- if (props.refreshRules) {
- await props.refreshRules();
- } else {
- message.info('Parent did not provide a refresh function; please refresh ruleset externally.');
- }
+ const out: ExecResult = await execFirewallRaw(cmd);
+ const stderrText = out?.stderr ? String(out.stderr).trim() : '';
+ if (stderrText) {
+ message.error(`Command produced stderr: ${stderrText}`);
+ } else if (out && (out.rc === 0 || out.rc === -1)) {
+ message.success('Command executed (rc ' + out.rc + ')');
+ if (props.refreshRules) await props.refreshRules();
if (props.onCreated) await props.onCreated();
} 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) {
- const resp = err?.response;
- 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)}`);
- }
+ message.error('Execution failed: ' + (err?.message ?? String(err)));
} finally {
setLoading(false);
}
},
});
},
- [form, props.refreshRules, resolveTable],
+ [props.refreshRules, props.onCreated],
);
const chainOptions = useMemo(() => {
const ts = form.getFieldValue('tableSelect');
- if (!ts || ts === MANUAL_TABLE) return null;
+ if (!ts) return [];
const [f, n] = String(ts).split(':');
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
- if (!tbl) return null;
+ if (!tbl) return [];
return tbl.chains.map((c) => (
{c.name}
@@ -501,8 +481,6 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
console.warn('refresh failed', err);
message.error('Refresh failed');
} finally {
- // parent should update props.tables — update options once props.tables changes
- // but ensure we recompute in case parent updated in place
updateInsertOptions();
setRefreshing(false);
}
@@ -513,11 +491,9 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
- Add Firewall Rule (JSON)
+ Add Firewall Rule (raw)
-
- Builds an nft JSON expr and sends it to the server. If rendering fails, use the Raw UI.
-
+ Builds a raw nft command and executes it via the raw endpoint.
@@ -538,103 +514,66 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
protocolChoice: 'preset',
protocolPreset: 'icmp',
action: 'drop',
- tableSelect: tableOptions.length > 0 ? tableOptions[0].value : MANUAL_TABLE,
+ nfqueue: 1,
+ tableSelect: tableOptions.length > 0 ? tableOptions[0].value : undefined,
}}
onFinish={handleCreate}
onValuesChange={onValuesChange}
>
- {/* Table selection */}
+ {/* Table & Chain selection */}
-
+
{tableOptions.map((t) => (
{t.label}
))}
-
- Manual / new table...
-
- {form.getFieldValue('tableSelect') === MANUAL_TABLE ? (
-
-
-
-
-
-
-
-
- ) : (
-
-
-
- )}
+
+
+ {chainOptions}
+
+
- {/* Chain selection */}
+ {/* Insert-before */}
-
-
- {chainOptions ?? (
-
- Manual chain...
+
+
+ {insertBeforeOptions.length === 0 ? (
+
+ (no rules available)
+ ) : (
+ insertBeforeOptions.map((o) => (
+
+ {o.label}
+
+ ))
)}
-
- {form.getFieldValue('chainSelect') === MANUAL_CHAIN ||
- form.getFieldValue('tableSelect') === MANUAL_TABLE ? (
-
-
-
- ) : (
-
-
-
- )}
+
+
+ Use when you want the new rule to appear before a known handle. Refresh tables to see latest handles.
+
- {/* Insert-before control */}
- {form.getFieldValue('tableSelect') !== MANUAL_TABLE &&
- form.getFieldValue('chainSelect') &&
- form.getFieldValue('chainSelect') !== MANUAL_CHAIN && (
-
-
-
-
- {insertBeforeOptions.length === 0 ? (
-
- (no rules available)
-
- ) : (
- insertBeforeOptions.map((o) => (
-
- {o.label}
-
- ))
- )}
-
-
-
-
- )}
-
{/* Protocol / addresses */}
@@ -703,8 +642,20 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
+ {/* NFQUEUE: required when selected, default 1 */}
{form.getFieldValue('action') === 'nfqueue' && (
-
+
)}
@@ -717,10 +668,10 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
- 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.
@@ -740,39 +691,26 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
-
+
}
size="small"
style={{ marginRight: 8 }}
- onClick={(e: any) => {
- if (e.shiftKey && requestJsonPreview) {
- navigator.clipboard?.writeText(JSON.stringify(requestJsonPreview, null, 2));
- message.success('Full request JSON copied');
- return;
- }
+ onClick={() => {
if (!cmdPreview) return;
navigator.clipboard?.writeText(cmdPreview);
- message.success('Command preview copied');
+ message.success('Command copied');
}}
/>
- }>
- Create rule
+ } disabled={noTables}>
+ Run command
-
-
- Full JSON NFT request:
-
-
- {JSON.stringify(requestJsonPreview, null, 2)}
-
-
);
};