firewall api and FE
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:
@@ -1,5 +1,5 @@
|
|||||||
# app.py
|
# app.py
|
||||||
from typing import Any, Dict, List, Optional, 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 pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
import logging
|
import logging
|
||||||
@@ -119,7 +119,7 @@ class NftManager:
|
|||||||
try:
|
try:
|
||||||
parsed = json.loads(s)
|
parsed = json.loads(s)
|
||||||
# parsed may be the whole ruleset (nftables list) or a list; find any "rule" objects
|
# parsed may be the whole ruleset (nftables list) or a list; find any "rule" objects
|
||||||
rule_lines = []
|
rule_lines: List[str] = []
|
||||||
# parsed might be dict with "nftables" or a list of records
|
# parsed might be dict with "nftables" or a list of records
|
||||||
records = parsed.get("nftables") if isinstance(parsed, dict) else parsed
|
records = parsed.get("nftables") if isinstance(parsed, dict) else parsed
|
||||||
if not isinstance(records, list):
|
if not isinstance(records, list):
|
||||||
@@ -186,13 +186,13 @@ router = APIRouter(prefix="/firewall", tags=["firewall"])
|
|||||||
mgr = NftManager()
|
mgr = NftManager()
|
||||||
|
|
||||||
|
|
||||||
# ---------- Request/Response models ----------
|
# ---------- Request/Response models (strongly typed) ----------
|
||||||
class RawCmdRequest(BaseModel):
|
class RawCmdRequest(BaseModel):
|
||||||
cmd: str = Field(description="Textual nft command to execute", example="add rule inet filter input ip saddr 10.0.0.0/8 drop")
|
cmd: str = Field(..., description="Textual nft command to execute", example="add rule inet filter input ip saddr 10.0.0.0/8 drop")
|
||||||
|
|
||||||
|
|
||||||
class ExecResult(BaseModel):
|
class ExecResult(BaseModel):
|
||||||
rc: int = Field(description="Return code from nft execution", example=0)
|
rc: int = Field(..., description="Return code from nft execution", example=0)
|
||||||
stdout: Optional[str] = Field(None, description="Standard output from nft", example="")
|
stdout: Optional[str] = Field(None, description="Standard output from nft", example="")
|
||||||
stderr: Optional[str] = Field(None, description="Standard error from nft", example="")
|
stderr: Optional[str] = Field(None, description="Standard error from nft", example="")
|
||||||
|
|
||||||
@@ -200,19 +200,65 @@ class ExecResult(BaseModel):
|
|||||||
schema_extra = {"example": {"rc": 0, "stdout": "ok", "stderr": ""}}
|
schema_extra = {"example": {"rc": 0, "stdout": "ok", "stderr": ""}}
|
||||||
|
|
||||||
|
|
||||||
# ruleset may be a parsed JSON object (dict/list) OR a raw string (text) OR null
|
# --- Strong models returned to frontend ---
|
||||||
RulesetValue = Union[Dict[str, Any], List[Any], str, None]
|
class RuleOut(BaseModel):
|
||||||
|
handle: Optional[int] = Field(None, description="The rule handle (unique per rule), if available", example=3)
|
||||||
|
expr: Any = Field(..., description="Machine-readable nft expression (original nft JSON expr).")
|
||||||
|
text: str = Field(..., description="Deterministic short display string derived from expr", example="ip protocol icmp drop")
|
||||||
|
position: Optional[Any] = Field(None, description="Optional position metadata from nft if present")
|
||||||
|
comment: Optional[str] = Field(None, description="Optional comment attached to the rule")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"handle": 3,
|
||||||
|
"expr": [{"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, "op": "==", "right": "icmp"}}, {"drop": None}],
|
||||||
|
"text": "ip protocol icmp drop",
|
||||||
|
"position": None,
|
||||||
|
"comment": None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ChainOut(BaseModel):
|
||||||
|
name: str = Field(..., description="Chain name", example="forward")
|
||||||
|
rules: List[RuleOut] = Field(..., description="Rules in this chain (ordered)")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {"example": {"name": "forward", "rules": []}}
|
||||||
|
|
||||||
|
|
||||||
|
class TableOut(BaseModel):
|
||||||
|
family: str = Field(..., description="Table family (inet/bridge/ipv4/...)")
|
||||||
|
name: str = Field(..., description="Table name", example="filter")
|
||||||
|
chains: List[ChainOut] = Field(..., description="Chains in this table")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {"example": {"family": "bridge", "name": "filter", "chains": []}}
|
||||||
|
|
||||||
|
|
||||||
|
class RulesetModel(BaseModel):
|
||||||
|
tables: List[TableOut] = Field(..., description="Top-level tables list")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {"example": {"tables": []}}
|
||||||
|
|
||||||
|
|
||||||
|
# ruleset may be typed RulesetModel or raw textual string (fallback)
|
||||||
|
RulesetValue = Optional[Union[RulesetModel, str]]
|
||||||
|
|
||||||
|
|
||||||
class RulesetOut(BaseModel):
|
class RulesetOut(BaseModel):
|
||||||
ruleset: RulesetValue = Field(
|
ruleset: RulesetValue = Field(
|
||||||
description="parsed JSON ruleset (dict/list), or the raw textual ruleset as a string.",
|
None,
|
||||||
|
description="Parsed, strongly-typed ruleset (RulesetModel) or raw textual ruleset string if JSON is unavailable.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------- Helpers to convert to desired shape ----------
|
# ---------- Helpers to convert to desired shape ----------
|
||||||
_handle_re = re.compile(r"\s+#\s*handle\s+\d+\s*$")
|
_handle_re = re.compile(r"\s+#\s*handle\s+\d+\s*$")
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -278,7 +324,7 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
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", "rules":[]}}}
|
# Build intermediate map: (family, table) -> {family, name, chains: {chain_name: {"name", "rules":[]}}}
|
||||||
tables: Dict[tuple, Dict[str, Any]] = {}
|
tables: Dict[Tuple[str, str], Dict[str, Any]] = {}
|
||||||
for rec in items:
|
for rec in items:
|
||||||
if "table" in rec:
|
if "table" in rec:
|
||||||
t = rec["table"]
|
t = rec["table"]
|
||||||
@@ -332,7 +378,8 @@ def build_predictable_ruleset(nft_json: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
@router.get("/rules", response_model=RulesetOut, summary="List ruleset")
|
@router.get("/rules", response_model=RulesetOut, summary="List ruleset")
|
||||||
def list_rules():
|
def list_rules():
|
||||||
"""
|
"""
|
||||||
Returns the ruleset in a stable, predictable 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:
|
Structure:
|
||||||
{ "ruleset": { "tables": [ { "family": ..., "name": ..., "chains": [ { "name": ..., "rules": [ { "handle", "expr", "text" } ] } ] } ] } }
|
{ "ruleset": { "tables": [ { "family": ..., "name": ..., "chains": [ { "name": ..., "rules": [ { "handle", "expr", "text" } ] } ] } ] } }
|
||||||
|
|
||||||
@@ -345,10 +392,12 @@ def list_rules():
|
|||||||
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)
|
||||||
text = mgr.list_rules()
|
text = mgr.list_rules()
|
||||||
return {"ruleset": text.strip() if text is not None else None}
|
return RulesetOut(ruleset=text.strip() if text is not None else None)
|
||||||
|
|
||||||
custom = build_predictable_ruleset(nft_json)
|
custom = build_predictable_ruleset(nft_json)
|
||||||
return {"ruleset": custom}
|
# Validate/construct Pydantic model so OpenAPI + client libs get accurate typing
|
||||||
|
ruleset_model = RulesetModel.parse_obj(custom)
|
||||||
|
return RulesetOut(ruleset=ruleset_model)
|
||||||
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))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/apiClient.ts
|
// src/apiClient.ts
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { RulesetModel } from "../types/firewall";
|
||||||
import {
|
import {
|
||||||
BridgeCreateRequest,
|
BridgeCreateRequest,
|
||||||
BridgeInfo,
|
BridgeInfo,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
InterfaceInfo,
|
InterfaceInfo,
|
||||||
RouteInfo,
|
RouteInfo,
|
||||||
} from "../types/network";
|
} from "../types/network";
|
||||||
|
import { EnableRequest, ScriptInfo } from "../types/scripting";
|
||||||
import {
|
import {
|
||||||
SnifferStatusResponse,
|
SnifferStatusResponse,
|
||||||
} from "../types/sniffer";
|
} from "../types/sniffer";
|
||||||
@@ -36,30 +38,6 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Local lightweight types for a few endpoints where a dedicated project type
|
|
||||||
* wasn't imported above. If you already have these in your codebase, replace
|
|
||||||
* these with imports instead.
|
|
||||||
*/
|
|
||||||
export type RawCmdRequest = {
|
|
||||||
cmd: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RulesetOut = {
|
|
||||||
ruleset: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ScriptInfo = {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EnableRequest = {
|
|
||||||
qnum: number;
|
|
||||||
service_name?: string | null;
|
|
||||||
extra_args?: string | null;
|
|
||||||
enable_at_boot?: boolean | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Basic endpoints
|
Basic endpoints
|
||||||
@@ -150,33 +128,30 @@ export const fetchPackets = async (limit = 100): Promise<any> => {
|
|||||||
Firewall
|
Firewall
|
||||||
------------------------- */
|
------------------------- */
|
||||||
|
|
||||||
export const listFirewallRules = async (): Promise<RulesetOut> => {
|
/**
|
||||||
const res = await api.get<RulesetOut>("/firewall/rules");
|
* GET /firewall/rules
|
||||||
|
* Returns: { ruleset: RulesetModel | string | null }
|
||||||
|
* - If the server returns a raw textual fallback (string), the caller should handle it.
|
||||||
|
*/
|
||||||
|
export const fetchRuleset = async (): Promise<{ ruleset: RulesetModel | string | null }> => {
|
||||||
|
const res = await api.get<{ ruleset: RulesetModel | string | null }>("/firewall/rules");
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFirewallRuleText = async (req: RawCmdRequest): Promise<any> => {
|
/**
|
||||||
// Executes textual nft command, returns raw stdout (201 expected per spec)
|
* DELETE /firewall/rules/{handle}?family=...&table=...&chain=...
|
||||||
const res = await api.post("/firewall/rules", req);
|
* On success the backend returns 204 No Content. This function resolves to void.
|
||||||
return res.data;
|
*/
|
||||||
};
|
export const deleteRule = async (
|
||||||
|
|
||||||
export const deleteFirewallRule = async (
|
|
||||||
handle: number,
|
handle: number,
|
||||||
options?: { family?: string; table?: string; chain?: string }
|
family: string,
|
||||||
|
table: string,
|
||||||
|
chain: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
// returns 204 on success (no content)
|
const res = await api.delete(`/firewall/rules/${encodeURIComponent(String(handle))}`, {
|
||||||
const params: Record<string, any> = {};
|
params: { family, table, chain },
|
||||||
if (options?.family) params.family = options.family;
|
});
|
||||||
if (options?.table) params.table = options.table;
|
// axios resolves non-2xx as reject; server uses 204 No Content so nothing to return
|
||||||
if (options?.chain) params.chain = options.chain;
|
|
||||||
|
|
||||||
await api.delete(`/firewall/rules/${handle}`, { params });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const execFirewallRaw = async (req: RawCmdRequest): Promise<any> => {
|
|
||||||
// Execute arbitrary textual nft command and return {rc, stdout, stderr}
|
|
||||||
const res = await api.post("/firewall/raw", req);
|
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
239
frontend/src/pages/Firewall.tsx
Normal file
239
frontend/src/pages/Firewall.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
// src/components/RulesView.tsx
|
||||||
|
import { DeleteOutlined, EyeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Card, Collapse, Drawer, Empty, Popconfirm, Space, Spin, Table, Typography, message } from 'antd';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { deleteRule, fetchRuleset } from '../api/apiClient';
|
||||||
|
import { ChainOut, RulesetModel, TableOut } from '../types/firewall';
|
||||||
|
|
||||||
|
const { Panel } = Collapse;
|
||||||
|
const { Paragraph, Text } = Typography;
|
||||||
|
|
||||||
|
interface RuleRow {
|
||||||
|
key: string;
|
||||||
|
handle?: number | null;
|
||||||
|
text: string;
|
||||||
|
expr: any;
|
||||||
|
comment?: string | null;
|
||||||
|
position?: any;
|
||||||
|
// context for delete
|
||||||
|
family: string;
|
||||||
|
table: string;
|
||||||
|
chain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render rules inside an antd Table. The UI layout:
|
||||||
|
* - Collapse per table (title = family:name)
|
||||||
|
* - Inside each table -> collapse per chain (only chains that have rules)
|
||||||
|
* - Each chain shows a Table of rules with delete and inspect actions
|
||||||
|
*/
|
||||||
|
export const RulesView: React.FC<{ apiBase?: string }> = ({ apiBase = '' }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [ruleset, setRuleset] = useState<RulesetModel | null>(null);
|
||||||
|
const [rawFallback, setRawFallback] = useState<string | null>(null);
|
||||||
|
const [exprDrawerVisible, setExprDrawerVisible] = useState(false);
|
||||||
|
const [exprForView, setExprForView] = useState<any>(null);
|
||||||
|
const [selectedRuleContext, setSelectedRuleContext] = useState<{
|
||||||
|
family: string;
|
||||||
|
table: string;
|
||||||
|
chain: string;
|
||||||
|
handle?: number | null;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const fetchRules = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await fetchRuleset();
|
||||||
|
if (!data || data.ruleset === null) {
|
||||||
|
setRuleset({ tables: [] });
|
||||||
|
setRawFallback(null);
|
||||||
|
} else if (typeof data.ruleset === 'string') {
|
||||||
|
// fallback textual ruleset
|
||||||
|
setRawFallback(data.ruleset);
|
||||||
|
setRuleset({ tables: [] });
|
||||||
|
} else {
|
||||||
|
setRuleset(data.ruleset as RulesetModel);
|
||||||
|
setRawFallback(null);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(`Failed to load ruleset: ${err?.message ?? err}`);
|
||||||
|
setRuleset({ tables: [] });
|
||||||
|
setRawFallback(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [apiBase]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRules();
|
||||||
|
}, [fetchRules]);
|
||||||
|
|
||||||
|
const onDelete = useCallback(
|
||||||
|
async (row: RuleRow) => {
|
||||||
|
if (!row.handle) {
|
||||||
|
message.error('Rule has no handle and cannot be deleted via API.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await deleteRule(row.handle, row.family, row.table, row.chain);
|
||||||
|
message.success('Rule deleted');
|
||||||
|
await fetchRules();
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(`Delete failed: ${err?.message ?? err}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[apiBase, fetchRules],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openExprViewer = useCallback((expr: any) => {
|
||||||
|
setExprForView(expr);
|
||||||
|
setExprDrawerVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeExprViewer = useCallback(() => {
|
||||||
|
setExprForView(null);
|
||||||
|
setExprDrawerVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// columns for rule table
|
||||||
|
const columns: ColumnsType<RuleRow> = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: 'Handle',
|
||||||
|
dataIndex: 'handle',
|
||||||
|
key: 'handle',
|
||||||
|
width: 100,
|
||||||
|
render: (val) => val ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Rule',
|
||||||
|
dataIndex: 'text',
|
||||||
|
key: 'text',
|
||||||
|
render: (txt: string, rec: RuleRow) => (
|
||||||
|
<Paragraph copyable={{ text: txt }} style={{ margin: 0 }}>
|
||||||
|
{txt}
|
||||||
|
</Paragraph>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Comment',
|
||||||
|
dataIndex: 'comment',
|
||||||
|
key: 'comment',
|
||||||
|
width: 200,
|
||||||
|
render: (c) => (c ? <Text>{c}</Text> : null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Actions',
|
||||||
|
key: 'actions',
|
||||||
|
width: 160,
|
||||||
|
align: 'right',
|
||||||
|
render: (_, rec) => (
|
||||||
|
<Space>
|
||||||
|
<Button type="default" icon={<EyeOutlined />} size="small" onClick={() => openExprViewer(rec.expr)}>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
<Popconfirm title="Delete this rule?" onConfirm={() => onDelete(rec)} okText="Delete" cancelText="Cancel">
|
||||||
|
<Button danger icon={<DeleteOutlined />} size="small">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[onDelete, openExprViewer],
|
||||||
|
);
|
||||||
|
|
||||||
|
// flatten rules for each chain into a table datasource
|
||||||
|
const renderChainTable = (table: TableOut, chain: ChainOut) => {
|
||||||
|
const data: RuleRow[] = chain.rules.map((r, idx) => ({
|
||||||
|
key: `${table.family}:${table.name}:${chain.name}:${String(r.handle ?? idx)}`,
|
||||||
|
handle: r.handle ?? null,
|
||||||
|
text: r.text,
|
||||||
|
expr: r.expr,
|
||||||
|
comment: r.comment ?? null,
|
||||||
|
position: r.position,
|
||||||
|
family: table.family,
|
||||||
|
table: table.name,
|
||||||
|
chain: chain.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table columns={columns} dataSource={data} pagination={{ pageSize: 8 }} size="small" rowKey={(rec) => rec.key} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI when JSON is not available
|
||||||
|
if (!loading && rawFallback) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Text strong>Ruleset (raw)</Text>
|
||||||
|
<Paragraph style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{rawFallback}</Paragraph>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => fetchRules()}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="Firewall rules"
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => fetchRules()} loading={loading}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: 40 }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : ruleset && ruleset.tables.length > 0 ? (
|
||||||
|
<Collapse accordion>
|
||||||
|
{ruleset.tables.map((t) => {
|
||||||
|
// only show chains that have rules
|
||||||
|
const chainsWithRules = t.chains.filter((c) => c.rules && c.rules.length > 0);
|
||||||
|
if (chainsWithRules.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<Panel header={`${t.family}:${t.name}`} key={`${t.family}:${t.name}`}>
|
||||||
|
<Collapse>
|
||||||
|
{chainsWithRules.map((c) => (
|
||||||
|
<Panel header={`${c.name} — ${c.rules.length} rule(s)`} key={`${t.family}:${t.name}:${c.name}`}>
|
||||||
|
{renderChainTable(t, c)}
|
||||||
|
</Panel>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Collapse>
|
||||||
|
) : (
|
||||||
|
<Empty description="No rules found" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title="Rule expression (JSON)"
|
||||||
|
placement="right"
|
||||||
|
width={640}
|
||||||
|
onClose={closeExprViewer}
|
||||||
|
open={exprDrawerVisible}
|
||||||
|
>
|
||||||
|
<Paragraph>
|
||||||
|
<Text strong>Expression (raw JSON)</Text>
|
||||||
|
</Paragraph>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(exprForView, null, 2)}</pre>
|
||||||
|
</Drawer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RulesView;
|
||||||
24
frontend/src/types/firewall.ts
Normal file
24
frontend/src/types/firewall.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export type Expr = any; // raw nft expr JSON (opaque to frontend)
|
||||||
|
|
||||||
|
export interface RuleOut {
|
||||||
|
handle?: number | null;
|
||||||
|
expr: Expr;
|
||||||
|
text: string;
|
||||||
|
position?: any;
|
||||||
|
comment?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChainOut {
|
||||||
|
name: string;
|
||||||
|
rules: RuleOut[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableOut {
|
||||||
|
family: string;
|
||||||
|
name: string;
|
||||||
|
chains: ChainOut[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RulesetModel {
|
||||||
|
tables: TableOut[];
|
||||||
|
}
|
||||||
11
frontend/src/types/scripting.ts
Normal file
11
frontend/src/types/scripting.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export type ScriptInfo = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EnableRequest = {
|
||||||
|
qnum: number;
|
||||||
|
service_name?: string | null;
|
||||||
|
extra_args?: string | null;
|
||||||
|
enable_at_boot?: boolean | null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user