add: filtering table of firewall + InterfaceSelector frontend fixes and improves

This commit is contained in:
Domingo Dirutigliano
2024-10-19 18:39:42 +02:00
parent 2658e74aca
commit d64e0aa73c
12 changed files with 147 additions and 86 deletions

View File

@@ -8,20 +8,19 @@ FROM --platform=$BUILDPLATFORM oven/bun AS frontend
WORKDIR /app
ADD ./frontend/package.json .
ADD ./frontend/bun.lockb .
RUN bun install
RUN bun i
COPY ./frontend/ .
RUN bun run build
#Building main conteiner
FROM --platform=$TARGETARCH debian:stable-slim AS base
RUN apt-get update -qq && apt-get upgrade -qq
RUN apt-get install -qq python3-pip build-essential
RUN apt-get install -qq git libpcre2-dev libnetfilter-queue-dev
RUN apt-get install -qq libssl-dev libnfnetlink-dev libmnl-dev libcap2-bin
RUN apt-get install -qq make cmake nftables libboost-all-dev autoconf
RUN apt-get install -qq automake cargo libffi-dev libvectorscan-dev libtins-dev
RUN apt-get install -qq python3-nftables
RUN apt-get update -qq && apt-get upgrade -qq && \
apt-get install -qq python3-pip build-essential \
git libpcre2-dev libnetfilter-queue-dev libssl-dev \
libnfnetlink-dev libmnl-dev libcap2-bin make cmake \
nftables libboost-all-dev autoconf automake cargo \
libffi-dev libvectorscan-dev libtins-dev python3-nftables
WORKDIR /tmp/
RUN git clone --single-branch --branch release https://github.com/jpcre2/jpcre2

View File

@@ -1,6 +1,6 @@
import uvicorn, secrets, utils
import os, asyncio
from fastapi import FastAPI, HTTPException, Depends, APIRouter
import os, asyncio, logging
from fastapi import FastAPI, HTTPException, Depends, APIRouter, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt
from passlib.context import CryptContext
@@ -34,7 +34,14 @@ app = FastAPI(debug=DEBUG, redoc_url=None, lifespan=lifespan)
utils.socketio = SocketManager(app, "/sock", socketio_path="")
if DEBUG:
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def APP_STATUS(): return "init" if db.get("password") is None else "run"
def JWT_SECRET(): return db.get("secret")
@@ -132,7 +139,10 @@ async def startup_main():
db.init()
if os.getenv("HEX_SET_PSW"):
set_psw(bytes.fromhex(os.getenv("HEX_SET_PSW")).decode())
sysctl.set()
try:
sysctl.set()
except Exception as e:
logging.error(f"Error setting sysctls: {e}")
await startup()
if not JWT_SECRET(): db.put("secret", secrets.token_hex(32))
await refresh_frontend()
@@ -149,7 +159,10 @@ async def reset_firegex(form: ResetRequest):
db.delete()
db.init()
db.put("secret", secrets.token_hex(32))
sysctl.set()
try:
sysctl.set()
except Exception as e:
logging.error(f"Error setting sysctls: {e}")
await reset(form)
await refresh_frontend()
return {'status': 'ok'}

View File

@@ -3,7 +3,7 @@ from utils import PortType
from pydantic import BaseModel
class Rule:
def __init__(self, proto: str, src:str, dst:str, port_src_from:str, port_dst_from:str, port_src_to:str, port_dst_to:str, action:str, mode:str, **other):
def __init__(self, proto: str, src:str, dst:str, port_src_from:str, port_dst_from:str, port_src_to:str, port_dst_to:str, action:str, mode:str, table:str, **_other):
self.proto = proto
self.src = src
self.dst = dst
@@ -15,6 +15,7 @@ class Rule:
self.input_mode = mode == "in"
self.output_mode = mode == "out"
self.forward_mode = mode == "forward"
self.table = table
@classmethod
def from_dict(cls, var: dict):
@@ -32,6 +33,10 @@ class Mode(str, Enum):
OUT = "out",
FORWARD = "forward"
class Table(str, Enum):
FILTER = "filter"
MANGLE = "mangle"
class Action(str, Enum):
ACCEPT = "accept",
DROP = "drop",
@@ -41,6 +46,7 @@ class RuleModel(BaseModel):
active: bool
name: str
proto: Protocol
table: Table
src: str
dst: str
port_src_from: PortType
@@ -48,7 +54,7 @@ class RuleModel(BaseModel):
port_src_to: PortType
port_dst_to: PortType
action: Action
mode:Mode
mode: Mode
class RuleFormAdd(BaseModel):
rules: list[RuleModel]

View File

