JSON WORKS HERE TAG
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
This commit is contained in:
@@ -18,20 +18,33 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createRuleJson, fetchRuleset } from '../api/apiClient';
|
||||
import { Expr, TableOut } from '../types/firewall';
|
||||
import { createRuleJson } from '../api/apiClient';
|
||||
import { ChainOut, 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<string, any>;
|
||||
|
||||
interface RuleBuilderProps {
|
||||
onCreated?: () => Promise<void> | void;
|
||||
tables?: TableOut[] | null;
|
||||
rulesLoading?: boolean;
|
||||
rulesError?: string | null;
|
||||
refreshRules?: () => Promise<void>;
|
||||
onRulesChange?: (tables: TableOut[]) => void;
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
/* Helpers (unchanged) */
|
||||
function isPresetProtocolChoice(values: FormValues) {
|
||||
return (values.protocolChoice ?? 'preset') === 'preset';
|
||||
}
|
||||
|
||||
function buildExprFromValues(values: any): Expr[] {
|
||||
export function buildExprFromValues(values: FormValues): Expr[] {
|
||||
const expr: Expr[] = [];
|
||||
|
||||
const protoChoice = values.protocolChoice ?? 'preset';
|
||||
@@ -82,7 +95,6 @@ function buildExprFromValues(values: any): Expr[] {
|
||||
} else if (/udp/i.test(custom)) {
|
||||
expr.push({ udp: {} });
|
||||
} else {
|
||||
// allow user to paste a JSON token string (advanced)
|
||||
try {
|
||||
const parsed = JSON.parse(custom);
|
||||
expr.push(parsed);
|
||||
@@ -112,30 +124,21 @@ function buildExprFromValues(values: any): Expr[] {
|
||||
});
|
||||
}
|
||||
|
||||
const chooseUdp = (preset?: string, custom?: string) => preset === 'udp' || (custom && /(udp)/i.test(custom));
|
||||
|
||||
if (values.sport) {
|
||||
const useUdp =
|
||||
values.protocolPreset === 'udp' ||
|
||||
(values.protocolChoice === 'custom' && /(udp)/i.test(values.protocolCustom || ''));
|
||||
const proto = useUdp ? 'udp' : 'tcp';
|
||||
const obj: any = {};
|
||||
obj[proto] = { sport: Number(values.sport) };
|
||||
expr.push(obj);
|
||||
const proto = chooseUdp(values.protocolPreset, values.protocolCustom) ? 'udp' : 'tcp';
|
||||
expr.push({ [proto]: { sport: Number(values.sport) } } as any);
|
||||
}
|
||||
if (values.dport) {
|
||||
const useUdp =
|
||||
values.protocolPreset === 'udp' ||
|
||||
(values.protocolChoice === 'custom' && /(udp)/i.test(values.protocolCustom || ''));
|
||||
const proto = useUdp ? 'udp' : 'tcp';
|
||||
const obj: any = {};
|
||||
obj[proto] = { dport: Number(values.dport) };
|
||||
expr.push(obj);
|
||||
const proto = chooseUdp(values.protocolPreset, values.protocolCustom) ? 'udp' : 'tcp';
|
||||
expr.push({ [proto]: { dport: Number(values.dport) } } as any);
|
||||
}
|
||||
|
||||
if (values.advanced) {
|
||||
const adv = values.advanced.trim();
|
||||
const adv = (values.advanced || '').trim();
|
||||
try {
|
||||
const parsed = JSON.parse(adv);
|
||||
expr.push(parsed);
|
||||
expr.push(JSON.parse(adv));
|
||||
} catch {
|
||||
expr.push(adv);
|
||||
}
|
||||
@@ -146,299 +149,264 @@ function buildExprFromValues(values: any): Expr[] {
|
||||
else if (action === 'accept') expr.push({ accept: null });
|
||||
else if (action === 'reject') expr.push({ reject: null });
|
||||
else if (action === 'nfqueue' || action === 'queue') {
|
||||
// include a numeric queue token in the expr. backend should accept this shape.
|
||||
const qnum = Number(values.nfqueue || 0);
|
||||
if (Number.isFinite(qnum) && qnum > 0) {
|
||||
expr.push({ queue: qnum });
|
||||
} else {
|
||||
// if no valid queue number provided, just push a generic queue token (backend may reject)
|
||||
expr.push({ queue: 0 });
|
||||
}
|
||||
expr.push({ queue: Number.isFinite(qnum) && qnum > 0 ? qnum : 0 } as any);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
function textFromExpr(expr: Expr): string {
|
||||
if (expr == null) return '';
|
||||
if (Array.isArray(expr)) {
|
||||
const tokens: string[] = [];
|
||||
for (const part of expr) {
|
||||
if (typeof part === 'string') {
|
||||
tokens.push(part);
|
||||
} else if (part && typeof part === 'object') {
|
||||
if ('match' in part) {
|
||||
const m = (part as any).match;
|
||||
const left = m.left;
|
||||
const right = m.right;
|
||||
if (left && left.payload && right !== undefined) {
|
||||
const p = left.payload;
|
||||
const prot = p.protocol;
|
||||
const field = p.field;
|
||||
if (prot && field && typeof right === 'string') {
|
||||
tokens.push(`${prot} ${field} ${right}`);
|
||||
continue;
|
||||
}
|
||||
tokens.push('match');
|
||||
continue;
|
||||
}
|
||||
tokens.push('match');
|
||||
} else if ('payload' in part) {
|
||||
const p = (part as any).payload;
|
||||
const prot = p.protocol;
|
||||
const field = p.field;
|
||||
tokens.push(`payload(${prot}.${field})`);
|
||||
} else if ('tcp' in part) {
|
||||
const v = (part as any).tcp;
|
||||
if (v && v.dport) tokens.push(`tcp dport ${v.dport}`);
|
||||
else if (v && v.sport) tokens.push(`tcp sport ${v.sport}`);
|
||||
else tokens.push('tcp');
|
||||
} else if ('udp' in part) {
|
||||
const v = (part as any).udp;
|
||||
if (v && v.dport) tokens.push(`udp dport ${v.dport}`);
|
||||
else if (v && v.sport) tokens.push(`udp sport ${v.sport}`);
|
||||
else tokens.push('udp');
|
||||
} else if ('drop' in part) {
|
||||
tokens.push('drop');
|
||||
} else if ('accept' in part) {
|
||||
tokens.push('accept');
|
||||
} else if ('reject' in part) {
|
||||
tokens.push('reject');
|
||||
} else if ('counter' in part) {
|
||||
tokens.push('counter');
|
||||
} else if ('queue' in part) {
|
||||
// show NFQUEUE / queue
|
||||
const q = (part as any).queue;
|
||||
tokens.push(`queue ${q}`);
|
||||
} else {
|
||||
const keys = Object.keys(part).sort().join('+');
|
||||
tokens.push(keys);
|
||||
}
|
||||
} else {
|
||||
tokens.push(String(part));
|
||||
}
|
||||
export function textFromExpr(expr: Expr | null | undefined): string {
|
||||
if (!expr) return '';
|
||||
if (!Array.isArray(expr)) return String(expr);
|
||||
|
||||
const tokens: string[] = [];
|
||||
for (const part of expr) {
|
||||
if (typeof part === 'string') {
|
||||
tokens.push(part);
|
||||
continue;
|
||||
}
|
||||
return tokens.join(' ');
|
||||
if (!part || typeof part !== 'object') {
|
||||
tokens.push(String(part));
|
||||
continue;
|
||||
}
|
||||
if ('match' in part) {
|
||||
const m = (part as any).match;
|
||||
const left = m.left;
|
||||
const right = m.right;
|
||||
if (left && left.payload && right !== undefined) {
|
||||
const p = left.payload;
|
||||
const prot = p.protocol;
|
||||
const field = p.field;
|
||||
if (prot && field && typeof right === 'string') {
|
||||
tokens.push(`${prot} ${field} ${right}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
tokens.push('match');
|
||||
continue;
|
||||
}
|
||||
if ('payload' in part) {
|
||||
const p = (part as any).payload;
|
||||
tokens.push(`payload(${p.protocol}.${p.field})`);
|
||||
continue;
|
||||
}
|
||||
if ('tcp' in part) {
|
||||
const v = (part as any).tcp;
|
||||
if (v && v.dport) tokens.push(`tcp dport ${v.dport}`);
|
||||
else if (v && v.sport) tokens.push(`tcp sport ${v.sport}`);
|
||||
else tokens.push('tcp');
|
||||
continue;
|
||||
}
|
||||
if ('udp' in part) {
|
||||
const v = (part as any).udp;
|
||||
if (v && v.dport) tokens.push(`udp dport ${v.dport}`);
|
||||
else if (v && v.sport) tokens.push(`udp sport ${v.sport}`);
|
||||
else tokens.push('udp');
|
||||
continue;
|
||||
}
|
||||
if ('drop' in part) {
|
||||
tokens.push('drop');
|
||||
continue;
|
||||
}
|
||||
if ('accept' in part) {
|
||||
tokens.push('accept');
|
||||
continue;
|
||||
}
|
||||
if ('reject' in part) {
|
||||
tokens.push('reject');
|
||||
continue;
|
||||
}
|
||||
if ('counter' in part) {
|
||||
tokens.push('counter');
|
||||
continue;
|
||||
}
|
||||
if ('queue' in part) {
|
||||
tokens.push(`queue ${(part as any).queue}`);
|
||||
continue;
|
||||
}
|
||||
tokens.push(Object.keys(part).sort().join('+'));
|
||||
}
|
||||
return String(expr);
|
||||
return tokens.join(' ');
|
||||
}
|
||||
|
||||
/* ---------- component ---------- */
|
||||
|
||||
export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tables, setTables] = useState<TableOut[]>([]);
|
||||
/* Component */
|
||||
export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [cmdPreview, setCmdPreview] = useState<string>('');
|
||||
const [requestJsonPreview, setRequestJsonPreview] = useState<any>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function loadTables() {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
const res = await fetchRuleset();
|
||||
if (!res || res.ruleset === null) {
|
||||
setTables([]);
|
||||
} else if (typeof res.ruleset === 'string') {
|
||||
setTables([]);
|
||||
} else {
|
||||
setTables(res.ruleset.tables ?? []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('fetchRuleset failed:', err);
|
||||
message.warning('Could not fetch tables/chains; you can still create rules manually.');
|
||||
setTables([]);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
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 }>>([]);
|
||||
|
||||
// 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// fetch on mount
|
||||
useEffect(() => {
|
||||
loadTables();
|
||||
const [family = 'inet', table = 'filter'] = String(tableSelect).split(':');
|
||||
return { family, table };
|
||||
}, []);
|
||||
|
||||
const tableOptions = useMemo(
|
||||
() => tables.map((t) => ({ family: t.family, name: t.name, label: `${t.family}:${t.name}` })),
|
||||
[tables],
|
||||
() => (props.tables || []).map((t) => ({ value: `${t.family}:${t.name}`, label: `${t.family}:${t.name}` })),
|
||||
[props.tables],
|
||||
);
|
||||
|
||||
// set sensible defaults once tables are known
|
||||
// compute insert-before options from current form selection + props.tables
|
||||
const updateInsertOptions = useCallback(() => {
|
||||
const ts = form.getFieldValue('tableSelect');
|
||||
const cs = form.getFieldValue('chainSelect');
|
||||
if (!ts || ts === MANUAL_TABLE || !cs || cs === MANUAL_CHAIN) {
|
||||
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([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = ch.rules
|
||||
.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))}`,
|
||||
}));
|
||||
|
||||
setInsertBeforeOptions(opts);
|
||||
}, [form, props.tables]);
|
||||
|
||||
// When the (parent-supplied) tables change, pick sensible defaults and update previews + insert options.
|
||||
useEffect(() => {
|
||||
if (tableOptions.length > 0) {
|
||||
const first = tableOptions[0];
|
||||
const first = tableOptions[0].value;
|
||||
form.setFieldsValue({
|
||||
tableSelect: `${first.family}:${first.name}`,
|
||||
tableSelect: first,
|
||||
protocolChoice: 'preset',
|
||||
protocolPreset: 'icmp',
|
||||
action: 'drop',
|
||||
});
|
||||
// also set chainSelect to the first chain for that table if exists
|
||||
const tbl = tables.find((x) => x.family === first.family && x.name === first.name);
|
||||
|
||||
const [f, n] = String(first).split(':');
|
||||
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
|
||||
if (tbl && tbl.chains && tbl.chains.length > 0) {
|
||||
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
|
||||
} else {
|
||||
form.setFieldsValue({ chainSelect: '__manual_chain__' });
|
||||
form.setFieldsValue({ chainSelect: MANUAL_CHAIN });
|
||||
}
|
||||
|
||||
// recompute insert-before options after defaults set
|
||||
// give AntD a tick to flush form fields, then compute options & previews
|
||||
setTimeout(() => {
|
||||
updateInsertOptions();
|
||||
updatePreviews();
|
||||
}, 0);
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
tableSelect: '__manual__',
|
||||
tableSelect: MANUAL_TABLE,
|
||||
protocolChoice: 'preset',
|
||||
protocolPreset: 'icmp',
|
||||
action: 'drop',
|
||||
chainSelect: '__manual_chain__',
|
||||
chainSelect: MANUAL_CHAIN,
|
||||
});
|
||||
setInsertBeforeOptions([]);
|
||||
setTimeout(() => updatePreviews(), 0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tableOptions]);
|
||||
// only when parent tables change
|
||||
}, [props.tables, tableOptions.length, updateInsertOptions]);
|
||||
|
||||
// update previews when form changes
|
||||
// preview update (reads form values)
|
||||
const updatePreviews = useCallback(() => {
|
||||
const vals = form.getFieldsValue();
|
||||
const expr = buildExprFromValues(vals);
|
||||
const values = form.getFieldsValue();
|
||||
const expr = buildExprFromValues(values);
|
||||
|
||||
const { family, table } = resolveTable(values);
|
||||
|
||||
// compute family/table/chain choices for cmd + request JSON
|
||||
let family = 'inet';
|
||||
let tableName = 'filter';
|
||||
let chain = 'input';
|
||||
const chainVal = values.chainSelect;
|
||||
if (!chainVal || chainVal === MANUAL_CHAIN) chain = values.chainManual || chain;
|
||||
else chain = chainVal;
|
||||
|
||||
const tableSelect = vals.tableSelect;
|
||||
if (tableSelect && tableSelect !== '__manual__') {
|
||||
const [f, n] = String(tableSelect).split(':');
|
||||
family = f || family;
|
||||
tableName = n || tableName;
|
||||
} else {
|
||||
family = vals.familyManual || family;
|
||||
tableName = vals.tableManual || tableName;
|
||||
}
|
||||
|
||||
const chainVal = vals.chainSelect;
|
||||
if (!chainVal || chainVal === '__manual_chain__') {
|
||||
chain = vals.chainManual || chain;
|
||||
} else {
|
||||
chain = chainVal;
|
||||
}
|
||||
|
||||
// compute insertion position if user selected an "insert after" handle
|
||||
let positionToken = '';
|
||||
if (
|
||||
vals.insertAfterHandle &&
|
||||
tableSelect &&
|
||||
tableSelect !== '__manual__' &&
|
||||
chain &&
|
||||
chain !== '__manual_chain__'
|
||||
) {
|
||||
// find index of the selected handle in current tables state
|
||||
try {
|
||||
const [f, n] = String(tableSelect).split(':');
|
||||
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||
if (tbl) {
|
||||
const ch = tbl.chains.find((c) => c.name === chain);
|
||||
if (ch) {
|
||||
const idx = ch.rules.findIndex((r) => String(r.handle) === String(vals.insertAfterHandle));
|
||||
if (idx >= 0) {
|
||||
const pos = idx + 1; // insert after -> index+1
|
||||
positionToken = ` position ${pos}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
positionToken = '';
|
||||
}
|
||||
}
|
||||
|
||||
const beforeHandle = values.insertBeforeHandle;
|
||||
const short = textFromExpr(expr);
|
||||
const cmd = `add rule ${family} ${tableName} ${chain}${positionToken} ${short}`.trim();
|
||||
setCmdPreview(cmd);
|
||||
const cmdFriendly = beforeHandle
|
||||
? `add rule ${family} ${table} ${chain} position ${beforeHandle} ${short}`
|
||||
: `add rule ${family} ${table} ${chain} ${short}`;
|
||||
setCmdPreview(cmdFriendly);
|
||||
|
||||
const reqObj: any = { family, table: tableName, chain, expr };
|
||||
if (positionToken) {
|
||||
// extract numeric pos and include in request body
|
||||
const m = positionToken.match(/position\s+(\d+)/);
|
||||
if (m) {
|
||||
reqObj.position = Number(m[1]);
|
||||
}
|
||||
const req: any = { family, table, chain, expr };
|
||||
if (beforeHandle != null && beforeHandle !== '') {
|
||||
// backend expects "position"
|
||||
req.position = Number(beforeHandle);
|
||||
}
|
||||
setRequestJsonPreview(reqObj);
|
||||
}, [form, tables]);
|
||||
setRequestJsonPreview(req);
|
||||
}, [form, resolveTable]);
|
||||
|
||||
// run once at mount to compute previews from initial values
|
||||
useEffect(() => {
|
||||
updatePreviews();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// keep preview updated on values change
|
||||
// when user changes form fields
|
||||
const onValuesChange = useCallback(() => {
|
||||
updatePreviews();
|
||||
|
||||
// when tableSelect changes, auto-select chain (first) if any
|
||||
const vals = form.getFieldsValue();
|
||||
const ts = vals.tableSelect;
|
||||
if (ts && ts !== '__manual__') {
|
||||
// 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 = tables.find((t) => t.family === f && t.name === n);
|
||||
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
|
||||
if (tbl) {
|
||||
if (tbl.chains && tbl.chains.length > 0) {
|
||||
// if currently no chain selected or manual, set to first available chain
|
||||
const cs = form.getFieldValue('chainSelect');
|
||||
if (!cs || cs === '__manual_chain__') {
|
||||
const currentChain = form.getFieldValue('chainSelect');
|
||||
if (!currentChain || currentChain === MANUAL_CHAIN) {
|
||||
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
|
||||
}
|
||||
} else {
|
||||
// if no chains, set chainSelect to manual so user can type
|
||||
form.setFieldsValue({ chainSelect: '__manual_chain__' });
|
||||
form.setFieldsValue({ chainSelect: MANUAL_CHAIN });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [form, tables, updatePreviews]);
|
||||
|
||||
// submit handler: build expr and call createRuleJson
|
||||
// recompute insert-before options when the user changes table/chain
|
||||
updateInsertOptions();
|
||||
}, [form, props.tables, updatePreviews, updateInsertOptions]);
|
||||
|
||||
// create handler — component will call props.refreshRules if provided; otherwise it will NOT fetch rules itself.
|
||||
const handleCreate = useCallback(
|
||||
async (values: any) => {
|
||||
const tableSelect = values.tableSelect;
|
||||
let family: string;
|
||||
let tableName: string;
|
||||
if (tableSelect === '__manual__' || !tableSelect) {
|
||||
family = values.familyManual || 'inet';
|
||||
tableName = values.tableManual || 'filter';
|
||||
} else {
|
||||
[family, tableName] = String(tableSelect).split(':');
|
||||
}
|
||||
|
||||
let chain: string | undefined = values.chainSelect;
|
||||
if (!chain || chain === '__manual_chain__') {
|
||||
chain = values.chainManual || 'input';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// compute optional position from insertAfterHandle
|
||||
const reqObj: any = { family, table: tableName, chain, expr };
|
||||
if (
|
||||
values.insertAfterHandle &&
|
||||
tableSelect &&
|
||||
tableSelect !== '__manual__' &&
|
||||
chain &&
|
||||
chain !== '__manual_chain__'
|
||||
) {
|
||||
// locate index of handle and set position = index+1
|
||||
const [f, n] = String(tableSelect).split(':');
|
||||
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||
if (tbl) {
|
||||
const ch = tbl.chains.find((c) => c.name === chain);
|
||||
if (ch) {
|
||||
const idx = ch.rules.findIndex((r) => String(r.handle) === String(values.insertAfterHandle));
|
||||
if (idx >= 0) {
|
||||
reqObj.position = idx + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 posStr = reqObj.position !== undefined ? ` position ${reqObj.position}` : '';
|
||||
const cmd = `add rule ${family} ${tableName} ${chain}${posStr} ${textual}`.trim();
|
||||
const previewHint = req.position ? ` ${req.position}` : '';
|
||||
const cmd = `add rule ${family} ${table} ${chain}${previewHint} ${textual}`.trim();
|
||||
|
||||
Modal.confirm({
|
||||
title: 'Create rule (JSON)',
|
||||
@@ -447,7 +415,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<Text>
|
||||
About to create rule in{' '}
|
||||
<b>
|
||||
{family}:{tableName} {chain}
|
||||
{family}:{table} {chain}
|
||||
</b>
|
||||
</Text>
|
||||
<Divider />
|
||||
@@ -455,20 +423,22 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{cmd || '(no textual preview available)'}</pre>
|
||||
<Divider />
|
||||
<Text strong>Full JSON NFT request:</Text>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{JSON.stringify(reqObj, null, 2)}</pre>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{JSON.stringify(req, null, 2)}</pre>
|
||||
</div>
|
||||
),
|
||||
okText: 'Create',
|
||||
onOk: async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await createRuleJson(reqObj);
|
||||
const res = await createRuleJson(req);
|
||||
if (res && res.rc === 0) {
|
||||
message.success('Rule created');
|
||||
// refresh tables/chains after successful creation
|
||||
await loadTables();
|
||||
if (onCreated) await onCreated();
|
||||
form.resetFields(['advanced', 'insertAfterHandle']);
|
||||
if (props.refreshRules) {
|
||||
await props.refreshRules();
|
||||
} else {
|
||||
message.info('Parent did not provide a refresh function; please refresh ruleset externally.');
|
||||
}
|
||||
if (props.onCreated) await props.onCreated();
|
||||
} else {
|
||||
message.error(`Create failed: ${res?.stderr ?? 'unknown error'}`);
|
||||
}
|
||||
@@ -480,12 +450,10 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
const rc = Number(data.rc ?? -1);
|
||||
const stderr = data.stderr ?? data;
|
||||
if (rc === 0) {
|
||||
message.warn(
|
||||
'Rule appears to have been created, but server returned an error status. Check output for details.',
|
||||
);
|
||||
await loadTables();
|
||||
if (onCreated) await onCreated();
|
||||
form.resetFields(['advanced', 'insertAfterHandle']);
|
||||
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}`);
|
||||
@@ -504,56 +472,41 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
},
|
||||
});
|
||||
},
|
||||
[form, onCreated, tables],
|
||||
[form, props.refreshRules, resolveTable],
|
||||
);
|
||||
|
||||
// prepare chain options for currently selected table
|
||||
const chainOptions = useMemo(() => {
|
||||
const ts = form.getFieldValue('tableSelect');
|
||||
if (ts && ts !== '__manual__') {
|
||||
const [f, n] = String(ts).split(':');
|
||||
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||
if (tbl && tbl.chains.length > 0) {
|
||||
return (
|
||||
<>
|
||||
{tbl.chains.map((c) => (
|
||||
<Option key={c.name} value={c.name}>
|
||||
{c.name}
|
||||
</Option>
|
||||
))}
|
||||
<Option key="__manual_chain__" value="__manual_chain__">
|
||||
Manual chain...
|
||||
</Option>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Option key="__manual_chain__" value="__manual_chain__">
|
||||
Manual chain...
|
||||
</Option>
|
||||
</>
|
||||
);
|
||||
}, [form, tables]);
|
||||
|
||||
// build insert-after options for currently selected table+chain
|
||||
const insertAfterOptions = useMemo(() => {
|
||||
const ts = form.getFieldValue('tableSelect');
|
||||
const cs = form.getFieldValue('chainSelect');
|
||||
if (!ts || ts === '__manual__' || !cs || cs === '__manual_chain__') return [];
|
||||
if (!ts || ts === MANUAL_TABLE) return null;
|
||||
const [f, n] = String(ts).split(':');
|
||||
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||
if (!tbl) return [];
|
||||
const ch = tbl.chains.find((c) => c.name === cs);
|
||||
if (!ch || !Array.isArray(ch.rules)) return [];
|
||||
return ch.rules
|
||||
.filter((r: any) => r && r.handle !== undefined && r.handle !== null)
|
||||
.map((r: any) => ({
|
||||
value: r.handle,
|
||||
label: `#${r.handle} — ${r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || r).slice(0, 120))}`,
|
||||
}));
|
||||
}, [form, tables]);
|
||||
const tbl = props.tables?.find((t) => t.family === f && t.name === n);
|
||||
if (!tbl) return null;
|
||||
return tbl.chains.map((c) => (
|
||||
<Option key={c.name} value={c.name}>
|
||||
{c.name}
|
||||
</Option>
|
||||
));
|
||||
}, [form, props.tables]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
if (props.refreshRules) {
|
||||
await props.refreshRules();
|
||||
message.success('Rules refresh requested');
|
||||
} else {
|
||||
message.info('No refresh function provided by parent.');
|
||||
}
|
||||
} catch (err) {
|
||||
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);
|
||||
}
|
||||
}, [props.refreshRules, updateInsertOptions]);
|
||||
|
||||
return (
|
||||
<Card style={{ maxWidth: 980 }}>
|
||||
@@ -566,9 +519,10 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
Builds an nft JSON <code>expr</code> and sends it to the server. If rendering fails, use the Raw UI.
|
||||
</Text>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadTables} loading={refreshing}>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleRefresh} loading={refreshing}>
|
||||
Refresh tables
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -584,7 +538,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
protocolChoice: 'preset',
|
||||
protocolPreset: 'icmp',
|
||||
action: 'drop',
|
||||
tableSelect: tableOptions.length > 0 ? `${tableOptions[0].family}:${tableOptions[0].name}` : '__manual__',
|
||||
tableSelect: tableOptions.length > 0 ? tableOptions[0].value : MANUAL_TABLE,
|
||||
}}
|
||||
onFinish={handleCreate}
|
||||
onValuesChange={onValuesChange}
|
||||
@@ -595,11 +549,11 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<Form.Item name="tableSelect" label="Table (family:name)" rules={[{ required: true }]}>
|
||||
<Select>
|
||||
{tableOptions.map((t) => (
|
||||
<Option key={`${t.family}:${t.name}`} value={`${t.family}:${t.name}`}>
|
||||
<Option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</Option>
|
||||
))}
|
||||
<Option key="__manual__" value="__manual__">
|
||||
<Option key={MANUAL_TABLE} value={MANUAL_TABLE}>
|
||||
Manual / new table...
|
||||
</Option>
|
||||
</Select>
|
||||
@@ -607,7 +561,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12}>
|
||||
{form.getFieldValue('tableSelect') === '__manual__' ? (
|
||||
{form.getFieldValue('tableSelect') === MANUAL_TABLE ? (
|
||||
<Space style={{ width: '100%' }}>
|
||||
<Form.Item name="familyManual" label="Family" style={{ flex: 1 }}>
|
||||
<Input placeholder="inet, bridge, ip, ip6 ..." />
|
||||
@@ -628,13 +582,19 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item name="chainSelect" label="Chain (select)" rules={[{ required: true }]}>
|
||||
<Select>{chainOptions}</Select>
|
||||
<Select>
|
||||
{chainOptions ?? (
|
||||
<Option key={MANUAL_CHAIN} value={MANUAL_CHAIN}>
|
||||
Manual chain...
|
||||
</Option>
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12}>
|
||||
{form.getFieldValue('chainSelect') === '__manual_chain__' ||
|
||||
form.getFieldValue('tableSelect') === '__manual__' ? (
|
||||
{form.getFieldValue('chainSelect') === MANUAL_CHAIN ||
|
||||
form.getFieldValue('tableSelect') === MANUAL_TABLE ? (
|
||||
<Form.Item name="chainManual" label="Chain (manual)">
|
||||
<Input placeholder="input, forward, output" />
|
||||
</Form.Item>
|
||||
@@ -646,24 +606,24 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Insert-after control */}
|
||||
{form.getFieldValue('tableSelect') !== '__manual__' &&
|
||||
{/* Insert-before control */}
|
||||
{form.getFieldValue('tableSelect') !== MANUAL_TABLE &&
|
||||
form.getFieldValue('chainSelect') &&
|
||||
form.getFieldValue('chainSelect') !== '__manual_chain__' && (
|
||||
form.getFieldValue('chainSelect') !== MANUAL_CHAIN && (
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item
|
||||
name="insertAfterHandle"
|
||||
label="Insert after (optional)"
|
||||
help="Pick an existing rule handle to insert *after*. If left empty the rule will be appended."
|
||||
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-after)">
|
||||
{insertAfterOptions.length === 0 ? (
|
||||
<Select allowClear placeholder="Append (no insert-before)">
|
||||
{insertBeforeOptions.length === 0 ? (
|
||||
<Option value="__none__" disabled>
|
||||
(no rules available)
|
||||
</Option>
|
||||
) : (
|
||||
insertAfterOptions.map((o: any) => (
|
||||
insertBeforeOptions.map((o) => (
|
||||
<Option key={String(o.value)} value={o.value}>
|
||||
{o.label}
|
||||
</Option>
|
||||
@@ -672,12 +632,6 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<Text type="secondary">
|
||||
Use when you want the new rule to appear right after a known handle. Refresh tables to see latest
|
||||
handles.
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -695,11 +649,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{form.getFieldValue('protocolChoice') === 'custom' ? (
|
||||
<Form.Item name="protocolCustom" label="Protocol (custom)">
|
||||
<Input placeholder="e.g. icmp, icmpv6, tcp, udp, meta ..." />
|
||||
</Form.Item>
|
||||
) : (
|
||||
{isPresetProtocolChoice(form.getFieldsValue()) ? (
|
||||
<Form.Item name="protocolPreset" label="Protocol (preset)">
|
||||
<Select>
|
||||
<Option value="">(none)</Option>
|
||||
@@ -709,6 +659,10 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<Option value="udp">UDP</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item name="protocolCustom" label="Protocol (custom)">
|
||||
<Input placeholder="e.g. icmp, icmpv6, tcp, udp, meta ..." />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
@@ -732,11 +686,13 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<InputNumber style={{ width: '100%' }} min={1} max={65535} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={6}>
|
||||
<Form.Item name="dport" label="Dest Port">
|
||||
<InputNumber style={{ width: '100%' }} min={1} max={65535} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item name="action" label="Action" rules={[{ required: true }]} initialValue="drop">
|
||||
<Radio.Group>
|
||||
@@ -796,12 +752,12 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
return;
|
||||
}
|
||||
if (!cmdPreview) return;
|
||||
// copy the textual preview as plain text
|
||||
navigator.clipboard?.writeText(cmdPreview);
|
||||
message.success('Command preview copied');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button type="primary" htmlType="submit" loading={loading} icon={<PlusOutlined />}>
|
||||
Create rule
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user