diff --git a/backend/src/api/nft_manager.py b/backend/src/api/nft_manager.py index 1a9438e..e6e0d44 100644 --- a/backend/src/api/nft_manager.py +++ b/backend/src/api/nft_manager.py @@ -91,7 +91,6 @@ class NftManager: cmd = f"list chain {family} {table} {chain}" # Attempt to temporarily disable JSON output on the wrapper (best-effort). json_toggled = False - prev_state_set = False try: if hasattr(self.nft, "set_json_output"): try: @@ -99,7 +98,6 @@ class NftManager: self.nft.set_json_output(False) json_toggled = True except Exception: - # If toggling fails, continue and try the cmd anyway. logger.debug("could not toggle set_json_output(False); will try command anyway") res = self.cmd(cmd) finally: @@ -129,27 +127,21 @@ class NftManager: for rec in records: if "rule" in rec: r = rec["rule"] - # Try to extract a concise textual representation: - # If expr present and is a list, build a short textual form. This is heuristic. expr = r.get("expr") if isinstance(expr, list): tokens: List[str] = [] for part in expr: - # common forms: {"match": {...}}, {"payload": ...}, {"cmp": ...}, {"drop": null}, {"accept": null} if "match" in part: m = part["match"] - # try to extract 'left' payload protocol match to 'ip protocol icmp' form left = m.get("left") right = m.get("right") - # try payload -> protocol -> ip / field -> protocol - if isinstance(left, dict) and "payload" in left: + if isinstance(left, dict) and "payload" in left and isinstance(right, str): p = left["payload"] prot = p.get("protocol") field = p.get("field") - if prot and field and isinstance(right, str): + if prot and field: tokens.append(f"{prot} {field} {right}") continue - # fallback to rough match string tokens.append("match") elif "payload" in part: p = part["payload"] @@ -163,19 +155,15 @@ class NftManager: elif "counter" in part: tokens.append("counter") else: - # generic fallback: include the keys present tokens.append("+".join(part.keys())) rule_lines.append(" ".join(tokens)) else: - # No expr list we can interpret; fallback to the raw JSON string (but not the entire nftables block) rule_lines.append(json.dumps(r)) - # Join into a pseudo-text block similar to `nft list chain` output (one rule per line) if rule_lines: return "\n".join(rule_lines) except Exception: logger.debug("fallback JSON parsing of chain output failed; returning raw output") - # Prefer returning the raw textual output if we have it (lines etc.) return out def delete_rule_by_handle_text(self, family: str, table: str, chain: str, handle: int) -> None: @@ -225,92 +213,117 @@ class RulesetOut(BaseModel): # ---------- Helpers to convert to desired shape ---------- _handle_re = re.compile(r"\s+#\s*handle\s+\d+\s*$") -def extract_rule_lines_from_chain_text(text: str) -> List[str]: +def rule_text_from_expr(expr: Any) -> str: """ - Given output of `nft list chain fam table chain`, extract the rule lines as strings. - - Only consider indented lines (rules are indented inside the chain block). - - Skip chain header metadata lines that typically end with ';' (e.g. "type ...; policy ...;"). - - Skip closing brace lines ('}'). - - Remove trailing '# handle N' fragments. - Returns cleaned rule strings like "ip protocol icmp drop". + Deterministic serializer to produce a compact UI-friendly string from expr list. + Covers common constructs; falls back to JSON dump for unknown constructs. """ - lines: List[str] = [] - if not text: - return lines - - for raw in text.splitlines(): - # preserve the original raw to check indentation - if raw is None: - continue - # ignore empty lines - if raw.strip() == "": - continue - # ignore closing braces (possibly with indentation) - if raw.strip() == "}": - continue - # Only accept lines that are indented (start with whitespace). - # This filters out top-level "table ..." and "chain ..." header lines. - if not raw.startswith((" ", "\t")): - # not indented => likely header/footer, skip - continue - # Now we have an indented line. Remove leading whitespace to get the content. - line = raw.lstrip().rstrip() - # skip chain metadata lines that end with ';' (e.g. "type filter hook ...; policy accept;") - if line.endswith(";"): - continue - # remove trailing " # handle N" if present - line = _handle_re.sub("", line).rstrip() - if line: - lines.append(line) - return lines + if expr is None: + return "" + if isinstance(expr, list): + tokens: List[str] = [] + for part in expr: + if isinstance(part, dict): + # common tokens + if "match" in part: + m = part["match"] + left = m.get("left") + right = m.get("right") + if isinstance(left, dict) and "payload" in left and isinstance(right, str): + p = left["payload"] + prot = p.get("protocol") + field = p.get("field") + if prot and field: + tokens.append(f"{prot} {field} {right}") + continue + tokens.append("match") + elif "payload" in part: + p = part["payload"] + prot = p.get("protocol") + field = p.get("field") + if prot and field: + tokens.append(f"payload({prot}.{field})") + continue + tokens.append("payload") + elif "cmp" in part or "binary" in part: + tokens.append("cmp") + elif "drop" in part: + tokens.append("drop") + elif "accept" in part: + tokens.append("accept") + elif "counter" in part: + tokens.append("counter") + elif "tcp" in part or "udp" in part: + proto = "tcp" if "tcp" in part else "udp" + tokens.append(proto) + else: + keys = "+".join(sorted(part.keys())) + tokens.append(keys) + else: + tokens.append(str(part)) + return " ".join(tokens) + return str(expr) -def build_custom_ruleset_from_nft_json(nft_json: Dict[str, Any]) -> Dict[str, Any]: +def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]: """ - Build the desired structure: - { "table": [ { "name":