@@ -7,12 +7,16 @@ class FiregexTables(NFTableManager):
rules_chain_out = "firegex_firewall_rules_out"
rules_chain_fwd = "firegex_firewall_rules_fwd"
filter_table = "filter"
mangle_table = "mangle"
def init_comands(self, policy:str=Action.ACCEPT, opt: FirewallSettings|None = None):
rules = [
{"add":{"table":{"name":self.filter_table,"family":"ip"}}},
{"add":{"table":{"name":self.filter_table,"family":"ip6"}}},
{"add":{"table":{"name":self.mangle_table,"family":"ip"}}},
{"add":{"table":{"name":self.mangle_table,"family":"ip6"}}},
{"add":{"chain":{"family":"ip","table":self.filter_table, "name":"INPUT","type":"filter","hook":"input","prio":0,"policy":policy}}},
{"add":{"chain":{"family":"ip6","table":self.filter_table,"name":"INPUT","type":"filter","hook":"input","prio":0,"policy":policy}}},
{"add":{"chain":{"family":"ip","table":self.filter_table,"name":"FORWARD","type":"filter","hook":"forward","prio":0,"policy":policy}}},
@@ -20,12 +24,22 @@ class FiregexTables(NFTableManager):
{"add":{"chain":{"family":"ip","table":self.filter_table,"name":"OUTPUT","type":"filter","hook":"output","prio":0,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip6","table":self.filter_table,"name":"OUTPUT","type":"filter","hook":"output","prio":0,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip","table":self.mangle_table, "name":"PREROUTING","type":"filter","hook":"prerouting","prio":-150,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip6","table":self.mangle_table,"name":"PREROUTING","type":"filter","hook":"prerouting","prio":-150,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip","table":self.mangle_table, "name":"POSTROUTING","type":"filter","hook":"postrouting","prio":-150,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip6","table":self.mangle_table,"name":"POSTROUTING","type":"filter","hook":"postrouting","prio":-150,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip","table":self.filter_table,"name":self.rules_chain_in}}},
{"add":{"chain":{"family":"ip6","table":self.filter_table,"name":self.rules_chain_in}}},
{"add":{"chain":{"family":"ip","table":self.filter_table,"name":self.rules_chain_out}}},
{"add":{"chain":{"family":"ip6","table":self.filter_table,"name":self.rules_chain_out}}},
{"add":{"chain":{"family":"ip","table":self.filter_table,"name":self.rules_chain_fwd}}},
{"add":{"chain":{"family":"ip6","table":self.filter_table,"name":self.rules_chain_fwd}}},
{"add":{"chain":{"family":"ip","table":self.mangle_table,"name":self.rules_chain_in}}},
{"add":{"chain":{"family":"ip6","table":self.mangle_table,"name":self.rules_chain_in}}},
{"add":{"chain":{"family":"ip","table":self.mangle_table,"name":self.rules_chain_out}}},
{"add":{"chain":{"family":"ip6","table":self.mangle_table,"name":self.rules_chain_out}}},
]
if opt is None: return rules
@@ -158,42 +172,49 @@ class FiregexTables(NFTableManager):
def __init__(self):
super().__init__(self.init_comands(),[
{"add":{"chain":{"family":"ip","table":self.filter_table, "name":"INPUT","type":"filter","hook":"input","prio":0,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip6","table":self.filter_table,"name":"INPUT","type":"filter","hook":"input","prio":0,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip","table":self.filter_table,"name":"FORWARD","type":"filter","hook":"forward","prio":0,"policy":Action.ACCEPT}}},
{"add":{"chain":{"family":"ip6","table":self.filter_table,"name":"FORWARD","type":"filter","hook":"forward","prio":0,"policy":Action.ACCEPT}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip", "name":self.rules_chain_in}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip", "name":self.rules_chain_out}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip", "name":self.rules_chain_fwd}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip6", "name":self.rules_chain_in}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip6", "name":self.rules_chain_out}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip6", "name":self.rules_chain_fwd}}},
{"flush":{"chain":{"table":self.mangle_table,"family":"ip", "name":self.rules_chain_in}}},
{"flush":{"chain":{"table":self.mangle_table,"family":"ip", "name":self.rules_chain_out}}},
{"flush":{"chain":{"table":self.mangle_table,"family":"ip6", "name":self.rules_chain_in}}},
{"flush":{"chain":{"table":self.mangle_table,"family":"ip6", "name":self.rules_chain_out}}}
])
def chain_to_firegex(self, chain:str):
match chain:
case "INPUT": return self.rules_chain_in
case "OUTPUT": return self.rules_chain_out
case "FORWARD": return self.rules_chain_fwd
def chain_to_firegex(self, chain:str, table:str):
if table == self.filter_table:
match chain:
case "INPUT": return self.rules_chain_in
case "OUTPUT": return self.rules_chain_out
case "FORWARD": return self.rules_chain_fwd
elif table == self.mangle_table:
match chain:
case "PREROUTING": return self.rules_chain_in
case "POSTROUTING": return self.rules_chain_out
return None
def insert_firegex_chains(self):
rules:list[dict] = list(self.list_rules(tables=[self.filter_table], chains=["INPUT", "OUTPUT", "FORWARD"]))
for family in ["ip", "ip6"]:
for chain in ["INPUT", "OUTPUT", "FORWARD"]:
found = False
rule_to_add = [{ "jump": { "target": self.chain_to_firegex(chain) }}]
for r in rules:
if r.get("family") == family and r.get("table") == "filter" and r.get("chain") == chain and r.get("expr") == rule_to_add:
found = True
break
if found: continue
yield { "add":{ "rule": {
"family": family,
"table": self.filter_table,
"chain": chain,
"expr": rule_to_add
}}}
rules:list[dict] = list(self.list_rules(tables=[self.filter_table, self.mangle_table], chains=["INPUT", "OUTPUT", "FORWARD", "PREROUTING", "POSTROUTING"]))
for table in [self.filter_table, self.mangle_table]:
for family in ["ip", "ip6"]:
for chain in ["INPUT", "OUTPUT", "FORWARD"] if table == self.filter_table else ["PREROUTING", "POSTROUTING"]:
found = False
rule_to_add = [{ "jump": { "target": self.chain_to_firegex(chain, table) }}]
for r in rules:
if r.get("family") == family and r.get("table") == table and r.get("chain") == chain and r.get("expr") == rule_to_add:
found = True
break
if found: continue
yield { "add":{ "rule": {
"family": family,
"table": table,
"chain": chain,
"expr": rule_to_add
}}}
def set(self, srvs:list[Rule], policy:str=Action.ACCEPT, opt:FirewallSettings = None):
srvs = list(srvs)
@@ -209,7 +230,8 @@ class FiregexTables(NFTableManager):
port_src_to=65535,
port_dst_to=65535,
action=Action.REJECT,
mode=Mode.IN
mode=Mode.IN,
table=Table.FILTER
))
rules = self.init_comands(policy, opt) + list(self.insert_firegex_chains()) + self.get_rules(*srvs)
@@ -261,7 +283,7 @@ class FiregexTables(NFTableManager):
for fam in families:
rules.append({ "add":{ "rule": {
"family": fam,
"table": self.filter_table,
"table": srv.table,
"chain": self.rules_chain_out if srv.output_mode else self.rules_chain_in if srv.input_mode else self.rules_chain_fwd,
"expr": ip_filters + port_filters + end_rules
}}})

