nfqueue added
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s

This commit is contained in:
2026-02-28 14:25:08 +01:00
parent 397ec24875
commit 87d950e2ab
3 changed files with 204 additions and 73 deletions

View File

@@ -154,6 +154,19 @@ class NftManager:
tokens.append("accept")
elif "counter" in part:
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:
tokens.append("+".join(part.keys()))
rule_lines.append(" ".join(tokens))
@@ -240,10 +253,13 @@ class CreateRuleRequest(BaseModel):
class Config:
schema_extra = {
"example": {
"family": "bridge",
"family": "inet",
"table": "filter",
"chain": "forward",
"expr": [{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, {"drop": None}],
"chain": "input",
"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 None
def rule_text_from_expr(expr: Any) -> str:
"""
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] = []
for part in expr:
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
if "match" in part:
m = part["match"]
@@ -393,10 +427,11 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
# extract metadata robustly
ch_type = ch.get("type")
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))
# also attempt to parse nested shapes if present (some nft JSON variations)
if ch_priority is None:
ch_priority = parse_priority(ch.get("hook") if isinstance(ch.get("hook"), dict) else None)
# priority may be provided in several forms; try them
ch_priority = parse_priority(ch.get("priority") if "priority" in ch else ch.get("prio") if "prio" in ch else ch.get("priority", None))
# further attempt if ch_hook is dict
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")
@@ -447,16 +482,12 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
chains_map[chain_name]["rules"].append(rule_obj)
# 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):
csub = r.get("chain")
# try to parse nested priority
if chains_map[chain_name].get("priority") is None:
parsed_prio = parse_priority(csub.get("priority") if "priority" in csub else csub.get("prio"))
if parsed_prio is not None:
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:
chains_map[chain_name]["type"] = csub.get("type")
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
fragment suitable to append to 'add rule <family> <table> <chain> ...'.
Returns None when it cannot deterministically render the provided expr.
Supported cases (common):
- [{'match': {'left': {'payload': {'protocol':'ip','field':'protocol'}}, 'op':'==', 'right':'icmp'}}, {'drop': None}]
-> 'ip protocol icmp drop'
- payload / tcp / udp / counter / accept
- simple dicts where keys are 'drop' | 'accept' | 'counter'
This intentionally does not attempt to support every nft JSON construct.
Supports queue + bypass:
{'queue': 0} -> "queue num0"
{'queue': '0-3'} -> "queue num0-3"
{'queue': {'num': 0, 'bypass': True}} -> "queue num0 bypass"
"""
if expr is None:
return ""
@@ -520,6 +549,23 @@ def expr_to_text(expr: Any) -> Optional[str]:
parts.append("counter")
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
if "match" in element:
m = element["match"]
@@ -532,11 +578,9 @@ def expr_to_text(expr: Any) -> Optional[str]:
field = p.get("field")
# common: protocol field match (protocol == icmp)
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":
parts.append(f"{prot} {field} {right}")
continue
# payload might be l4 ports etc; produce generic payload(...) token
parts.append(f"payload({prot}.{field}) {right}")
continue
# 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)
# 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}"
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)
except NftError as e:
@@ -705,7 +746,6 @@ def create_rule_json(req: CreateRuleRequest):
raise HTTPException(status_code=500, detail=str(e))
@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"):
"""