This commit is contained in:
@@ -23,15 +23,15 @@ class NftManager:
|
|||||||
"""
|
"""
|
||||||
Thin wrapper around python-nftables exposing:
|
Thin wrapper around python-nftables exposing:
|
||||||
- cmd execution (textual nft commands via Nftables.cmd())
|
- cmd execution (textual nft commands via Nftables.cmd())
|
||||||
|
- json execution via Nftables.json_cmd() when available
|
||||||
- convenience list_rules_text / list_rules_json / list_chain_text
|
- convenience list_rules_text / list_rules_json / list_chain_text
|
||||||
We prefer JSON globally, but for per-chain textual listing we temporarily disable JSON
|
|
||||||
so the output matches `nft list chain ...` textual rule lines.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.nft = Nftables()
|
self.nft = Nftables()
|
||||||
# Try to prefer JSON for general listing; we'll toggle off for chain-list calls.
|
# Try to prefer JSON for general listing; we'll toggle off for chain-list calls.
|
||||||
try:
|
try:
|
||||||
|
# If set_json_output exists it's a convenience; we won't rely on it for JSON path.
|
||||||
self.nft.set_json_output(True)
|
self.nft.set_json_output(True)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("set_json_output not available or ignored")
|
logger.debug("set_json_output not available or ignored")
|
||||||
@@ -46,157 +46,155 @@ class NftManager:
|
|||||||
logger.warning("nft cmd rc=%s stderr=%s cmd=%s", rc, err, text_cmd)
|
logger.warning("nft cmd rc=%s stderr=%s cmd=%s", rc, err, text_cmd)
|
||||||
return {"rc": rc, "stdout": out, "stderr": err}
|
return {"rc": rc, "stdout": out, "stderr": err}
|
||||||
|
|
||||||
|
def json_cmd(self, text_cmd: str) -> Tuple[int, str, str]:
|
||||||
|
"""
|
||||||
|
Execute a JSON-output nft command. Prefer Nftables.json_cmd if available
|
||||||
|
(returns tuple (rc, stdout, stderr)). Otherwise, call `nft <cmd> -j` using cmd()
|
||||||
|
and attempt to return the same tuple shape.
|
||||||
|
"""
|
||||||
|
# Prefer built-in json_cmd if present
|
||||||
|
if hasattr(self.nft, "json_cmd"):
|
||||||
|
try:
|
||||||
|
res = self.nft.json_cmd(text_cmd)
|
||||||
|
# Expecting (rc, out, err)
|
||||||
|
if isinstance(res, (list, tuple)) and len(res) >= 3:
|
||||||
|
return int(res[0]), res[1], res[2]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("json_cmd failed, falling back to cmd with -j: %s", e)
|
||||||
|
|
||||||
|
# Fallback: call cmd with -j variant and parse output
|
||||||
|
# Use 'list ruleset -j' or similar command suffixes as caller provides the whole command
|
||||||
|
cmd_with_j = f"{text_cmd} -j" if "-j" not in text_cmd else text_cmd
|
||||||
|
r = self.cmd(cmd_with_j)
|
||||||
|
rc = int(r.get("rc", -1) or -1)
|
||||||
|
out = r.get("stdout") or ""
|
||||||
|
err = r.get("stderr") or ""
|
||||||
|
return rc, out, err
|
||||||
|
|
||||||
def list_rules(self) -> str:
|
def list_rules(self) -> str:
|
||||||
"""
|
"""
|
||||||
Return the textual ruleset as produced by 'nft list ruleset'.
|
Return the textual ruleset as produced by 'nft list ruleset'.
|
||||||
Uses the textual command path.
|
|
||||||
"""
|
"""
|
||||||
res = self.cmd("list ruleset")
|
res = self.cmd("list ruleset")
|
||||||
if res["rc"] != 0:
|
if res["rc"] != 0:
|
||||||
raise NftError(f"nft list ruleset failed: {res['stderr']}")
|
raise NftError(f"nft list ruleset failed: {res['stderr']}")
|
||||||
return res["stdout"]
|
return res["stdout"] or ""
|
||||||
|
|
||||||
def list_rules_json(self) -> Dict[str, Any]:
|
def list_rules_json(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Try to obtain nft -j list ruleset (JSON). Returns parsed JSON dict on success.
|
Try to obtain nft -j list ruleset (JSON). Returns parsed JSON dict on success.
|
||||||
Raises NftError on failure or when output cannot be parsed as JSON.
|
Raises NftError on failure or when output cannot be parsed as JSON.
|
||||||
"""
|
"""
|
||||||
# prefer using json_cmd when available for a clean tuple (rc, out, err)
|
rc, out, err = self.json_cmd("list ruleset")
|
||||||
|
if rc != 0:
|
||||||
|
raise NftError(f"nft list ruleset failed: {err}")
|
||||||
|
if not out:
|
||||||
|
raise NftError("empty JSON output from nft")
|
||||||
try:
|
try:
|
||||||
res = self.nft.json_cmd("list ruleset")
|
parsed = json.loads(out)
|
||||||
if not isinstance(res, (list, tuple)) or len(res) < 3:
|
return parsed
|
||||||
raise Exception("unexpected json_cmd result shape")
|
except json.JSONDecodeError as e:
|
||||||
if res[0] != 0:
|
raise NftError(f"unable to parse JSON output from nft: {e}")
|
||||||
raise NftError(f"nft list ruleset failed: {res[2]}")
|
|
||||||
return json.loads(res[1])
|
|
||||||
except AttributeError:
|
|
||||||
# fallback to textual cmd + json.loads if json_cmd is not available
|
|
||||||
cmd_variants = ["list ruleset -j", "list ruleset"]
|
|
||||||
last_err = None
|
|
||||||
for c in cmd_variants:
|
|
||||||
r = self.cmd(c)
|
|
||||||
if r["rc"] != 0:
|
|
||||||
last_err = r["stderr"]
|
|
||||||
continue
|
|
||||||
out = r["stdout"]
|
|
||||||
if not out:
|
|
||||||
last_err = "empty output"
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
parsed = json.loads(out)
|
|
||||||
return parsed
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
last_err = f"json decode error: {e}"
|
|
||||||
continue
|
|
||||||
raise NftError(f"unable to get JSON ruleset: {last_err}")
|
|
||||||
|
|
||||||
def list_chain_text(self, family: str, table: str, chain: str) -> str:
|
def list_chain_text(self, family: str, table: str, chain: str) -> str:
|
||||||
"""
|
"""
|
||||||
Return textual output of `nft list chain <family> <table> <chain>`.
|
Return textual output of `nft list chain <family> <table> <chain>`.
|
||||||
This tries to temporarily disable JSON output so the wrapper returns the textual
|
We prefer calling nft in textual mode (nft.cmd). If the wrapper can't return textual
|
||||||
representation used by `nft list ruleset`. If disabling JSON is not possible,
|
output and returns JSON, we attempt a best-effort reconstruction of the textual lines.
|
||||||
we attempt to parse returned JSON (as a last resort), but the preferred path is
|
|
||||||
to get textual output.
|
|
||||||
"""
|
"""
|
||||||
cmd = f"list chain {family} {table} {chain}"
|
cmd = f"list chain {family} {table} {chain}"
|
||||||
# Attempt to temporarily disable JSON output on the wrapper (best-effort).
|
# Attempt to call textual cmd (this uses self.nft.cmd)
|
||||||
json_toggled = False
|
res = self.cmd(cmd)
|
||||||
res = {"rc": -1, "stdout": "", "stderr": "unknown"}
|
|
||||||
try:
|
|
||||||
if hasattr(self.nft, "set_json_output"):
|
|
||||||
try:
|
|
||||||
# Turn off JSON output to force textual output for this call.
|
|
||||||
self.nft.set_json_output(False)
|
|
||||||
json_toggled = True
|
|
||||||
except Exception:
|
|
||||||
logger.debug("could not toggle set_json_output(False); will try command anyway")
|
|
||||||
res = self.cmd(cmd)
|
|
||||||
finally:
|
|
||||||
# Restore JSON output preference if we toggled it.
|
|
||||||
if json_toggled and hasattr(self.nft, "set_json_output"):
|
|
||||||
try:
|
|
||||||
self.nft.set_json_output(True)
|
|
||||||
except Exception:
|
|
||||||
logger.debug("failed to restore set_json_output(True)")
|
|
||||||
|
|
||||||
if res["rc"] != 0:
|
if res["rc"] != 0:
|
||||||
raise NftError(f"nft {cmd} failed: {res['stderr']}")
|
# if the raw textual command failed, try JSON and attempt to reconstruct textual
|
||||||
|
logger.debug("list_chain_text textual cmd rc!=0; trying JSON fallback: %s", res["stderr"])
|
||||||
out = res["stdout"] or ""
|
# try json_cmd
|
||||||
# If the output looks like JSON (starts with '{' or '['), try a safe fallback:
|
rc, out, err = self.json_cmd(cmd)
|
||||||
s = out.strip()
|
if rc != 0:
|
||||||
if s.startswith("{") or s.startswith("["):
|
raise NftError(f"nft {cmd} failed: {err}")
|
||||||
# Best-effort: parse JSON and attempt to extract rule textual forms if present.
|
# attempt to build textual lines from JSON
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(s)
|
parsed = json.loads(out)
|
||||||
# parsed may be the whole ruleset (nftables list) or a list; find any "rule" objects
|
except Exception:
|
||||||
rule_lines: List[str] = []
|
# give up, return raw text (could be empty)
|
||||||
# parsed might be dict with "nftables" or a list of records
|
return res.get("stdout") or ""
|
||||||
records = parsed.get("nftables") if isinstance(parsed, dict) else parsed
|
# build textual representation from JSON (best-effort)
|
||||||
if not isinstance(records, list):
|
lines: List[str] = []
|
||||||
records = []
|
records = parsed.get("nftables") if isinstance(parsed, dict) else (parsed or [])
|
||||||
for rec in records:
|
if not isinstance(records, list):
|
||||||
if "rule" in rec:
|
records = []
|
||||||
r = rec["rule"]
|
for rec in records:
|
||||||
expr = r.get("expr")
|
if "rule" in rec:
|
||||||
if isinstance(expr, list):
|
# try to use 'line' if present
|
||||||
tokens: List[str] = []
|
r = rec["rule"]
|
||||||
for part in expr:
|
# sometimes json includes 'expr' or 'handle' — we try to recompose a line
|
||||||
if isinstance(part, dict) and "match" in part:
|
line_parts: List[str] = []
|
||||||
|
handle = r.get("handle")
|
||||||
|
# attempt to render expr to short textual form
|
||||||
|
expr = r.get("expr")
|
||||||
|
if isinstance(expr, list):
|
||||||
|
# best-effort: reuse simple tokens
|
||||||
|
for part in expr:
|
||||||
|
if isinstance(part, dict):
|
||||||
|
if "payload" in part:
|
||||||
|
p = part["payload"]
|
||||||
|
prot = p.get("protocol")
|
||||||
|
field = p.get("field")
|
||||||
|
if prot and field:
|
||||||
|
line_parts.append(f"payload({prot}.{field})")
|
||||||
|
continue
|
||||||
|
if "match" in part:
|
||||||
m = part["match"]
|
m = part["match"]
|
||||||
left = m.get("left")
|
left = m.get("left")
|
||||||
right = m.get("right")
|
right = m.get("right")
|
||||||
if isinstance(left, dict) and "payload" in left and isinstance(right, str):
|
if isinstance(left, dict) and "payload" in left and isinstance(right, (str, int)):
|
||||||
p = left["payload"]
|
p = left["payload"]
|
||||||
prot = p.get("protocol")
|
prot = p.get("protocol")
|
||||||
field = p.get("field")
|
field = p.get("field")
|
||||||
if prot and field:
|
if prot and field:
|
||||||
tokens.append(f"{prot} {field} {right}")
|
line_parts.append(f"{prot} {field} {right}")
|
||||||
continue
|
continue
|
||||||
tokens.append("match")
|
line_parts.append("match")
|
||||||
elif isinstance(part, dict) and "payload" in part:
|
continue
|
||||||
p = part["payload"]
|
if "drop" in part:
|
||||||
prot = p.get("protocol")
|
line_parts.append("drop")
|
||||||
field = p.get("field")
|
continue
|
||||||
tokens.append(f"payload({prot}.{field})")
|
if "accept" in part:
|
||||||
elif isinstance(part, dict) and "drop" in part:
|
line_parts.append("accept")
|
||||||
tokens.append("drop")
|
continue
|
||||||
elif isinstance(part, dict) and "accept" in part:
|
if "counter" in part:
|
||||||
tokens.append("accept")
|
line_parts.append("counter")
|
||||||
elif isinstance(part, dict) and "counter" in part:
|
continue
|
||||||
tokens.append("counter")
|
if "queue" in part:
|
||||||
elif isinstance(part, dict) and "queue" in part:
|
|
||||||
# handle fallback queue textualization
|
|
||||||
q = part["queue"]
|
q = part["queue"]
|
||||||
if isinstance(q, dict):
|
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")
|
num = q.get("num") or q.get("number") or q.get("queue_number")
|
||||||
tok = "queue"
|
token = "queue"
|
||||||
if num is not None:
|
if num is not None:
|
||||||
tok += f" num {num}"
|
token += f" num {num}"
|
||||||
if q.get("bypass"):
|
if q.get("bypass"):
|
||||||
tok += " bypass"
|
token += " bypass"
|
||||||
tokens.append(tok)
|
line_parts.append(token)
|
||||||
|
elif isinstance(q, (int, float)):
|
||||||
|
line_parts.append(f"queue num {int(q)}")
|
||||||
else:
|
else:
|
||||||
# numeric or string value
|
line_parts.append("queue")
|
||||||
if isinstance(q, (int, float)):
|
continue
|
||||||
tokens.append(f"queue num {int(q)}")
|
# fallback to keys
|
||||||
else:
|
line_parts.append("+".join(sorted(part.keys())))
|
||||||
tokens.append(f"queue num {q}")
|
else:
|
||||||
else:
|
line_parts.append(str(part))
|
||||||
# fallback for unknown dict token
|
else:
|
||||||
if isinstance(part, dict):
|
# expr not list -> fallback to JSON dump of rule
|
||||||
tokens.append("+".join(part.keys()))
|
line_parts.append(json.dumps(r))
|
||||||
else:
|
# combine into single textual line; append handle if present
|
||||||
tokens.append(str(part))
|
text_line = " ".join(line_parts).strip()
|
||||||
rule_lines.append(" ".join(tokens))
|
if handle is not None:
|
||||||
else:
|
text_line = f"{text_line} # handle {handle}"
|
||||||
rule_lines.append(json.dumps(r))
|
lines.append(text_line)
|
||||||
if rule_lines:
|
return "\n".join(lines) if lines else (res.get("stdout") or "")
|
||||||
return "\n".join(rule_lines)
|
# textual cmd succeeded
|
||||||
except Exception:
|
return res.get("stdout") or ""
|
||||||
logger.debug("fallback JSON parsing of chain output failed; returning raw output")
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
def delete_rule_by_handle_text(self, family: str, table: str, chain: str, handle: int) -> None:
|
def delete_rule_by_handle_text(self, family: str, table: str, chain: str, handle: int) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -205,7 +203,6 @@ class NftManager:
|
|||||||
"""
|
"""
|
||||||
if not isinstance(handle, int) or handle <= 0:
|
if not isinstance(handle, int) or handle <= 0:
|
||||||
raise ValueError("handle must be a positive integer")
|
raise ValueError("handle must be a positive integer")
|
||||||
# construct textual command
|
|
||||||
cmd = f"delete rule {family} {table} {chain} handle {handle}"
|
cmd = f"delete rule {family} {table} {chain} handle {handle}"
|
||||||
res = self.cmd(cmd)
|
res = self.cmd(cmd)
|
||||||
if res["rc"] != 0:
|
if res["rc"] != 0:
|
||||||
@@ -297,38 +294,23 @@ _handle_re = re.compile(r"\s+#\s*handle\s+\d+\s*$")
|
|||||||
|
|
||||||
|
|
||||||
def parse_priority(val: Any) -> Optional[int]:
|
def parse_priority(val: Any) -> Optional[int]:
|
||||||
"""
|
|
||||||
Robustly parse a priority value returned in various nft JSON shapes.
|
|
||||||
Accepts:
|
|
||||||
- int -> returns unchanged
|
|
||||||
- numeric string -> parsed int
|
|
||||||
- dict -> tries common nested keys ('priority', 'prio')
|
|
||||||
Returns None if not parseable.
|
|
||||||
"""
|
|
||||||
if val is None:
|
if val is None:
|
||||||
return None
|
return None
|
||||||
# if it's already an int
|
|
||||||
if isinstance(val, int):
|
if isinstance(val, int):
|
||||||
return val
|
return val
|
||||||
# numeric string
|
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
s = val.strip()
|
s = val.strip()
|
||||||
# try integer parse
|
|
||||||
try:
|
try:
|
||||||
return int(s)
|
return int(s)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
# sometimes it's "0.0" or similar
|
|
||||||
return int(float(s))
|
return int(float(s))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
# nested dicts sometimes appear
|
|
||||||
if isinstance(val, dict):
|
if isinstance(val, dict):
|
||||||
# look for common keys
|
|
||||||
for key in ("priority", "prio"):
|
for key in ("priority", "prio"):
|
||||||
if key in val:
|
if key in val:
|
||||||
return parse_priority(val.get(key))
|
return parse_priority(val.get(key))
|
||||||
# try nested dict values
|
|
||||||
for v in val.values():
|
for v in val.values():
|
||||||
p = parse_priority(v)
|
p = parse_priority(v)
|
||||||
if p is not None:
|
if p is not None:
|
||||||
@@ -339,8 +321,6 @@ def parse_priority(val: Any) -> Optional[int]:
|
|||||||
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.
|
||||||
Covers common constructs; falls back to JSON dump for unknown constructs.
|
|
||||||
(Used for display in GET /rules).
|
|
||||||
"""
|
"""
|
||||||
if expr is None:
|
if expr is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -348,7 +328,6 @@ 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):
|
||||||
# common tokens
|
|
||||||
if "match" in part:
|
if "match" in part:
|
||||||
m = part["match"]
|
m = part["match"]
|
||||||
left = m.get("left")
|
left = m.get("left")
|
||||||
@@ -404,51 +383,32 @@ def rule_text_from_expr(expr: Any) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
|
||||||
Convert nft -j list ruleset parsed JSON into a deterministic, predictable JSON:
|
|
||||||
{
|
|
||||||
"tables": [
|
|
||||||
{ "family": ..., "name": ..., "chains": [ { "name": ..., "type": ..., "hook": ..., "priority": ..., "policy": ..., "rules": [ { handle, expr, text } ] } ] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
result: Dict[str, Any] = {"tables": []}
|
result: Dict[str, Any] = {"tables": []}
|
||||||
items = nft_json.get("nftables", []) if isinstance(nft_json, dict) else (nft_json or [])
|
items = nft_json.get("nftables", []) if isinstance(nft_json, dict) else (nft_json or [])
|
||||||
|
|
||||||
# Build intermediate map: (family, table) -> {family, name, chains: {chain_name: {"name", "type", "hook", "priority", "policy", "rules":[]}}}
|
|
||||||
tables: Dict[Tuple[str, str], Dict[str, Any]] = {}
|
tables: Dict[Tuple[str, str], Dict[str, Any]] = {}
|
||||||
|
|
||||||
for rec in items:
|
for rec in items:
|
||||||
# table records
|
|
||||||
if "table" in rec:
|
if "table" in rec:
|
||||||
t = rec["table"]
|
t = rec["table"]
|
||||||
fam = t.get("family")
|
fam = t.get("family")
|
||||||
name = t.get("name")
|
name = t.get("name")
|
||||||
if fam and name:
|
if fam and name:
|
||||||
tables.setdefault((fam, name), {"family": fam, "name": name, "chains": {}})
|
tables.setdefault((fam, name), {"family": fam, "name": name, "chains": {}})
|
||||||
# chain records: capture chain metadata
|
|
||||||
elif "chain" in rec:
|
elif "chain" in rec:
|
||||||
ch = rec["chain"]
|
ch = rec["chain"]
|
||||||
# chain may include family/table or nested table reference
|
|
||||||
fam = ch.get("family") or (ch.get("table") or {}).get("family")
|
fam = ch.get("family") or (ch.get("table") or {}).get("family")
|
||||||
table_name = ch.get("table") or (ch.get("table") or {}).get("name")
|
table_name = ch.get("table") or (ch.get("table") or {}).get("name")
|
||||||
cname = ch.get("name")
|
cname = ch.get("name")
|
||||||
if fam and table_name and cname:
|
if fam and table_name and cname:
|
||||||
tables.setdefault((fam, table_name), {"family": fam, "name": table_name, "chains": {}})
|
tables.setdefault((fam, table_name), {"family": fam, "name": table_name, "chains": {}})
|
||||||
chains_map = tables[(fam, table_name)]["chains"]
|
chains_map = tables[(fam, table_name)]["chains"]
|
||||||
|
|
||||||
# existing chain (maybe created earlier by rule processing)
|
|
||||||
existing = chains_map.get(cname)
|
existing = chains_map.get(cname)
|
||||||
# extract metadata robustly
|
|
||||||
ch_type = ch.get("type")
|
ch_type = ch.get("type")
|
||||||
ch_hook = ch.get("hook")
|
ch_hook = ch.get("hook")
|
||||||
# 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))
|
ch_priority = parse_priority(ch.get("priority") if "priority" in ch else ch.get("prio") if "prio" in ch else ch.get("priority", None))
|
||||||
# also attempt to parse nested shapes if present (some nft JSON variations)
|
|
||||||
if ch_priority is None:
|
if ch_priority is None:
|
||||||
ch_priority = parse_priority(ch.get("hook") if isinstance(ch.get("hook"), dict) else None)
|
ch_priority = parse_priority(ch.get("hook") if isinstance(ch.get("hook"), dict) else None)
|
||||||
|
|
||||||
ch_policy = ch.get("policy")
|
ch_policy = ch.get("policy")
|
||||||
|
|
||||||
if existing is None:
|
if existing is None:
|
||||||
chains_map[cname] = {
|
chains_map[cname] = {
|
||||||
"name": cname,
|
"name": cname,
|
||||||
@@ -459,7 +419,6 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"rules": [],
|
"rules": [],
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# merge into placeholder (do not overwrite existing rules)
|
|
||||||
if isinstance(existing, dict):
|
if isinstance(existing, dict):
|
||||||
if existing.get("type") is None and ch_type is not None:
|
if existing.get("type") is None and ch_type is not None:
|
||||||
existing["type"] = ch_type
|
existing["type"] = ch_type
|
||||||
@@ -469,7 +428,6 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
existing["priority"] = ch_priority
|
existing["priority"] = ch_priority
|
||||||
if existing.get("policy") is None and ch_policy is not None:
|
if existing.get("policy") is None and ch_policy is not None:
|
||||||
existing["policy"] = ch_policy
|
existing["policy"] = ch_policy
|
||||||
# rule records
|
|
||||||
elif "rule" in rec:
|
elif "rule" in rec:
|
||||||
r = rec["rule"]
|
r = rec["rule"]
|
||||||
fam = r.get("family")
|
fam = r.get("family")
|
||||||
@@ -480,32 +438,24 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if fam and table_name and chain_name:
|
if fam and table_name and chain_name:
|
||||||
tables.setdefault((fam, table_name), {"family": fam, "name": table_name, "chains": {}})
|
tables.setdefault((fam, table_name), {"family": fam, "name": table_name, "chains": {}})
|
||||||
chains_map = tables[(fam, table_name)]["chains"]
|
chains_map = tables[(fam, table_name)]["chains"]
|
||||||
# ensure chain placeholder exists, with possible metadata defaults
|
|
||||||
chains_map.setdefault(chain_name, {"name": chain_name, "type": None, "hook": None, "priority": None, "policy": None, "rules": []})
|
chains_map.setdefault(chain_name, {"name": chain_name, "type": None, "hook": None, "priority": None, "policy": None, "rules": []})
|
||||||
|
|
||||||
rule_obj: Dict[str, Any] = {
|
rule_obj: Dict[str, Any] = {
|
||||||
"handle": handle,
|
"handle": handle,
|
||||||
"expr": expr,
|
"expr": expr,
|
||||||
"text": rule_text_from_expr(expr),
|
"text": rule_text_from_expr(expr),
|
||||||
}
|
}
|
||||||
# include other useful metadata if present
|
|
||||||
if "position" in r:
|
if "position" in r:
|
||||||
rule_obj["position"] = r["position"]
|
rule_obj["position"] = r["position"]
|
||||||
if "comment" in r:
|
if "comment" in r:
|
||||||
rule_obj["comment"] = r["comment"]
|
rule_obj["comment"] = r["comment"]
|
||||||
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
|
|
||||||
# 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:
|
||||||
@@ -513,7 +463,6 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if chains_map[chain_name].get("policy") is None and csub.get("policy") is not None:
|
if chains_map[chain_name].get("policy") is None and csub.get("policy") is not None:
|
||||||
chains_map[chain_name]["policy"] = csub.get("policy")
|
chains_map[chain_name]["policy"] = csub.get("policy")
|
||||||
|
|
||||||
# Convert map to sorted lists for deterministic order, and include chain metadata
|
|
||||||
for (fam, tname) in sorted(tables.keys(), key=lambda k: (k[0], k[1])):
|
for (fam, tname) in sorted(tables.keys(), key=lambda k: (k[0], k[1])):
|
||||||
tdata = tables[(fam, tname)]
|
tdata = tables[(fam, tname)]
|
||||||
chains_list: List[Dict[str, Any]] = []
|
chains_list: List[Dict[str, Any]] = []
|
||||||
@@ -534,127 +483,8 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# ---------- Helpers to render expr -> textual nft (best-effort) ----------
|
|
||||||
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
|
|
||||||
- queue tokens and optional bypass support
|
|
||||||
This intentionally does not attempt to support every nft JSON construct.
|
|
||||||
"""
|
|
||||||
if expr is None:
|
|
||||||
return ""
|
|
||||||
if isinstance(expr, str):
|
|
||||||
return expr
|
|
||||||
if not isinstance(expr, list):
|
|
||||||
# unsupported top-level type
|
|
||||||
return None
|
|
||||||
|
|
||||||
parts: List[str] = []
|
|
||||||
for element in expr:
|
|
||||||
if isinstance(element, dict):
|
|
||||||
# handle drop/accept/counter directly
|
|
||||||
if "drop" in element:
|
|
||||||
parts.append("drop")
|
|
||||||
continue
|
|
||||||
if "accept" in element:
|
|
||||||
parts.append("accept")
|
|
||||||
continue
|
|
||||||
if "counter" in element:
|
|
||||||
parts.append("counter")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 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}"
|
|
||||||
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"]
|
|
||||||
left = m.get("left")
|
|
||||||
right = m.get("right")
|
|
||||||
# payload matches
|
|
||||||
if isinstance(left, dict) and "payload" in left and isinstance(right, (str, int)):
|
|
||||||
p = left["payload"]
|
|
||||||
prot = p.get("protocol")
|
|
||||||
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
|
|
||||||
parts.append("match")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# payload shorthand
|
|
||||||
if "payload" in element:
|
|
||||||
p = element["payload"]
|
|
||||||
prot = p.get("protocol")
|
|
||||||
field = p.get("field")
|
|
||||||
if prot and field:
|
|
||||||
parts.append(f"payload({prot}.{field})")
|
|
||||||
continue
|
|
||||||
parts.append("payload")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# tcp/udp as nested dicts sometimes appear
|
|
||||||
if "tcp" in element or "udp" in element:
|
|
||||||
proto = "tcp" if "tcp" in element else "udp"
|
|
||||||
val = element.get(proto)
|
|
||||||
# attempt to detect dport/sport keys
|
|
||||||
if isinstance(val, dict):
|
|
||||||
if "dport" in val:
|
|
||||||
parts.append(f"{proto} dport {val['dport']}")
|
|
||||||
continue
|
|
||||||
if "sport" in val:
|
|
||||||
parts.append(f"{proto} sport {val['sport']}")
|
|
||||||
continue
|
|
||||||
parts.append(proto)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# cmp/binary operators etc — not supported deterministically
|
|
||||||
# return None to indicate we can't safely render this expr
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
# non-dict token (string/number)
|
|
||||||
parts.append(str(element))
|
|
||||||
|
|
||||||
# join tokens
|
|
||||||
return " ".join(parts).strip()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- New helper: populate_text_from_chain_text ----------
|
# ---------- New helper: populate_text_from_chain_text ----------
|
||||||
def populate_text_from_chain_text(custom: Dict[str, Any]) -> None:
|
def populate_text_from_chain_text(custom: Dict[str, Any]) -> None:
|
||||||
"""
|
|
||||||
Replace rule['text'] in the 'custom' predictable ruleset with the exact textual
|
|
||||||
rule lines as produced by `nft list chain <family> <table> <chain>` when possible.
|
|
||||||
|
|
||||||
This modifies `custom` in-place. If textual listing for a chain fails, we fall
|
|
||||||
back to the existing rule['text'] that was produced from JSON.
|
|
||||||
"""
|
|
||||||
tables = custom.get("tables") or []
|
tables = custom.get("tables") or []
|
||||||
for t in tables:
|
for t in tables:
|
||||||
fam = t.get("family")
|
fam = t.get("family")
|
||||||
@@ -668,7 +498,6 @@ def populate_text_from_chain_text(custom: Dict[str, Any]) -> None:
|
|||||||
try:
|
try:
|
||||||
chain_text = mgr.list_chain_text(fam, tname, cname) or ""
|
chain_text = mgr.list_chain_text(fam, tname, cname) or ""
|
||||||
lines = [ln.rstrip() for ln in chain_text.splitlines() if ln.strip() != ""]
|
lines = [ln.rstrip() for ln in chain_text.splitlines() if ln.strip() != ""]
|
||||||
# build handle -> line map
|
|
||||||
handle_map: Dict[str, str] = {}
|
handle_map: Dict[str, str] = {}
|
||||||
for ln in lines:
|
for ln in lines:
|
||||||
m = re.search(r"\bhandle\s+(\d+)\b", ln)
|
m = re.search(r"\bhandle\s+(\d+)\b", ln)
|
||||||
@@ -685,17 +514,14 @@ def populate_text_from_chain_text(custom: Dict[str, Any]) -> None:
|
|||||||
replaced = True
|
replaced = True
|
||||||
|
|
||||||
if not replaced:
|
if not replaced:
|
||||||
# fallback: try to find a line that contains the JSON-derived compact text fragment
|
|
||||||
expr = rule.get("expr")
|
expr = rule.get("expr")
|
||||||
probe = rule.get("text") or rule_text_from_expr(expr)
|
probe = rule.get("text") or rule_text_from_expr(expr)
|
||||||
if probe:
|
if probe:
|
||||||
# try longest-first strategy (not strictly necessary here) — simple substring match
|
|
||||||
for ln in lines:
|
for ln in lines:
|
||||||
if probe in ln:
|
if probe in ln:
|
||||||
rule["text"] = ln.strip()
|
rule["text"] = ln.strip()
|
||||||
replaced = True
|
replaced = True
|
||||||
break
|
break
|
||||||
# if still not replaced, keep existing rule["text"]
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"populate_text_from_chain_text: failed to get textual chain for %s %s %s: %s",
|
"populate_text_from_chain_text: failed to get textual chain for %s %s %s: %s",
|
||||||
@@ -713,12 +539,6 @@ def populate_text_from_chain_text(custom: Dict[str, Any]) -> None:
|
|||||||
def list_rules():
|
def list_rules():
|
||||||
"""
|
"""
|
||||||
Returns the ruleset in a stable, strongly-typed JSON shape derived from `nft -j list ruleset`.
|
Returns the ruleset in a stable, strongly-typed JSON shape derived from `nft -j list ruleset`.
|
||||||
|
|
||||||
Structure:
|
|
||||||
{ "ruleset": { "tables": [ { "family": ..., "name": ..., "chains": [ { "name": ..., "type": ..., "hook": ..., "priority": ..., "policy": ..., "rules": [ { "handle", "expr", "text" } ] } ] } ] } }
|
|
||||||
|
|
||||||
Fallback:
|
|
||||||
- If nft JSON is unavailable, falls back to returning the raw textual ruleset string.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@@ -730,18 +550,14 @@ def list_rules():
|
|||||||
|
|
||||||
custom = build_predictable_ruleset(nft_json)
|
custom = build_predictable_ruleset(nft_json)
|
||||||
|
|
||||||
# Enrich rule['text'] by attempting to fetch the exact textual nft rule lines
|
|
||||||
# as printed by `nft list chain <family> <table> <chain>`. This is best-effort and
|
|
||||||
# will not fail the overall listing if textual retrieval fails for some chains.
|
|
||||||
try:
|
try:
|
||||||
populate_text_from_chain_text(custom)
|
populate_text_from_chain_text(custom)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("list_rules: populate_text_from_chain_text failed: %s", e)
|
logger.debug("list_rules: populate_text_from_chain_text failed: %s", e)
|
||||||
|
|
||||||
# IMPORTANT: return the parsed RulesetModel (not a JSON string) so FastAPI/Pydantic
|
# IMPORTANT: return a plain dict (not a JSON string) so FastAPI/Pydantic serializes it naturally.
|
||||||
# will serialize the structure properly without double-escaping.
|
|
||||||
ruleset_model = RulesetModel.parse_obj(custom)
|
ruleset_model = RulesetModel.parse_obj(custom)
|
||||||
return RulesetOut(ruleset=ruleset_model)
|
return RulesetOut(ruleset=ruleset_model.dict())
|
||||||
except NftError as e:
|
except NftError as e:
|
||||||
logger.exception("list_rules failed")
|
logger.exception("list_rules failed")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@@ -757,15 +573,6 @@ def list_rules():
|
|||||||
summary="Create rule (JSON, expr required; returns ExecResult with rc/stdout/stderr)",
|
summary="Create rule (JSON, expr required; returns ExecResult with rc/stdout/stderr)",
|
||||||
)
|
)
|
||||||
def create_rule_json(req: CreateRuleRequest):
|
def create_rule_json(req: CreateRuleRequest):
|
||||||
"""
|
|
||||||
Create a rule from JSON (expr required).
|
|
||||||
- If req.position is provided, uses: insert rule <family> <table> <chain> position <n> <expr>
|
|
||||||
- Otherwise, uses: add rule <family> <table> <chain> <expr> (append)
|
|
||||||
- If rendering fails: 400 instructing the client to use POST /firewall/raw
|
|
||||||
- Returns ExecResult on success (201) or on error (400) with stdout/stderr in body.
|
|
||||||
- If nft wrapper returns an invalid rc but the command produced no stderr, we double-check the chain
|
|
||||||
to see if the new rule is present; if present we treat as success.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
family = req.family
|
family = req.family
|
||||||
table = req.table
|
table = req.table
|
||||||
@@ -783,12 +590,9 @@ def create_rule_json(req: CreateRuleRequest):
|
|||||||
|
|
||||||
expr_text = rendered.strip()
|
expr_text = rendered.strip()
|
||||||
|
|
||||||
# If a position is explicitly requested, use the 'insert rule ... position <n> ...' form.
|
|
||||||
# 'add rule ... position ...' is not supported by some nft versions / syntaxes.
|
|
||||||
if req.position is not None:
|
if req.position is not None:
|
||||||
try:
|
try:
|
||||||
pos = int(req.position)
|
pos = int(req.position)
|
||||||
# clamp pos to >= 0
|
|
||||||
if pos < 0:
|
if pos < 0:
|
||||||
pos = 0
|
pos = 0
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -800,12 +604,10 @@ def create_rule_json(req: CreateRuleRequest):
|
|||||||
logger.info("create_rule_json executing command: %s", cmd)
|
logger.info("create_rule_json executing command: %s", cmd)
|
||||||
|
|
||||||
res = mgr.cmd(cmd)
|
res = mgr.cmd(cmd)
|
||||||
# res expected {"rc": rc, "stdout": out, "stderr": err}
|
|
||||||
raw_rc = res.get("rc")
|
raw_rc = res.get("rc")
|
||||||
stdout = res.get("stdout") or ""
|
stdout = res.get("stdout") or ""
|
||||||
stderr = res.get("stderr") or ""
|
stderr = res.get("stderr") or ""
|
||||||
|
|
||||||
# Coerce rc to int safely; if not int-like, set -1 to indicate unknown.
|
|
||||||
try:
|
try:
|
||||||
rc = int(raw_rc)
|
rc = int(raw_rc)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -815,33 +617,21 @@ def create_rule_json(req: CreateRuleRequest):
|
|||||||
|
|
||||||
exec_res = ExecResult(rc=rc, stdout=stdout or None, stderr=stderr or None)
|
exec_res = ExecResult(rc=rc, stdout=stdout or None, stderr=stderr or None)
|
||||||
|
|
||||||
# If rc == 0 — success
|
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
return exec_res
|
return exec_res
|
||||||
|
|
||||||
# Handle the annoying case: wrapper returned invalid rc (<0) or non-zero,
|
|
||||||
# but stderr is empty. The command may have succeeded nevertheless.
|
|
||||||
if (rc < 0 or rc != 0) and stderr.strip() == "":
|
if (rc < 0 or rc != 0) and stderr.strip() == "":
|
||||||
logger.debug("create_rule_json: rc indicates failure but stderr empty; verifying rule presence")
|
logger.debug("create_rule_json: rc indicates failure but stderr empty; verifying rule presence")
|
||||||
|
|
||||||
# Attempt to verify the rule exists by listing the chain and searching for a textual match.
|
|
||||||
# We use list_chain_text because it returns textual rule lines we can search for the preview text.
|
|
||||||
try:
|
try:
|
||||||
chain_text = mgr.list_chain_text(family, table, chain) or ""
|
chain_text = mgr.list_chain_text(family, table, chain) or ""
|
||||||
# Simple presence check: the textual fragment we attempted to add should be present
|
|
||||||
# as a substring in the chain listing (e.g. "ip protocol icmp drop").
|
|
||||||
if expr_text and expr_text in chain_text:
|
if expr_text and expr_text in chain_text:
|
||||||
logger.info("create_rule_json: detected rule in chain after add; treating as success")
|
logger.info("create_rule_json: detected rule in chain after add; treating as success")
|
||||||
# return success ExecResult with rc=0 to indicate success to client
|
|
||||||
return ExecResult(rc=0, stdout=stdout or None, stderr=stderr or None)
|
return ExecResult(rc=0, stdout=stdout or None, stderr=stderr or None)
|
||||||
else:
|
else:
|
||||||
logger.debug("create_rule_json: rule not found in chain text; chain_text=%r", chain_text)
|
logger.debug("create_rule_json: rule not found in chain text; chain_text=%r", chain_text)
|
||||||
except Exception as e_chain:
|
except Exception as e_chain:
|
||||||
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.
|
|
||||||
# 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)
|
||||||
raise HTTPException(status_code=400, detail=detail)
|
raise HTTPException(status_code=400, detail=detail)
|
||||||
@@ -850,21 +640,14 @@ def create_rule_json(req: CreateRuleRequest):
|
|||||||
logger.warning("create_rule_json NftError: %s", e)
|
logger.warning("create_rule_json NftError: %s", e)
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
# re-raise HTTPException so we don't wrap it again
|
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("create_rule_json internal error")
|
logger.exception("create_rule_json internal error")
|
||||||
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"):
|
||||||
"""
|
|
||||||
Delete a rule by handle using textual nft command.
|
|
||||||
Command executed:
|
|
||||||
delete rule <family> <table> <chain> handle <handle>
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
mgr.delete_rule_by_handle_text(family=family, table=table, chain=chain, handle=handle)
|
mgr.delete_rule_by_handle_text(family=family, table=table, chain=chain, handle=handle)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -880,9 +663,6 @@ def delete_rule(handle: int, family: str = "inet", table: str = "filter", chain:
|
|||||||
|
|
||||||
@router.post("/raw", response_model=ExecResult, summary="Execute raw textual nft command")
|
@router.post("/raw", response_model=ExecResult, summary="Execute raw textual nft command")
|
||||||
def exec_raw(req: RawCmdRequest):
|
def exec_raw(req: RawCmdRequest):
|
||||||
"""
|
|
||||||
Execute an arbitrary textual nft command and return structured {rc, stdout, stderr}.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
res = mgr.cmd(req.cmd)
|
res = mgr.cmd(req.cmd)
|
||||||
rc = int(res.get("rc", -1) or -1)
|
rc = int(res.get("rc", -1) or -1)
|
||||||
@@ -891,4 +671,5 @@ def exec_raw(req: RawCmdRequest):
|
|||||||
logger.exception("exec_raw failed")
|
logger.exception("exec_raw failed")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
Reference in New Issue
Block a user