View File

@@ -11,6 +11,7 @@ db = SQLite('db/firewall-rules.db', {
'rules': {
'rule_id': 'INT PRIMARY KEY CHECK (rule_id >= 0)',
'mode': 'VARCHAR(10) NOT NULL CHECK (mode IN ("in", "out", "forward"))',
'`table`': 'VARCHAR(10) NOT NULL CHECK (`table` IN ("filter", "mangle", "raw"))',
'name': 'VARCHAR(100) NOT NULL',
'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1))',
'proto': 'VARCHAR(10) NOT NULL CHECK (proto IN ("tcp", "udp", "both", "any"))',
@@ -81,7 +82,7 @@ async def get_rule_list():
"""Get the list of existent firegex rules"""
return {
"policy": firewall.policy,
"rules": db.query("SELECT active, name, proto, src, dst, port_src_from, port_dst_from, port_src_to, port_dst_to, action, mode FROM rules ORDER BY rule_id;"),
"rules": db.query("SELECT active, name, proto, src, dst, port_src_from, port_dst_from, port_src_to, port_dst_to, action, mode, `table` FROM rules ORDER BY rule_id;"),
"enabled": firewall.enabled
}
@@ -99,6 +100,9 @@ async def disable_firewall():
def parse_and_check_rule(rule:RuleModel):
if rule.table == Table.MANGLE and rule.mode == Mode.FORWARD:
raise HTTPException(status_code=400, detail="Mangle table does not support forward mode")
is_src_ip = is_dst_ip = True
try:
@@ -137,14 +141,14 @@ async def add_new_service(form: RuleFormAdd):
src, dst,
port_src_from, port_dst_from,
port_src_to, port_dst_to,
action, mode
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,?, ?)""",
action, mode, `table`
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,?, ?, ?)""",
rid, ele.active, ele.name,
ele.proto,
ele.src, ele.dst,
ele.port_src_from, ele.port_dst_from,
ele.port_src_to, ele.port_dst_to,
ele.action, ele.mode
ele.action, ele.mode, ele.table
) for rid, ele in enumerate(rules)]
)
firewall.policy = form.policy.value

