add position to add rule
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
This commit is contained in:
@@ -255,7 +255,7 @@ class CreateRuleRequest(BaseModel):
|
|||||||
table: str = Field(..., description="Table name (e.g. filter)", example="filter")
|
table: str = Field(..., description="Table name (e.g. filter)", example="filter")
|
||||||
chain: str = Field(..., description="Chain name (e.g. forward)", example="forward")
|
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.")
|
expr: Any = Field(..., description="nft JSON expression (machine-readable). This field is required for JSON rule creation.")
|
||||||
position: Optional[Any] = Field(None, description="Optional position metadata (if you want to specify insertion position)")
|
position: Optional[int] = Field(None, description="Optional zero-based insertion position (0 = top). If omitted the rule is appended.")
|
||||||
comment: Optional[str] = Field(None, description="Optional comment")
|
comment: Optional[str] = Field(None, description="Optional comment")
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -264,6 +264,7 @@ class CreateRuleRequest(BaseModel):
|
|||||||
"family": "bridge",
|
"family": "bridge",
|
||||||
"table": "filter",
|
"table": "filter",
|
||||||
"chain": "forward",
|
"chain": "forward",
|
||||||
|
"position": 1,
|
||||||
"expr": [{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, {"drop": None}],
|
"expr": [{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, {"drop": None}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,7 +707,7 @@ def list_rules():
|
|||||||
def create_rule_json(req: CreateRuleRequest):
|
def create_rule_json(req: CreateRuleRequest):
|
||||||
"""
|
"""
|
||||||
Create a rule from JSON (expr required).
|
Create a rule from JSON (expr required).
|
||||||
- Attempts to render expr -> textual fragment and execute: `add rule <family> <table> <chain> <fragment>`
|
- Attempts to render expr -> textual fragment and execute: `add rule <family> <table> <chain> [position N] <fragment>`
|
||||||
- If rendering fails: 400 instructing the client to use POST /firewall/raw
|
- 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.
|
- 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
|
- If nft wrapper returns an invalid rc but the command produced no stderr, we double-check the chain
|
||||||
@@ -728,7 +729,39 @@ def create_rule_json(req: CreateRuleRequest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
expr_text = rendered.strip()
|
expr_text = rendered.strip()
|
||||||
cmd = f"add rule {family} {table} {chain} {expr_text}"
|
|
||||||
|
# If a position is provided, try to determine chain length to clamp the position.
|
||||||
|
position_token = ""
|
||||||
|
if req.position is not None:
|
||||||
|
# ensure numeric and non-negative
|
||||||
|
try:
|
||||||
|
pos_candidate = int(req.position)
|
||||||
|
except Exception:
|
||||||
|
raise NftError("position must be an integer >= 0")
|
||||||
|
if pos_candidate < 0:
|
||||||
|
raise NftError("position must be >= 0")
|
||||||
|
|
||||||
|
# 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)
|
logger.info("create_rule_json executing command: %s", cmd)
|
||||||
|
|
||||||
res = mgr.cmd(cmd)
|
res = mgr.cmd(cmd)
|
||||||
|
|||||||
@@ -320,13 +320,48 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
chain = chainVal;
|
chain = chainVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compute insertion position if user selected an "insert after" handle
|
||||||
|
let positionToken = '';
|
||||||
|
if (
|
||||||
|
vals.insertAfterHandle &&
|
||||||
|
tableSelect &&
|
||||||
|
tableSelect !== '__manual__' &&
|
||||||
|
chain &&
|
||||||
|
chain !== '__manual_chain__'
|
||||||
|
) {
|
||||||
|
// find index of the selected handle in current tables state
|
||||||
|
try {
|
||||||
|
const [f, n] = String(tableSelect).split(':');
|
||||||
|
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||||
|
if (tbl) {
|
||||||
|
const ch = tbl.chains.find((c) => c.name === chain);
|
||||||
|
if (ch) {
|
||||||
|
const idx = ch.rules.findIndex((r) => String(r.handle) === String(vals.insertAfterHandle));
|
||||||
|
if (idx >= 0) {
|
||||||
|
const pos = idx + 1; // insert after -> index+1
|
||||||
|
positionToken = ` position ${pos}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
positionToken = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const short = textFromExpr(expr);
|
const short = textFromExpr(expr);
|
||||||
const cmd = `add rule ${family} ${tableName} ${chain} ${short}`.trim();
|
const cmd = `add rule ${family} ${tableName} ${chain}${positionToken} ${short}`.trim();
|
||||||
setCmdPreview(cmd);
|
setCmdPreview(cmd);
|
||||||
|
|
||||||
const reqObj = { family, table: tableName, chain, expr };
|
const reqObj: any = { family, table: tableName, chain, expr };
|
||||||
|
if (positionToken) {
|
||||||
|
// extract numeric pos and include in request body
|
||||||
|
const m = positionToken.match(/position\s+(\d+)/);
|
||||||
|
if (m) {
|
||||||
|
reqObj.position = Number(m[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
setRequestJsonPreview(reqObj);
|
setRequestJsonPreview(reqObj);
|
||||||
}, [form]);
|
}, [form, tables]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updatePreviews();
|
updatePreviews();
|
||||||
@@ -377,9 +412,33 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expr = buildExprFromValues(values);
|
const expr = buildExprFromValues(values);
|
||||||
|
|
||||||
|
// compute optional position from insertAfterHandle
|
||||||
|
const reqObj: any = { family, table: tableName, chain, expr };
|
||||||
|
if (
|
||||||
|
values.insertAfterHandle &&
|
||||||
|
tableSelect &&
|
||||||
|
tableSelect !== '__manual__' &&
|
||||||
|
chain &&
|
||||||
|
chain !== '__manual_chain__'
|
||||||
|
) {
|
||||||
|
// locate index of handle and set position = index+1
|
||||||
|
const [f, n] = String(tableSelect).split(':');
|
||||||
|
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||||
|
if (tbl) {
|
||||||
|
const ch = tbl.chains.find((c) => c.name === chain);
|
||||||
|
if (ch) {
|
||||||
|
const idx = ch.rules.findIndex((r) => String(r.handle) === String(values.insertAfterHandle));
|
||||||
|
if (idx >= 0) {
|
||||||
|
reqObj.position = idx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const textual = textFromExpr(expr);
|
const textual = textFromExpr(expr);
|
||||||
const cmd = `add rule ${family} ${tableName} ${chain} ${textual}`.trim();
|
const posStr = reqObj.position !== undefined ? ` position ${reqObj.position}` : '';
|
||||||
const reqObj = { family, table: tableName, chain, expr };
|
const cmd = `add rule ${family} ${tableName} ${chain}${posStr} ${textual}`.trim();
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: 'Create rule (JSON)',
|
title: 'Create rule (JSON)',
|
||||||
@@ -409,7 +468,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
// refresh tables/chains after successful creation
|
// refresh tables/chains after successful creation
|
||||||
await loadTables();
|
await loadTables();
|
||||||
if (onCreated) await onCreated();
|
if (onCreated) await onCreated();
|
||||||
form.resetFields(['advanced']);
|
form.resetFields(['advanced', 'insertAfterHandle']);
|
||||||
} else {
|
} else {
|
||||||
message.error(`Create failed: ${res?.stderr ?? 'unknown error'}`);
|
message.error(`Create failed: ${res?.stderr ?? 'unknown error'}`);
|
||||||
}
|
}
|
||||||
@@ -426,7 +485,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
);
|
);
|
||||||
await loadTables();
|
await loadTables();
|
||||||
if (onCreated) await onCreated();
|
if (onCreated) await onCreated();
|
||||||
form.resetFields(['advanced']);
|
form.resetFields(['advanced', 'insertAfterHandle']);
|
||||||
} else {
|
} else {
|
||||||
const errMsg = typeof stderr === 'string' ? stderr : JSON.stringify(stderr);
|
const errMsg = typeof stderr === 'string' ? stderr : JSON.stringify(stderr);
|
||||||
message.error(`Create failed: ${errMsg}`);
|
message.error(`Create failed: ${errMsg}`);
|
||||||
@@ -445,7 +504,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[form, onCreated],
|
[form, onCreated, tables],
|
||||||
);
|
);
|
||||||
|
|
||||||
// prepare chain options for currently selected table
|
// prepare chain options for currently selected table
|
||||||
@@ -478,6 +537,24 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
);
|
);
|
||||||
}, [form, tables]);
|
}, [form, tables]);
|
||||||
|
|
||||||
|
// build insert-after options for currently selected table+chain
|
||||||
|
const insertAfterOptions = useMemo(() => {
|
||||||
|
const ts = form.getFieldValue('tableSelect');
|
||||||
|
const cs = form.getFieldValue('chainSelect');
|
||||||
|
if (!ts || ts === '__manual__' || !cs || cs === '__manual_chain__') return [];
|
||||||
|
const [f, n] = String(ts).split(':');
|
||||||
|
const tbl = tables.find((t) => t.family === f && t.name === n);
|
||||||
|
if (!tbl) return [];
|
||||||
|
const ch = tbl.chains.find((c) => c.name === cs);
|
||||||
|
if (!ch || !Array.isArray(ch.rules)) return [];
|
||||||
|
return ch.rules
|
||||||
|
.filter((r: any) => r && r.handle !== undefined && r.handle !== null)
|
||||||
|
.map((r: any) => ({
|
||||||
|
value: r.handle,
|
||||||
|
label: `#${r.handle} — ${r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || r).slice(0, 120))}`,
|
||||||
|
}));
|
||||||
|
}, [form, tables]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card style={{ maxWidth: 980 }}>
|
<Card style={{ maxWidth: 980 }}>
|
||||||
<Row justify="space-between" align="middle">
|
<Row justify="space-between" align="middle">
|
||||||
@@ -569,6 +646,41 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{/* Insert-after control */}
|
||||||
|
{form.getFieldValue('tableSelect') !== '__manual__' &&
|
||||||
|
form.getFieldValue('chainSelect') &&
|
||||||
|
form.getFieldValue('chainSelect') !== '__manual_chain__' && (
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="insertAfterHandle"
|
||||||
|
label="Insert after (optional)"
|
||||||
|
help="Pick an existing rule handle to insert *after*. If left empty the rule will be appended."
|
||||||
|
>
|
||||||
|
<Select allowClear placeholder="Append (no insert-after)">
|
||||||
|
{insertAfterOptions.length === 0 ? (
|
||||||
|
<Option value="__none__" disabled>
|
||||||
|
(no rules available)
|
||||||
|
</Option>
|
||||||
|
) : (
|
||||||
|
insertAfterOptions.map((o: any) => (
|
||||||
|
<Option key={String(o.value)} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</Option>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||||
|
<Text type="secondary">
|
||||||
|
Use when you want the new rule to appear right after a known handle. Refresh tables to see latest
|
||||||
|
handles.
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Protocol / addresses */}
|
{/* Protocol / addresses */}
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} sm={8}>
|
<Col xs={24} sm={8}>
|
||||||
|
|||||||
Reference in New Issue
Block a user