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:
@@ -154,6 +154,19 @@ class NftManager:
|
|||||||
tokens.append("accept")
|
tokens.append("accept")
|
||||||
elif "counter" in part:
|
elif "counter" in part:
|
||||||
tokens.append("counter")
|
tokens.append("counter")
|
||||||
|
elif "queue" in part:
|
||||||
|
# handle fallback queue textualization
|
||||||
|
q = part["queue"]
|
||||||
|
if isinstance(q, dict):
|
||||||
|
num = q.get("num") or q.get("number") or q.get("range") or q.get("from")
|
||||||
|
tok = "queue"
|
||||||
|
if num is not None:
|
||||||
|
tok += f" num{num}"
|
||||||
|
if q.get("bypass"):
|
||||||
|
tok += " bypass"
|
||||||
|
tokens.append(tok)
|
||||||
|
else:
|
||||||
|
tokens.append(f"queue{q}")
|
||||||
else:
|
else:
|
||||||
tokens.append("+".join(part.keys()))
|
tokens.append("+".join(part.keys()))
|
||||||
rule_lines.append(" ".join(tokens))
|
rule_lines.append(" ".join(tokens))
|
||||||
@@ -240,10 +253,13 @@ class CreateRuleRequest(BaseModel):
|
|||||||
class Config:
|
class Config:
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
"example": {
|
"example": {
|
||||||
"family": "bridge",
|
"family": "inet",
|
||||||
"table": "filter",
|
"table": "filter",
|
||||||
"chain": "forward",
|
"chain": "input",
|
||||||
"expr": [{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, {"drop": None}],
|
"expr": [
|
||||||
|
{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}},
|
||||||
|
{"queue": {"num": 0, "bypass": True}}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +318,7 @@ def parse_priority(val: Any) -> Optional[int]:
|
|||||||
return p
|
return p
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def rule_text_from_expr(expr: Any) -> str:
|
def rule_text_from_expr(expr: Any) -> str:
|
||||||
"""
|
"""
|
||||||
Deterministic serializer to produce a compact UI-friendly string from expr list.
|
Deterministic serializer to produce a compact UI-friendly string from expr list.
|
||||||
@@ -314,6 +331,23 @@ def rule_text_from_expr(expr: Any) -> str:
|
|||||||
tokens: List[str] = []
|
tokens: List[str] = []
|
||||||
for part in expr:
|
for part in expr:
|
||||||
if isinstance(part, dict):
|
if isinstance(part, dict):
|
||||||
|
# queue handling: support {'queue': 0}, {'queue': '0-3'}, {'queue': {'num': 0, 'bypass': True}}
|
||||||
|
if "queue" in part:
|
||||||
|
q = part["queue"]
|
||||||
|
token = "queue"
|
||||||
|
if isinstance(q, dict):
|
||||||
|
num = q.get("num") or q.get("number") or q.get("queue_number") or q.get("from") or q.get("range")
|
||||||
|
if num is not None:
|
||||||
|
token += f" num{num}"
|
||||||
|
if q.get("bypass"):
|
||||||
|
token += " bypass"
|
||||||
|
elif isinstance(q, (int, float)):
|
||||||
|
token += f" num{int(q)}"
|
||||||
|
elif isinstance(q, str):
|
||||||
|
token += f" num{q}"
|
||||||
|
tokens.append(token)
|
||||||
|
continue
|
||||||
|
|
||||||
# common tokens
|
# common tokens
|
||||||
if "match" in part:
|
if "match" in part:
|
||||||
m = part["match"]
|
m = part["match"]
|
||||||
@@ -393,10 +427,11 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
# extract metadata robustly
|
# extract metadata robustly
|
||||||
ch_type = ch.get("type")
|
ch_type = ch.get("type")
|
||||||
ch_hook = ch.get("hook")
|
ch_hook = ch.get("hook")
|
||||||
ch_priority = parse_priority(ch.get("priority") if "priority" in ch else ch.get("prio") if "prio" in ch else ch.get("prio", None))
|
# priority may be provided in several forms; try them
|
||||||
# also attempt to parse nested shapes if present (some nft JSON variations)
|
ch_priority = parse_priority(ch.get("priority") if "priority" in ch else ch.get("prio") if "prio" in ch else ch.get("priority", None))
|
||||||
if ch_priority is None:
|
# further attempt if ch_hook is dict
|
||||||
ch_priority = parse_priority(ch.get("hook") if isinstance(ch.get("hook"), dict) else None)
|
if ch_priority is None and isinstance(ch.get("hook"), dict):
|
||||||
|
ch_priority = parse_priority(ch.get("hook").get("priority") if ch.get("hook") else None)
|
||||||
|
|
||||||
ch_policy = ch.get("policy")
|
ch_policy = ch.get("policy")
|
||||||
|
|
||||||
@@ -447,16 +482,12 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
chains_map[chain_name]["rules"].append(rule_obj)
|
chains_map[chain_name]["rules"].append(rule_obj)
|
||||||
|
|
||||||
# Attempt to salvage chain metadata from rule record if present
|
# Attempt to salvage chain metadata from rule record if present
|
||||||
# some nft JSON may include 'chain' subfields inside rule record
|
|
||||||
# e.g. r.get('chain') might be an object - handle that defensively
|
|
||||||
if isinstance(r.get("chain"), dict):
|
if isinstance(r.get("chain"), dict):
|
||||||
csub = r.get("chain")
|
csub = r.get("chain")
|
||||||
# try to parse nested priority
|
|
||||||
if chains_map[chain_name].get("priority") is None:
|
if chains_map[chain_name].get("priority") is None:
|
||||||
parsed_prio = parse_priority(csub.get("priority") if "priority" in csub else csub.get("prio"))
|
parsed_prio = parse_priority(csub.get("priority") if "priority" in csub else csub.get("prio"))
|
||||||
if parsed_prio is not None:
|
if parsed_prio is not None:
|
||||||
chains_map[chain_name]["priority"] = parsed_prio
|
chains_map[chain_name]["priority"] = parsed_prio
|
||||||
# type/hook/policy from nested if present
|
|
||||||
if chains_map[chain_name].get("type") is None and csub.get("type") is not None:
|
if chains_map[chain_name].get("type") is None and csub.get("type") is not None:
|
||||||
chains_map[chain_name]["type"] = csub.get("type")
|
chains_map[chain_name]["type"] = csub.get("type")
|
||||||
if chains_map[chain_name].get("hook") is None and csub.get("hook") is not None:
|
if chains_map[chain_name].get("hook") is None and csub.get("hook") is not None:
|
||||||
@@ -491,12 +522,10 @@ def expr_to_text(expr: Any) -> Optional[str]:
|
|||||||
Best-effort renderer that converts a typical nft JSON expr (list) into a textual
|
Best-effort renderer that converts a typical nft JSON expr (list) into a textual
|
||||||
fragment suitable to append to 'add rule <family> <table> <chain> ...'.
|
fragment suitable to append to 'add rule <family> <table> <chain> ...'.
|
||||||
Returns None when it cannot deterministically render the provided expr.
|
Returns None when it cannot deterministically render the provided expr.
|
||||||
Supported cases (common):
|
Supports queue + bypass:
|
||||||
- [{'match': {'left': {'payload': {'protocol':'ip','field':'protocol'}}, 'op':'==', 'right':'icmp'}}, {'drop': None}]
|
{'queue': 0} -> "queue num0"
|
||||||
-> 'ip protocol icmp drop'
|
{'queue': '0-3'} -> "queue num0-3"
|
||||||
- payload / tcp / udp / counter / accept
|
{'queue': {'num': 0, 'bypass': True}} -> "queue num0 bypass"
|
||||||
- simple dicts where keys are 'drop' | 'accept' | 'counter'
|
|
||||||
This intentionally does not attempt to support every nft JSON construct.
|
|
||||||
"""
|
"""
|
||||||
if expr is None:
|
if expr is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -520,6 +549,23 @@ def expr_to_text(expr: Any) -> Optional[str]:
|
|||||||
parts.append("counter")
|
parts.append("counter")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# queue support (NEW)
|
||||||
|
if "queue" in element:
|
||||||
|
q = element["queue"]
|
||||||
|
token = "queue"
|
||||||
|
if isinstance(q, dict):
|
||||||
|
num = q.get("num") or q.get("number") or q.get("queue_number") or q.get("from") or q.get("range")
|
||||||
|
if num is not None:
|
||||||
|
token += f" num{num}"
|
||||||
|
if q.get("bypass"):
|
||||||
|
token += " bypass"
|
||||||
|
elif isinstance(q, (int, float)):
|
||||||
|
token += f" num{int(q)}"
|
||||||
|
elif isinstance(q, str):
|
||||||
|
token += f" num{q}"
|
||||||
|
parts.append(token)
|
||||||
|
continue
|
||||||
|
|
||||||
# match left/right payload equals -> ip protocol icmp, or ip saddr/daddr
|
# match left/right payload equals -> ip protocol icmp, or ip saddr/daddr
|
||||||
if "match" in element:
|
if "match" in element:
|
||||||
m = element["match"]
|
m = element["match"]
|
||||||
@@ -532,11 +578,9 @@ def expr_to_text(expr: Any) -> Optional[str]:
|
|||||||
field = p.get("field")
|
field = p.get("field")
|
||||||
# common: protocol field match (protocol == icmp)
|
# common: protocol field match (protocol == icmp)
|
||||||
if prot and field and isinstance(right, str):
|
if prot and field and isinstance(right, str):
|
||||||
# ip vs ip6 decision is left to the frontend; here we render 'ip protocol icmp' (works for many setups)
|
|
||||||
if field == "protocol":
|
if field == "protocol":
|
||||||
parts.append(f"{prot} {field} {right}")
|
parts.append(f"{prot} {field} {right}")
|
||||||
continue
|
continue
|
||||||
# payload might be l4 ports etc; produce generic payload(...) token
|
|
||||||
parts.append(f"payload({prot}.{field}) {right}")
|
parts.append(f"payload({prot}.{field}) {right}")
|
||||||
continue
|
continue
|
||||||
# fallback for match: try to stringify right
|
# fallback for match: try to stringify right
|
||||||
@@ -687,11 +731,8 @@ def create_rule_json(req: CreateRuleRequest):
|
|||||||
logger.warning("create_rule_json: failed to list chain for verification: %s", e_chain)
|
logger.warning("create_rule_json: failed to list chain for verification: %s", e_chain)
|
||||||
|
|
||||||
# If we reach here -> treat as error: return 400 with exec_res in body.
|
# If we reach here -> treat as error: return 400 with exec_res in body.
|
||||||
# FastAPI cannot both raise HTTPException and include ExecResult as body easily, so raise HTTPException
|
|
||||||
# with detail that includes stderr and the executed cmd.
|
|
||||||
detail = f"nft command failed rc={rc}. stderr: {stderr!r}. cmd: {cmd}"
|
detail = f"nft command failed rc={rc}. stderr: {stderr!r}. cmd: {cmd}"
|
||||||
logger.warning("create_rule_json failed: %s", detail)
|
logger.warning("create_rule_json failed: %s", detail)
|
||||||
# Return an HTTPException with the detail (frontend can still inspect error.response.data if ExecResult was included)
|
|
||||||
raise HTTPException(status_code=400, detail=detail)
|
raise HTTPException(status_code=400, detail=detail)
|
||||||
|
|
||||||
except NftError as e:
|
except NftError as e:
|
||||||
@@ -705,7 +746,6 @@ def create_rule_json(req: CreateRuleRequest):
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/rules/{handle}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete rule by handle")
|
@router.delete("/rules/{handle}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete rule by handle")
|
||||||
def delete_rule(handle: int, family: str = "inet", table: str = "filter", chain: str = "input"):
|
def delete_rule(handle: int, family: str = "inet", table: str = "filter", chain: str = "input"):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/components/RuleBuilder.tsx
|
// src/components/RuleBuilder.tsx
|
||||||
import { CopyOutlined, PlusOutlined } from '@ant-design/icons';
|
import { CopyOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -82,7 +82,13 @@ function buildExprFromValues(values: any): Expr[] {
|
|||||||
} else if (/udp/i.test(custom)) {
|
} else if (/udp/i.test(custom)) {
|
||||||
expr.push({ udp: {} });
|
expr.push({ udp: {} });
|
||||||
} else {
|
} 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 });
|
if (action === 'drop') expr.push({ drop: null });
|
||||||
else if (action === 'accept') expr.push({ accept: null });
|
else if (action === 'accept') expr.push({ accept: null });
|
||||||
else if (action === 'reject') expr.push({ reject: 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;
|
return expr;
|
||||||
}
|
}
|
||||||
@@ -186,8 +202,14 @@ function textFromExpr(expr: Expr): string {
|
|||||||
tokens.push('drop');
|
tokens.push('drop');
|
||||||
} else if ('accept' in part) {
|
} else if ('accept' in part) {
|
||||||
tokens.push('accept');
|
tokens.push('accept');
|
||||||
|
} else if ('reject' in part) {
|
||||||
|
tokens.push('reject');
|
||||||
} else if ('counter' in part) {
|
} else if ('counter' in part) {
|
||||||
tokens.push('counter');
|
tokens.push('counter');
|
||||||
|
} else if ('queue' in part) {
|
||||||
|
// show NFQUEUE / queue
|
||||||
|
const q = (part as any).queue;
|
||||||
|
tokens.push(`queue ${q}`);
|
||||||
} else {
|
} else {
|
||||||
const keys = Object.keys(part).sort().join('+');
|
const keys = Object.keys(part).sort().join('+');
|
||||||
tokens.push(keys);
|
tokens.push(keys);
|
||||||
@@ -210,26 +232,31 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
|
|
||||||
const [cmdPreview, setCmdPreview] = useState<string>('');
|
const [cmdPreview, setCmdPreview] = useState<string>('');
|
||||||
const [requestJsonPreview, setRequestJsonPreview] = useState<any>(null);
|
const [requestJsonPreview, setRequestJsonPreview] = useState<any>(null);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
// fetch tables/chains on mount
|
async function loadTables() {
|
||||||
useEffect(() => {
|
setRefreshing(true);
|
||||||
setLoading(true);
|
try {
|
||||||
fetchRuleset()
|
const res = await fetchRuleset();
|
||||||
.then((res) => {
|
if (!res || res.ruleset === null) {
|
||||||
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([]);
|
setTables([]);
|
||||||
})
|
} else if (typeof res.ruleset === 'string') {
|
||||||
.finally(() => setLoading(false));
|
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(
|
const tableOptions = useMemo(
|
||||||
@@ -237,7 +264,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
[tables],
|
[tables],
|
||||||
);
|
);
|
||||||
|
|
||||||
// set initial defaults
|
// set sensible defaults once tables are known
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tableOptions.length > 0) {
|
if (tableOptions.length > 0) {
|
||||||
const first = tableOptions[0];
|
const first = tableOptions[0];
|
||||||
@@ -247,12 +274,20 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
protocolPreset: 'icmp',
|
protocolPreset: 'icmp',
|
||||||
action: 'drop',
|
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 {
|
} else {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
tableSelect: '__manual__',
|
tableSelect: '__manual__',
|
||||||
protocolChoice: 'preset',
|
protocolChoice: 'preset',
|
||||||
protocolPreset: 'icmp',
|
protocolPreset: 'icmp',
|
||||||
action: 'drop',
|
action: 'drop',
|
||||||
|
chainSelect: '__manual_chain__',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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
|
// keep preview updated on values change
|
||||||
const onValuesChange = useCallback(() => {
|
const onValuesChange = useCallback(() => {
|
||||||
updatePreviews();
|
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
|
// submit handler: build expr and call createRuleJson
|
||||||
const handleCreate = useCallback(
|
const handleCreate = useCallback(
|
||||||
@@ -322,7 +377,8 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expr = buildExprFromValues(values);
|
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 };
|
const reqObj = { family, table: tableName, chain, expr };
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
@@ -350,6 +406,8 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
const res = await createRuleJson(reqObj);
|
const res = await createRuleJson(reqObj);
|
||||||
if (res && res.rc === 0) {
|
if (res && res.rc === 0) {
|
||||||
message.success('Rule created');
|
message.success('Rule created');
|
||||||
|
// refresh tables/chains after successful creation
|
||||||
|
await loadTables();
|
||||||
if (onCreated) await onCreated();
|
if (onCreated) await onCreated();
|
||||||
form.resetFields(['advanced']);
|
form.resetFields(['advanced']);
|
||||||
} else {
|
} else {
|
||||||
@@ -366,6 +424,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
message.warn(
|
message.warn(
|
||||||
'Rule appears to have been created, but server returned an error status. Check output for details.',
|
'Rule appears to have been created, but server returned an error status. Check output for details.',
|
||||||
);
|
);
|
||||||
|
await loadTables();
|
||||||
if (onCreated) await onCreated();
|
if (onCreated) await onCreated();
|
||||||
form.resetFields(['advanced']);
|
form.resetFields(['advanced']);
|
||||||
} else {
|
} else {
|
||||||
@@ -396,27 +455,48 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
const [f, n] = String(ts).split(':');
|
const [f, n] = String(ts).split(':');
|
||||||
const tbl = tables.find((t) => t.family === f && t.name === n);
|
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||||
if (tbl && tbl.chains.length > 0) {
|
if (tbl && tbl.chains.length > 0) {
|
||||||
return tbl.chains.map((c) => (
|
return (
|
||||||
<Option key={c.name} value={c.name}>
|
<>
|
||||||
{c.name}
|
{tbl.chains.map((c) => (
|
||||||
</Option>
|
<Option key={c.name} value={c.name}>
|
||||||
));
|
{c.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
<Option key="__manual_chain__" value="__manual_chain__">
|
||||||
|
Manual chain...
|
||||||
|
</Option>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [
|
return (
|
||||||
<Option key="__manual_chain__" value="__manual_chain__">
|
<>
|
||||||
Manual chain...
|
<Option key="__manual_chain__" value="__manual_chain__">
|
||||||
</Option>,
|
Manual chain...
|
||||||
];
|
</Option>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}, [form, tables]);
|
}, [form, tables]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card style={{ maxWidth: 980 }}>
|
<Card style={{ maxWidth: 980 }}>
|
||||||
<Title level={4}>Add Firewall Rule (JSON)</Title>
|
<Row justify="space-between" align="middle">
|
||||||
<Text type="secondary">
|
<Col>
|
||||||
This builder constructs a native nft JSON <code>expr</code> and sends it to the server. If the server cannot
|
<Title level={4} style={{ margin: 0 }}>
|
||||||
render your expression to the system's nft syntax, use the Raw command UI instead.
|
Add Firewall Rule (JSON)
|
||||||
</Text>
|
</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 />
|
<Divider />
|
||||||
|
|
||||||
@@ -470,7 +550,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
{/* Chain selection */}
|
{/* Chain selection */}
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} sm={12}>
|
<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>
|
<Select>{chainOptions}</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -505,7 +585,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
|
|
||||||
{form.getFieldValue('protocolChoice') === 'custom' ? (
|
{form.getFieldValue('protocolChoice') === 'custom' ? (
|
||||||
<Form.Item name="protocolCustom" label="Protocol (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>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item name="protocolPreset" label="Protocol (preset)">
|
<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="drop">Drop</Radio>
|
||||||
<Radio value="accept">Accept</Radio>
|
<Radio value="accept">Accept</Radio>
|
||||||
<Radio value="reject">Reject</Radio>
|
<Radio value="reject">Reject</Radio>
|
||||||
|
<Radio value="nfqueue">NFQUEUE</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</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>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
@@ -562,7 +649,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
<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). Use Raw endpoint for complex commands."
|
placeholder='Append arbitrary JSON token or text (advanced). 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 expr as a token. Prefer structured fields where possible.
|
||||||
@@ -585,20 +672,21 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ width: 320 }}>
|
<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
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={(e) => {
|
style={{ marginRight: 8 }}
|
||||||
// shiftKey => copy full request JSON
|
onClick={(e: any) => {
|
||||||
if (e.shiftKey && requestJsonPreview) {
|
if (e.shiftKey && requestJsonPreview) {
|
||||||
navigator.clipboard?.writeText(JSON.stringify(requestJsonPreview, null, 2));
|
navigator.clipboard?.writeText(JSON.stringify(requestJsonPreview, null, 2));
|
||||||
message.success('Full request JSON copied');
|
message.success('Full request JSON copied');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!cmdPreview) return;
|
if (!cmdPreview) return;
|
||||||
navigator.clipboard?.writeText(JSON.stringify(cmdPreview, null, 2));
|
// copy the textual preview as plain text
|
||||||
message.success('Expression JSON copied');
|
navigator.clipboard?.writeText(cmdPreview);
|
||||||
|
message.success('Command preview copied');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -612,9 +700,11 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text strong>Full JSON NFT request:</Text>
|
<Text strong>Full JSON NFT request:</Text>
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', background: '#f6f6f6', padding: 12 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
{JSON.stringify(requestJsonPreview, null, 2)}
|
<Card size="small" type="inner" style={{ background: '#fafafa' }}>
|
||||||
</pre>
|
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{JSON.stringify(requestJsonPreview, null, 2)}</pre>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -416,7 +416,8 @@ export default function FirewallTables(): ReactElement {
|
|||||||
<div>
|
<div>
|
||||||
<Text strong>{chain.name}</Text>
|
<Text strong>{chain.name}</Text>
|
||||||
<Text type="secondary" style={{ marginLeft: 12 }}>
|
<Text type="secondary" style={{ marginLeft: 12 }}>
|
||||||
<b>Hook:</b> {chain.hook ?? '-'} <b>Policy:</b> {chain.policy ?? '-'}
|
<b>Hook:</b> {chain.hook ?? '-'} <b>Type:</b> {chain.type ?? '-'} {' '}
|
||||||
|
<b>Policy:</b> {chain.policy ?? '-'} <b>Priority:</b> {chain.priority ?? '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user