View File

@@ -12,8 +12,8 @@ socketio:SocketManager = None
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
ON_DOCKER = len(sys.argv) > 1 and sys.argv[1] == "DOCKER"
DEBUG = len(sys.argv) > 1 and sys.argv[1] == "DEBUG"
ON_DOCKER = "DOCKER" in sys.argv
DEBUG = "DEBUG" in sys.argv
FIREGEX_PORT = int(os.getenv("PORT","4444"))
JWT_ALGORITHM: str = "HS256"
API_VERSION = "2.2.0"
@@ -44,9 +44,10 @@ class SysctlManager:
for name in ctl_table.keys():
self.old_table[name] = read_sysctl(name)
def write_table(self, table):
def write_table(self, table) -> bool:
for name, value in table.items():
write_sysctl(name, value)
if read_sysctl(name) != value:
write_sysctl(name, value)
def set(self):
self.write_table(self.new_table)
@@ -124,7 +125,6 @@ class NFTableManager(Singleton):
def cmd(self, *cmds):
code, out, err = self.raw_cmd(*cmds)
if code == 0: return out
else: raise Exception(err)

View File

@@ -1,13 +1,11 @@
import os, httpx
from typing import Callable
from fastapi import APIRouter, WebSocket
import asyncio
from fastapi import APIRouter
from starlette.responses import StreamingResponse
from fastapi.responses import FileResponse
from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func
from utils.models import ResetRequest
from fastapi.middleware.cors import CORSMiddleware
REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/"
REACT_HTML_PATH: str = os.path.join(REACT_BUILD_DIR,"index.html")
@@ -26,15 +24,6 @@ async def react_deploy(path):
return FileResponse(file_request)
def frontend_deploy(app):
if DEBUG:
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/{full_path:path}", include_in_schema=False)
async def catch_all(full_path:str):
if DEBUG:

View File

@@ -1,25 +1,27 @@
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
import { RuleMode } from "./utils";
import { RuleMode, Table } from "./utils";
import { table } from "console";
export const ModeSelector = (props:Omit<SegmentedControlProps, "data">) => (
<SegmentedControl
export const ModeSelector = (props:Omit<SegmentedControlProps, "data"> & { table: Table }) => {
const isFilterTable = props.table == Table.FILTER
return <SegmentedControl
data={[
{
value: RuleMode.IN,
label: 'IN',
label: isFilterTable?'IN':'PREROUTING',
},
{
...(isFilterTable?[{
value: RuleMode.FORWARD,
label: 'FWD',
},
}]:[]),
{
value: RuleMode.OUT,
label: 'OUT',
label: isFilterTable?'OUT':'POSTROUTING',
}
]}
size={props.size?props.size:"xs"}
{...props}
/>
)
}

View File

