nfqueue added
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// src/components/RuleBuilder.tsx
|
||||
import { CopyOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { CopyOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -82,7 +82,13 @@ function buildExprFromValues(values: any): Expr[] {
|
||||
} else if (/udp/i.test(custom)) {
|
||||
expr.push({ udp: {} });
|
||||
} else {
|
||||
expr.push(custom);
|
||||
// allow user to paste a JSON token string (advanced)
|
||||
try {
|
||||
const parsed = JSON.parse(custom);
|
||||
expr.push(parsed);
|
||||
} catch {
|
||||
expr.push(custom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +145,16 @@ function buildExprFromValues(values: any): Expr[] {
|
||||
if (action === 'drop') expr.push({ drop: null });
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
@@ -186,8 +202,14 @@ function textFromExpr(expr: Expr): string {
|
||||
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);
|
||||
@@ -210,26 +232,31 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
|
||||
const [cmdPreview, setCmdPreview] = useState<string>('');
|
||||
const [requestJsonPreview, setRequestJsonPreview] = useState<any>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// fetch tables/chains on mount
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchRuleset()
|
||||
.then((res) => {
|
||||
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.');
|
||||
async function loadTables() {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
const res = await fetchRuleset();
|
||||
if (!res || res.ruleset === null) {
|
||||
setTables([]);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch on mount
|
||||
useEffect(() => {
|
||||
loadTables();
|
||||
}, []);
|
||||
|
||||
const tableOptions = useMemo(
|
||||
@@ -237,7 +264,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
[tables],
|
||||
);
|
||||
|
||||
// set initial defaults
|
||||
// set sensible defaults once tables are known
|
||||
useEffect(() => {
|
||||
if (tableOptions.length > 0) {
|
||||
const first = tableOptions[0];
|
||||
@@ -247,12 +274,20 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
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);
|
||||
if (tbl && tbl.chains && tbl.chains.length > 0) {
|
||||
form.setFieldsValue({ chainSelect: tbl.chains[0].name });
|
||||
} else {
|
||||
form.setFieldsValue({ chainSelect: '__manual_chain__' });
|
||||
}
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
tableSelect: '__manual__',
|
||||
protocolChoice: 'preset',
|
||||
protocolPreset: 'icmp',
|
||||
action: 'drop',
|
||||
chainSelect: '__manual_chain__',
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -301,7 +336,27 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
// keep preview updated on values change
|
||||
const onValuesChange = useCallback(() => {
|
||||
updatePreviews();
|
||||
}, [updatePreviews]);
|
||||
|
||||
// when tableSelect changes, auto-select chain (first) if any
|
||||
const vals = form.getFieldsValue();
|
||||
const ts = vals.tableSelect;
|
||||
if (ts && ts !== '__manual__') {
|
||||
const [f, n] = String(ts).split(':');
|
||||
const tbl = 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__') {
|
||||
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, tables, updatePreviews]);
|
||||
|
||||
// submit handler: build expr and call createRuleJson
|
||||
const handleCreate = useCallback(
|
||||
@@ -322,7 +377,8 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
}
|
||||
|
||||
const expr = buildExprFromValues(values);
|
||||
const cmd = `add rule ${family} ${tableName} ${chain} ${textFromExpr(expr)}`.trim();
|
||||
const textual = textFromExpr(expr);
|
||||
const cmd = `add rule ${family} ${tableName} ${chain} ${textual}`.trim();
|
||||
const reqObj = { family, table: tableName, chain, expr };
|
||||
|
||||
Modal.confirm({
|
||||
@@ -350,6 +406,8 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
const res = await createRuleJson(reqObj);
|
||||
if (res && res.rc === 0) {
|
||||
message.success('Rule created');
|
||||
// refresh tables/chains after successful creation
|
||||
await loadTables();
|
||||
if (onCreated) await onCreated();
|
||||
form.resetFields(['advanced']);
|
||||
} else {
|
||||
@@ -366,6 +424,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
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']);
|
||||
} else {
|
||||
@@ -396,27 +455,48 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
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>
|
||||
));
|
||||
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>,
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Option key="__manual_chain__" value="__manual_chain__">
|
||||
Manual chain...
|
||||
</Option>
|
||||
</>
|
||||
);
|
||||
}, [form, tables]);
|
||||
|
||||
return (
|
||||
<Card style={{ maxWidth: 980 }}>
|
||||
<Title level={4}>Add Firewall Rule (JSON)</Title>
|
||||
<Text type="secondary">
|
||||
This builder constructs a native nft JSON <code>expr</code> and sends it to the server. If the server cannot
|
||||
render your expression to the system's nft syntax, use the Raw command UI instead.
|
||||
</Text>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
Add Firewall Rule (JSON)
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
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}>
|
||||
Refresh tables
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
@@ -470,7 +550,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
{/* Chain selection */}
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Form.Item name="chainSelect" label="Chain (select)">
|
||||
<Form.Item name="chainSelect" label="Chain (select)" rules={[{ required: true }]}>
|
||||
<Select>{chainOptions}</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -505,7 +585,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
|
||||
{form.getFieldValue('protocolChoice') === 'custom' ? (
|
||||
<Form.Item name="protocolCustom" label="Protocol (custom)">
|
||||
<Input placeholder="e.g. icmp, icmpv6, meta l4proto tcp" />
|
||||
<Input placeholder="e.g. icmp, icmpv6, tcp, udp, meta ..." />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item name="protocolPreset" label="Protocol (preset)">
|
||||
@@ -551,8 +631,15 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<Radio value="drop">Drop</Radio>
|
||||
<Radio value="accept">Accept</Radio>
|
||||
<Radio value="reject">Reject</Radio>
|
||||
<Radio value="nfqueue">NFQUEUE</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{form.getFieldValue('action') === 'nfqueue' && (
|
||||
<Form.Item name="nfqueue" label="Queue number" initialValue={1}>
|
||||
<InputNumber style={{ width: '100%' }} min={1} max={65535} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -562,7 +649,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
<Form.Item name="advanced" label="Advanced (optional)">
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder="Append arbitrary JSON token or text (advanced). Use Raw endpoint for complex commands."
|
||||
placeholder='Append arbitrary JSON token or text (advanced). Example: {"limit": {"rate":"5/second"}}'
|
||||
/>
|
||||
<Text type="secondary">
|
||||
Advanced is appended to the expr as a token. Prefer structured fields where possible.
|
||||
@@ -585,20 +672,21 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
</div>
|
||||
|
||||
<div style={{ width: 320 }}>
|
||||
<Tooltip title="Click to copy expr JSON. Shift+click to copy full request JSON.">
|
||||
<Tooltip title="Click to copy expr JSON. Shift+Click copies full request JSON.">
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
// shiftKey => copy full request JSON
|
||||
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;
|
||||
}
|
||||
if (!cmdPreview) return;
|
||||
navigator.clipboard?.writeText(JSON.stringify(cmdPreview, null, 2));
|
||||
message.success('Expression JSON copied');
|
||||
// copy the textual preview as plain text
|
||||
navigator.clipboard?.writeText(cmdPreview);
|
||||
message.success('Command preview copied');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -612,9 +700,11 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
||||
|
||||
<Divider />
|
||||
<Text strong>Full JSON NFT request:</Text>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', background: '#f6f6f6', padding: 12 }}>
|
||||
{JSON.stringify(requestJsonPreview, null, 2)}
|
||||
</pre>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user