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

This commit is contained in:
2026-02-28 22:39:31 +01:00
parent f33c708cce
commit 216f8464f2

View File

@@ -1,6 +1,7 @@
# app.py # app.py
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from fastapi import FastAPI, APIRouter, HTTPException, status from fastapi import FastAPI, APIRouter, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import logging import logging
import json import json
@@ -29,7 +30,7 @@ class NftManager:
def __init__(self) -> None: def __init__(self) -> None:
self.nft = Nftables() self.nft = Nftables()
# set_json_output is optional; don't rely on it for JSON path. # best-effort: don't strictly rely on set_json_output for JSON path
try: try:
if hasattr(self.nft, "set_json_output"): if hasattr(self.nft, "set_json_output"):
self.nft.set_json_output(True) self.nft.set_json_output(True)
@@ -41,7 +42,7 @@ class NftManager:
rc, out, err = self.nft.cmd(text_cmd) rc, out, err = self.nft.cmd(text_cmd)
if rc != 0: if rc != 0:
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": int(rc), "stdout": out or "", "stderr": err or ""}
def json_cmd(self, text_cmd: str) -> Tuple[int, str, str]: def json_cmd(self, text_cmd: str) -> Tuple[int, str, str]:
""" """
@@ -53,24 +54,21 @@ class NftManager:
try: try:
res = self.nft.json_cmd(text_cmd) res = self.nft.json_cmd(text_cmd)
if isinstance(res, (list, tuple)) and len(res) >= 3: if isinstance(res, (list, tuple)) and len(res) >= 3:
return int(res[0]), res[1], res[2] return int(res[0]), res[1] or "", res[2] or ""
except Exception as e: except Exception as e:
logger.debug("nft.json_cmd failed, falling back to cmd -j: %s", e) logger.debug("nft.json_cmd failed, falling back to cmd -j: %s", e)
# fallback: append -j if not present and call textual cmd() # fallback: append -j if not present and call textual cmd()
cmd_with_j = text_cmd if "-j" in text_cmd else f"{text_cmd} -j" cmd_with_j = text_cmd if "-j" in text_cmd else f"{text_cmd} -j"
r = self.cmd(cmd_with_j) r = self.cmd(cmd_with_j)
rc = int(r.get("rc", -1) or -1) return int(r.get("rc", -1) or -1), r.get("stdout", "") or "", r.get("stderr", "") or ""
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 textual ruleset from `nft list ruleset`.""" """Return textual ruleset from `nft list ruleset`."""
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"] or "" return res["stdout"]
def list_rules_json(self) -> Dict[str, Any]: def list_rules_json(self) -> Dict[str, Any]:
"""Return parsed JSON from `nft -j list ruleset`.""" """Return parsed JSON from `nft -j list ruleset`."""
@@ -100,7 +98,8 @@ class NftManager:
logger.debug("list_chain_text: textual cmd failed (%s), attempting JSON fallback", res["stderr"]) logger.debug("list_chain_text: textual cmd failed (%s), attempting JSON fallback", res["stderr"])
rc, out, err = self.json_cmd(cmd) rc, out, err = self.json_cmd(cmd)
if rc != 0: if rc != 0:
raise NftError(f"nft {cmd} failed: {err}") # raise original textual error if JSON fallback doesn't work either
raise NftError(f"nft {cmd} failed: {res['stderr'] or err}")
try: try:
parsed = json.loads(out) parsed = json.loads(out)
@@ -166,7 +165,6 @@ class NftManager:
continue continue
parts.append("queue") parts.append("queue")
continue continue
# fallback: join keys
parts.append("+".join(sorted(part.keys()))) parts.append("+".join(sorted(part.keys())))
else: else:
parts.append(str(part)) parts.append(str(part))
@@ -193,7 +191,7 @@ router = APIRouter(prefix="/firewall", tags=["firewall"])
mgr = NftManager() mgr = NftManager()
# ---------- Request/Response models ---------- # ---------- Request/Response models (only used for validation / docs) ----------
class RawCmdRequest(BaseModel): class RawCmdRequest(BaseModel):
cmd: str = Field(..., example="add rule inet filter input ip saddr 10.0.0.0/8 drop") cmd: str = Field(..., example="add rule inet filter input ip saddr 10.0.0.0/8 drop")
@@ -204,33 +202,6 @@ class ExecResult(BaseModel):
stderr: Optional[str] = None stderr: Optional[str] = None
class RuleOut(BaseModel):
handle: Optional[int] = None
expr: Any
text: str
position: Optional[Any] = None
comment: Optional[str] = None
class ChainOut(BaseModel):
name: str
type: Optional[str] = None
hook: Optional[str] = None
priority: Optional[int] = None
policy: Optional[str] = None
rules: List[RuleOut]
class TableOut(BaseModel):
family: str
name: str
chains: List[ChainOut]
class RulesetModel(BaseModel):
tables: List[TableOut]
class CreateRuleRequest(BaseModel): class CreateRuleRequest(BaseModel):
family: str family: str
table: str table: str
@@ -240,15 +211,7 @@ class CreateRuleRequest(BaseModel):
comment: Optional[str] = None comment: Optional[str] = None
# ruleset may be typed RulesetModel or raw textual string (fallback) # ---------- Helpers to make predictable output ----------
RulesetValue = Optional[Union[Dict[str, Any], str]]
class RulesetOut(BaseModel):
ruleset: RulesetValue = None
# ---------- Helpers ----------
_handle_re = re.compile(r"\s+#\s*handle\s+\d+\s*$") _handle_re = re.compile(r"\s+#\s*handle\s+\d+\s*$")
@@ -480,26 +443,32 @@ def populate_text_from_chain_text(custom: Dict[str, Any]) -> None:
# ---------- Routes ---------- # ---------- Routes ----------
@router.get("/rules", response_model=RulesetOut, summary="List ruleset") @router.get("/rules", summary="List ruleset")
def list_rules(): def list_rules():
"""
Returns JSON: { "ruleset": <dict | textual-string-fallback> }
Ensures `ruleset` is a native dict when JSON is available and parsed.
"""
try: try:
try: try:
nft_json = mgr.list_rules_json() nft_json = mgr.list_rules_json()
except NftError as e: except NftError as e:
logger.debug("could not obtain nft JSON ruleset: %s", e) logger.debug("could not obtain nft JSON ruleset: %s", e)
# textual fallback - return plain textual ruleset string
text = mgr.list_rules() text = mgr.list_rules()
return RulesetOut(ruleset=text.strip() if text is not None else None) return JSONResponse(content={"ruleset": text.strip() if text is not None else None}, status_code=200)
# build structured representation (native Python)
custom = build_predictable_ruleset(nft_json) custom = build_predictable_ruleset(nft_json)
# Try to replace rule['text'] with exact textual lines from nft list chain ...
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("populate_text_from_chain_text failed: %s", e)
# IMPORTANT: return native Python structure (dict) — not a JSON string. # Return native structure (do NOT json.dumps)
ruleset_model = RulesetModel.parse_obj(custom) return JSONResponse(content={"ruleset": custom}, status_code=200)
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))
@@ -541,19 +510,14 @@ def create_rule_json(req: CreateRuleRequest):
logger.info("create_rule_json executing: %s", cmd) logger.info("create_rule_json executing: %s", cmd)
res = mgr.cmd(cmd) res = mgr.cmd(cmd)
raw_rc = res.get("rc") rc = int(res.get("rc", -1) or -1)
stdout = res.get("stdout") or "" stdout = res.get("stdout") or ""
stderr = res.get("stderr") or "" stderr = res.get("stderr") or ""
try:
rc = int(raw_rc)
except Exception:
rc = -1
exec_res = ExecResult(rc=rc, stdout=stdout or None, stderr=stderr or None)
if rc == 0: if rc == 0:
return exec_res return ExecResult(rc=rc, stdout=stdout or None, stderr=stderr or None)
# If rc indicates failure but stderr empty, check chain presence
if (rc < 0 or rc != 0) and stderr.strip() == "": if (rc < 0 or rc != 0) and stderr.strip() == "":
try: try:
chain_text = mgr.list_chain_text(family, table, chain) or "" chain_text = mgr.list_chain_text(family, table, chain) or ""
@@ -596,7 +560,7 @@ def exec_raw(req: RawCmdRequest):
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)
return ExecResult(rc=rc, stdout=res.get("stdout"), stderr=res.get("stderr")) return ExecResult(rc=rc, stdout=res.get("stdout") or None, stderr=res.get("stderr") or None)
except Exception as e: except Exception as e:
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))