@@ -21,6 +21,11 @@ export enum RuleMode {
FORWARD = "forward"
}
export enum Table {
MANGLE = "mangle",
FILTER = "filter",
}
export type Rule = {
active: boolean
name:string,
@@ -33,6 +38,7 @@ export type Rule = {
port_dst_to: number,
action: ActionType,
mode: RuleMode,
table: Table
}
export type RuleInfo = {

View File

@@ -1,5 +1,5 @@
import { Combobox, TextInput, useCombobox } from "@mantine/core";
import { useState } from "react";
import { useEffect, useState } from "react";
import { ipInterfacesQuery } from "../js/utils";
interface ItemProps{
@@ -38,7 +38,7 @@ export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames,
},
});
const data = [...customIpInterfaces, ...interfaces]
const data = [...customIpInterfaces.filter(v => !interfaces.map(v => v.value).includes(v.value)), ...interfaces]
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const [search, setSearch] = useState('');
@@ -52,12 +52,17 @@ export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames,
return a.value.localeCompare(b.value)
});
const options = filteredOptions.map((item) => (
const options = filteredOptions.sort( (a, b) => a.value == selectedValue? -1 : b.value == selectedValue? 1: a.value.localeCompare(b.value) ).map((item) => (
<Combobox.Option value={item.value} key={item.value}>
( <b>{item.netint}</b> ) -{">"} <b>{item.value}</b>
( <b>{item.value == selectedValue ? "SELECTED" : item.netint}</b> ) -{">"} <b>{item.value}</b>
</Combobox.Option>
));
useEffect(() => {
setSelectedValue(data.find((item) => item.value === defaultValue)?.value??null)
setSearch(defaultValue??'')
}, [])
return <>
<Combobox
store={combobox}
@@ -81,9 +86,8 @@ export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames,
<Combobox.Target>
<TextInput
style={{width:"100%"}}
defaultValue={defaultValue}
rightSection={<Combobox.Chevron />}
value={value??(defaultValue?undefined:search)}
value={value??search}
placeholder="10.1.1.1"
rightSectionPointerEvents="none"
onChange={(event) => {
@@ -107,7 +111,7 @@ export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames,
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options mah={100} style={{ overflowY: 'auto' }}>
<Combobox.Options mah={200} style={{ overflowY: 'auto' }}>
{options}
{(exactOptionMatch==null) && search.trim().length > 0 && (
<Combobox.Option value="$create">+ Use this: {search}</Combobox.Option>

View File

@@ -17,7 +17,7 @@ export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
export const DEV_IP_BACKEND = "127.0.0.1:4444"
export const DEV_IP_BACKEND = "198.19.249.69:4444"
export const queryClient = new QueryClient({ defaultOptions: { queries: {
staleTime: Infinity

View File

@@ -1,8 +1,8 @@
import { ActionIcon, Badge, Box, Button, Divider, LoadingOverlay, Space, Switch, TextInput, Title, Tooltip, useMantineTheme } from "@mantine/core"
import { ActionIcon, Badge, Box, Divider, FloatingIndicator, LoadingOverlay, Space, Switch, Table, Tabs, TextInput, Title, Tooltip, useMantineTheme } from "@mantine/core"
import { useEffect, useState } from "react";
import { BsPlusLg, BsTrashFill } from "react-icons/bs"
import { rem } from '@mantine/core';
import { ActionType, Protocol, Rule, RuleMode, firewall, firewallRulesQuery } from "../../components/Firewall/utils";
import { ActionType, Protocol, Rule, RuleMode, Table as NFTables, firewall, firewallRulesQuery } from "../../components/Firewall/utils";
import { errorNotify, getErrorMessage, isMediumScreen, makeid, okNotify } from "../../js/utils";
import { useListState, useMediaQuery } from '@mantine/hooks';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
@@ -62,6 +62,8 @@ export const Firewall = () => {
const {rule_id, ...rest} = v
return rest
})) || rules.data?.policy != currentPolicy
const [selectedTab, setSelectedTab] = useState<NFTables>(NFTables.FILTER)
const enableFirewall = () => {
if (valuesChanged){
@@ -122,7 +124,8 @@ export const Firewall = () => {
port_src_to: 65535,
port_dst_to: 8080,
action: ActionType.ACCEPT,
mode: RuleMode.IN
mode: RuleMode.IN,
table: selectedTab
})
}
@@ -145,7 +148,7 @@ export const Firewall = () => {
const items = state.map((item, index) => (
<Draggable key={item.rule_id} index={index} draggableId={item.rule_id}>
item.table == selectedTab && <Draggable key={item.rule_id} index={index} draggableId={item.rule_id}>
{(provided, snapshot) => {
const customInt = [
{ value: "0.0.0.0/0", netint: "ANY IPv4", label: "0.0.0.0/0" },
@@ -313,6 +316,7 @@ export const Firewall = () => {
value={item.mode}
onChange={(value)=>{item.mode = value as RuleMode;updateMe()}}
style={{width:"100%"}}
table={item.table}
/>, !isMedium)}
<Space h="xs" />
{condDiv(<ProtocolSelector
@@ -335,7 +339,7 @@ export const Firewall = () => {
</Box>
}}
</Draggable>
));
)).filter(v => v);
return <>
@@ -389,6 +393,18 @@ export const Firewall = () => {
</Box>
<Space h="xl" />
<Divider />
<Space h="md"/>
<Tabs variant="pills" value={selectedTab} onChange={(v)=>setSelectedTab(v==NFTables.MANGLE?NFTables.MANGLE:NFTables.FILTER)} style={{ display:"flex", justifyContent:"center", alignItems:"center"}}>
<Box mr="md">Filtering Table:</Box>
<Tabs.List>
<Tabs.Tab value={NFTables.FILTER}>
Filter
</Tabs.Tab>
<Tabs.Tab value={NFTables.MANGLE}>
Mangle
</Tabs.Tab>
</Tabs.List>
</Tabs>
{items.length > 0?<DragDropContext
onDragEnd={({ destination, source }) =>
handlers.reorder({ from: source.index, to: destination?.index || 0 })