From e70167cf91ed3843a2e5faeab6441d836983cc97 Mon Sep 17 00:00:00 2001 From: malmert Date: Sat, 28 Feb 2026 14:31:26 +0100 Subject: [PATCH] fix nfqueue --- backend/src/api/nft_manager.py | 86 +++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/backend/src/api/nft_manager.py b/backend/src/api/nft_manager.py index b1e674b..c9452c7 100644 --- a/backend/src/api/nft_manager.py +++ b/backend/src/api/nft_manager.py @@ -158,15 +158,19 @@ class NftManager: # 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") + num = q.get("num") or q.get("number") or q.get("queue_number") or q.get("from") or q.get("range") tok = "queue" if num is not None: - tok += f" num{num}" + tok += f" num {num}" if q.get("bypass"): tok += " bypass" tokens.append(tok) else: - tokens.append(f"queue{q}") + # numeric or string value + if isinstance(q, (int, float)): + tokens.append(f"queue num {int(q)}") + else: + tokens.append(f"queue num {q}") else: tokens.append("+".join(part.keys())) rule_lines.append(" ".join(tokens)) @@ -253,13 +257,10 @@ class CreateRuleRequest(BaseModel): class Config: schema_extra = { "example": { - "family": "inet", + "family": "bridge", "table": "filter", - "chain": "input", - "expr": [ - {"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, - {"queue": {"num": 0, "bypass": True}} - ], + "chain": "forward", + "expr": [{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, {"drop": None}], } } @@ -331,23 +332,6 @@ 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"] @@ -380,6 +364,20 @@ def rule_text_from_expr(expr: Any) -> str: elif "tcp" in part or "udp" in part: proto = "tcp" if "tcp" in part else "udp" tokens.append(proto) + elif "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) else: keys = "+".join(sorted(part.keys())) tokens.append(keys) @@ -427,11 +425,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") - # priority may be provided in several forms; try them + # try multiple keys for priority/prio shapes 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) + # 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) ch_policy = ch.get("policy") @@ -482,12 +480,16 @@ 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: @@ -522,10 +524,12 @@ 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 ...'. Returns None when it cannot deterministically render the provided expr. - Supports queue + bypass: - {'queue': 0} -> "queue num0" - {'queue': '0-3'} -> "queue num0-3" - {'queue': {'num': 0, 'bypass': True}} -> "queue num0 bypass" + Supported cases (common): + - [{'match': {'left': {'payload': {'protocol':'ip','field':'protocol'}}, 'op':'==', 'right':'icmp'}}, {'drop': None}] + -> 'ip protocol icmp drop' + - payload / tcp / udp / counter / accept + - queue tokens and optional bypass support + This intentionally does not attempt to support every nft JSON construct. """ if expr is None: return "" @@ -549,20 +553,20 @@ def expr_to_text(expr: Any) -> Optional[str]: parts.append("counter") continue - # queue support (NEW) + # queue support: allow {"queue": 1} or {"queue": {"num":1, "bypass": True}} etc. 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}" + token += f" num {num}" if q.get("bypass"): token += " bypass" elif isinstance(q, (int, float)): - token += f" num{int(q)}" + token += f" num {int(q)}" elif isinstance(q, str): - token += f" num{q}" + token += f" num {q}" parts.append(token) continue @@ -578,9 +582,11 @@ 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 @@ -731,8 +737,11 @@ 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: @@ -746,6 +755,7 @@ 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"): """