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

This commit is contained in:
2026-01-27 17:52:44 +01:00
parent ae80e967ed
commit 86cc1e6e50

View File

@@ -1,22 +1,26 @@
# app.py # app.py
""" """
Unrestricted nftables FastAPI service (libnftables only) with a preview endpoint. Unrestricted nftables FastAPI service (libnftables only) with separate endpoints for
textual commands (/firewall/raw) and JSON commands (/firewall/jsoncmd and /firewall/rules),
plus a preview endpoint that does NOT execute.
Endpoints (high level): Endpoints (high level):
GET /firewall/rules -> list rules (kernel-provided rules with handles) GET /firewall/rules -> list rules (kernel-provided rules with handles)
POST /firewall/rules -> add rule (executes; accepts 'cmd' or 'json') POST /firewall/rules -> add/execute libnftables JSON command (executes)
DELETE /firewall/rules/{handle} -> delete rule by handle (executes) DELETE /firewall/rules/{handle} -> delete rule by handle (executes)
POST /firewall/raw -> execute textual nft command (executes) POST /firewall/raw -> execute textual nft command (executes)
POST /firewall/jsoncmd -> execute libnftables JSON command (executes) POST /firewall/jsoncmd -> execute libnftables JSON command (executes)
POST /firewall/preview -> PREVIEW what would be executed (no changes) POST /firewall/preview -> PREVIEW what would be executed (no changes)
Requirements: Requirements:
- python-nftables installed - python-nftables installed
- CAP_NET_ADMIN or root required to modify nftables - CAP_NET_ADMIN or root required to modify nftables
WARNING: WARNING:
This service can run arbitrary nft commands. Run only in a trusted environment. This service can run arbitrary nft commands. Run only in a trusted environment.
""" """
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional
from fastapi import FastAPI, APIRouter, HTTPException, status from fastapi import FastAPI, APIRouter, HTTPException, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import logging import logging
@@ -105,22 +109,30 @@ class NftManager:
# ---------- FastAPI + Router ---------- # ---------- FastAPI + Router ----------
app = FastAPI(title="Unrestricted nftables API (libnftables only)")
router = APIRouter(prefix="/firewall", tags=["firewall"]) router = APIRouter(prefix="/firewall", tags=["firewall"])
mgr = NftManager() mgr = NftManager()
# Request models # Request models
class CreateRuleRequest(BaseModel): class CreateRuleJsonRequest(BaseModel):
cmd: Optional[str] = Field(None, description="Textual nft command (e.g. 'add rule ...')") json: Dict[str, Any] = Field(..., description="libnftables JSON command object")
json: Optional[Dict[str, Any]] = Field(None, description="libnftables JSON command object")
class RawCmdRequest(BaseModel): class RawCmdRequest(BaseModel):
cmd: str = Field(..., description="Textual nft command to execute") cmd: str = Field(..., description="Textual nft command to execute")
class JsonCmdRequest(BaseModel): class JsonCmdRequest(BaseModel):
json: Dict[str, Any] = Field(..., description="libnftables JSON command object") json: Dict[str, Any] = Field(..., description="libnftables JSON command object")
class PreviewRequest(BaseModel):
# preview accepts either textual cmd or json object
cmd: Optional[str] = Field(None, description="Textual nft command (preview only)")
json: Optional[Dict[str, Any]] = Field(None, description="libnftables JSON command object (preview only)")
class RuleOut(BaseModel): class RuleOut(BaseModel):
family: Optional[str] family: Optional[str]
table: Optional[str] table: Optional[str]
@@ -129,20 +141,13 @@ class RuleOut(BaseModel):
expr: Optional[Any] expr: Optional[Any]
# ---------- PREVIEW helpers ---------- # ---------- PREVIEW helpers (unchanged) ----------
def parse_text_cmd_summary(cmd: str) -> Dict[str, Any]: def parse_text_cmd_summary(cmd: str) -> Dict[str, Any]:
"""
Try to parse a short summary from a textual nft cmd.
We do NOT execute anything here — just regexp/token heuristics.
Returns a dict with possible keys: operation, family, table, chain, remainder.
"""
summary: Dict[str, Any] = {"operation": None, "family": None, "table": None, "chain": None, "remainder": cmd} summary: Dict[str, Any] = {"operation": None, "family": None, "table": None, "chain": None, "remainder": cmd}
# normalize whitespace
s = cmd.strip() s = cmd.strip()
tokens = s.split() tokens = s.split()
if len(tokens) >= 1: if len(tokens) >= 1:
summary["operation"] = tokens[0].lower() summary["operation"] = tokens[0].lower()
# detect patterns like: add rule <family> <table> <chain> ...
m = re.match(r'^(add|insert|delete|replace)\s+rule\s+(\S+)\s+(\S+)\s+(\S+)\b', s, flags=re.I) m = re.match(r'^(add|insert|delete|replace)\s+rule\s+(\S+)\s+(\S+)\s+(\S+)\b', s, flags=re.I)
if m: if m:
summary["operation"] = m.group(1).lower() summary["operation"] = m.group(1).lower()
@@ -151,7 +156,6 @@ def parse_text_cmd_summary(cmd: str) -> Dict[str, Any]:
summary["chain"] = m.group(4) summary["chain"] = m.group(4)
summary["remainder"] = s[m.end():].strip() summary["remainder"] = s[m.end():].strip()
return summary return summary
# detect delete rule by handle: e.g. delete rule inet filter input handle 42
m2 = re.match(r'^(delete)\s+rule\s+(\S+)\s+(\S+)\s+(\S+)\s+handle\s+(\d+)', s, flags=re.I) m2 = re.match(r'^(delete)\s+rule\s+(\S+)\s+(\S+)\s+(\S+)\s+handle\s+(\d+)', s, flags=re.I)
if m2: if m2:
summary["operation"] = m2.group(1).lower() summary["operation"] = m2.group(1).lower()
@@ -160,7 +164,6 @@ def parse_text_cmd_summary(cmd: str) -> Dict[str, Any]:
summary["chain"] = m2.group(4) summary["chain"] = m2.group(4)
summary["remainder"] = f"handle {m2.group(5)}" summary["remainder"] = f"handle {m2.group(5)}"
return summary return summary
# fallback: try to find family/table/chain tokens near 'rule'
try: try:
idx = next(i for i,t in enumerate(tokens) if t.lower() == "rule") idx = next(i for i,t in enumerate(tokens) if t.lower() == "rule")
if len(tokens) > idx + 3: if len(tokens) > idx + 3:
@@ -174,16 +177,11 @@ def parse_text_cmd_summary(cmd: str) -> Dict[str, Any]:
def parse_json_cmd_summary(obj: Dict[str, Any]) -> Dict[str, Any]: def parse_json_cmd_summary(obj: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract a short summary from a libnftables JSON command object.
Looks into top-level 'nftables' list for add/delete/replace/insert keys and extracts rule family/table/chain.
"""
summary = {"entries": []} summary = {"entries": []}
nft_entries = obj.get("nftables") if isinstance(obj, dict) else None nft_entries = obj.get("nftables") if isinstance(obj, dict) else None
if not isinstance(nft_entries, list): if not isinstance(nft_entries, list):
return {"error": "not a libnftables JSON object with 'nftables' list"} return {"error": "not a libnftables JSON object with 'nftables' list"}
for item in nft_entries: for item in nft_entries:
# each item is like {"add": {"rule": {"family":...}}} or {"delete": {...}}
if not isinstance(item, dict): if not isinstance(item, dict):
continue continue
for k, v in item.items(): for k, v in item.items():
@@ -193,19 +191,16 @@ def parse_json_cmd_summary(obj: Dict[str, Any]) -> Dict[str, Any]:
entry["family"] = r.get("family") entry["family"] = r.get("family")
entry["table"] = r.get("table") entry["table"] = r.get("table")
entry["chain"] = r.get("chain") entry["chain"] = r.get("chain")
# include handle if present
if "handle" in r: if "handle" in r:
entry["handle"] = r.get("handle") entry["handle"] = r.get("handle")
else: else:
# some commands are different shapes (e.g., add table ...). Try to capture names
# look for 'table' or 'chain' nested keys
if isinstance(v, dict): if isinstance(v, dict):
entry["info"] = {kk: vv for kk, vv in v.items() if kk in ("table", "chain", "family")} entry["info"] = {kk: vv for kk, vv in v.items() if kk in ("table", "chain", "family")}
summary["entries"].append(entry) summary["entries"].append(entry)
return summary return summary
# ---------- End preview helpers ---------- # ---------- Routes ----------
# List rules # List rules
@router.get("/rules", response_model=List[RuleOut]) @router.get("/rules", response_model=List[RuleOut])
@@ -227,34 +222,27 @@ def list_rules():
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
# Create rule: executes # Create/execute rule via JSON only (structured)
@router.post("/rules", status_code=status.HTTP_201_CREATED) @router.post("/rules", status_code=status.HTTP_201_CREATED)
def create_rule(req: CreateRuleRequest): def create_rule_json(req: CreateRuleJsonRequest):
""" """
Create a rule by sending either textual 'cmd' or libnftables 'json' object. Execute a libnftables JSON command object.
This endpoint executes the command. Use this endpoint to add structured rules or multi-op transactions.
""" """
try: try:
if req.cmd:
res = mgr.cmd(req.cmd)
if res["rc"] != 0:
raise NftError(f"cmd failed rc={res['rc']}: {res.get('stderr')}")
return {"status": "ok", "stdout": res.get("stdout")}
if req.json:
out = mgr.json_cmd(req.json) out = mgr.json_cmd(req.json)
return {"status": "ok", "output": out} return {"status": "ok", "output": out}
raise HTTPException(status_code=400, detail="either 'cmd' or 'json' must be provided")
except NftError as e: except NftError as e:
logger.warning("create_rule failed: %s", e) logger.warning("create_rule_json failed: %s", e)
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception as e: except Exception as e:
logger.exception("create_rule 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))
# PREVIEW endpoint (does NOT execute anything) # PREVIEW endpoint (does NOT execute anything)
@router.post("/preview") @router.post("/preview")
def preview_rule(req: CreateRuleRequest): def preview_rule(req: PreviewRequest):
""" """
Preview what would be applied if you executed the given 'cmd' or 'json'. Preview what would be applied if you executed the given 'cmd' or 'json'.
This endpoint only analyzes and returns: textual command (if present), pretty JSON (if present), This endpoint only analyzes and returns: textual command (if present), pretty JSON (if present),
@@ -315,12 +303,3 @@ def exec_raw(req: RawCmdRequest):
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
# Execute arbitrary JSON command
@router.post("/jsoncmd")
def exec_json(req: JsonCmdRequest):
try:
out = mgr.json_cmd(req.json)
return {"output": out}
except Exception as e:
logger.exception("exec_json failed")
raise HTTPException(status_code=400, detail=str(e))