From eb1eef23c4c608358f91281130d2cb257691ed6d Mon Sep 17 00:00:00 2001 From: malmert Date: Sat, 28 Feb 2026 15:56:48 +0100 Subject: [PATCH] fix position --- backend/src/api/nft_manager.py | 76 +++++++--------------------------- frontend/src/types/firewall.ts | 2 +- 2 files changed, 15 insertions(+), 63 deletions(-) diff --git a/backend/src/api/nft_manager.py b/backend/src/api/nft_manager.py index 9ba9871..b809ddc 100644 --- a/backend/src/api/nft_manager.py +++ b/backend/src/api/nft_manager.py @@ -255,7 +255,7 @@ class CreateRuleRequest(BaseModel): table: str = Field(..., description="Table name (e.g. filter)", example="filter") chain: str = Field(..., description="Chain name (e.g. forward)", example="forward") expr: Any = Field(..., description="nft JSON expression (machine-readable). This field is required for JSON rule creation.") - position: Optional[int] = Field(None, description="Optional zero-based insertion position (0 = top). If omitted the rule is appended.") + position: Optional[int] = Field(None, description="Optional insertion position (zero-based). If provided endpoint will insert at that position.") comment: Optional[str] = Field(None, description="Optional comment") class Config: @@ -264,42 +264,12 @@ class CreateRuleRequest(BaseModel): "family": "bridge", "table": "filter", "chain": "forward", - "position": 1, "expr": [{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, {"drop": None}], + "position": 0, } } -# ---------- Move endpoints: request/response models ---------- -class MoveRequest(BaseModel): - """ - Request body for moving a rule. Exactly one of these should typically be provided: - - position: numeric index to insert at (0 = first) - - before_handle: insert before an existing handle in the same chain - - to_top: boolean - - to_bottom: boolean - family/table/chain are required to identify the chain. - """ - family: str = Field(..., example="bridge") - table: str = Field(..., example="filter") - chain: str = Field(..., example="forward") - position: Optional[int] = Field(None, description="Zero-based position to insert at (0 = top)") - before_handle: Optional[int] = Field(None, description="Insert before this handle (find its index and use that)") - to_top: Optional[bool] = Field(False, description="Move to top (equivalent to position=0)") - to_bottom: Optional[bool] = Field(False, description="Move to bottom (append)") - - -class MoveSubResult(BaseModel): - cmd: str - out: Optional[ExecResult] = None - err: Optional[str] = None - - -class MoveResult(BaseModel): - added: MoveSubResult - deleted: MoveSubResult - - # ruleset may be typed RulesetModel or raw textual string (fallback) RulesetValue = Optional[Union[RulesetModel, str]] @@ -601,7 +571,6 @@ def expr_to_text(expr: Any) -> Optional[str]: elif isinstance(q, (int, float)): token += f" num {int(q)}" elif isinstance(q, str): - # preserve string but ensure spacing: client must supply numeric if nft expects it token += f" num {q}" parts.append(token) continue @@ -707,7 +676,8 @@ def list_rules(): def create_rule_json(req: CreateRuleRequest): """ Create a rule from JSON (expr required). - - Attempts to render expr -> textual fragment and execute: `add rule [position N] ` + - If req.position is provided, uses: insert rule
position + - Otherwise, uses: add rule
(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 @@ -730,38 +700,20 @@ def create_rule_json(req: CreateRuleRequest): expr_text = rendered.strip() - # If a position is provided, try to determine chain length to clamp the position. - position_token = "" + # If a position is explicitly requested, use the 'insert rule ... position ...' form. + # 'add rule ... position ...' is not supported by some nft versions / syntaxes. if req.position is not None: - # ensure numeric and non-negative try: - pos_candidate = int(req.position) + pos = int(req.position) + # clamp pos to >= 0 + if pos < 0: + pos = 0 except Exception: - raise NftError("position must be an integer >= 0") - if pos_candidate < 0: - raise NftError("position must be >= 0") + pos = 0 + cmd = f"insert rule {family} {table} {chain} position {pos} {expr_text}" + else: + cmd = f"add rule {family} {table} {chain} {expr_text}" - # attempt to read current ruleset to know chain length - try: - nft_json = mgr.list_rules_json() - custom = build_predictable_ruleset(nft_json) - chain_rules: List[Dict[str, Any]] = [] - for t in custom.get("tables", []): - if t.get("family") == family and t.get("name") == table: - for ch in t.get("chains", []): - if ch.get("name") == chain: - chain_rules = ch.get("rules", []) - break - chain_len = len(chain_rules) - # clamp position to [0, chain_len] - pos = max(0, min(chain_len, pos_candidate)) - except Exception: - # if we cannot read ruleset, just use provided pos_candidate (server may still accept or fail) - pos = pos_candidate - - position_token = f" position {pos}" - - cmd = f"add rule {family} {table} {chain}{position_token} {expr_text}" logger.info("create_rule_json executing command: %s", cmd) res = mgr.cmd(cmd) diff --git a/frontend/src/types/firewall.ts b/frontend/src/types/firewall.ts index 18ff7f4..e87024c 100644 --- a/frontend/src/types/firewall.ts +++ b/frontend/src/types/firewall.ts @@ -28,7 +28,7 @@ export interface CreateRuleRequest { table: string; chain: string; expr: Expr; - position?: any | null; + position?: number | null; comment?: string | null; }