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

View File

@@ -1,6 +1,6 @@
import uvicorn, secrets, utils import uvicorn, secrets, utils
import os, asyncio import os, asyncio, logging
from fastapi import FastAPI, HTTPException, Depends, APIRouter from fastapi import FastAPI, HTTPException, Depends, APIRouter, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt from jose import jwt
from passlib.context import CryptContext 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="") utils.socketio = SocketManager(app, "/sock", socketio_path="")
if DEBUG: 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 APP_STATUS(): return "init" if db.get("password") is None else "run"
def JWT_SECRET(): return db.get("secret") def JWT_SECRET(): return db.get("secret")
@@ -132,7 +139,10 @@ async def startup_main():
db.init() db.init()
if os.getenv("HEX_SET_PSW"): if os.getenv("HEX_SET_PSW"):
set_psw(bytes.fromhex(os.getenv("HEX_SET_PSW")).decode()) 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() await startup()
if not JWT_SECRET(): db.put("secret", secrets.token_hex(32)) if not JWT_SECRET(): db.put("secret", secrets.token_hex(32))
await refresh_frontend() await refresh_frontend()
@@ -149,7 +159,10 @@ async def reset_firegex(form: ResetRequest):
db.delete() db.delete()
db.init() db.init()
db.put("secret", secrets.token_hex(32)) 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 reset(form)
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}

View File

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

View File

@@ -7,12 +7,16 @@ class FiregexTables(NFTableManager):
rules_chain_out = "firegex_firewall_rules_out" rules_chain_out = "firegex_firewall_rules_out"
rules_chain_fwd = "firegex_firewall_rules_fwd" rules_chain_fwd = "firegex_firewall_rules_fwd"
filter_table = "filter" filter_table = "filter"
mangle_table = "mangle"
def init_comands(self, policy:str=Action.ACCEPT, opt: FirewallSettings|None = None): def init_comands(self, policy:str=Action.ACCEPT, opt: FirewallSettings|None = None):
rules = [ rules = [
{"add":{"table":{"name":self.filter_table,"family":"ip"}}}, {"add":{"table":{"name":self.filter_table,"family":"ip"}}},
{"add":{"table":{"name":self.filter_table,"family":"ip6"}}}, {"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":"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":"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}}}, {"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":"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":"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":"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":"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":"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":"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":"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":"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 if opt is None: return rules
@@ -158,42 +172,49 @@ class FiregexTables(NFTableManager):
def __init__(self): def __init__(self):
super().__init__(self.init_comands(),[ 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_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_out}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip", "name":self.rules_chain_fwd}}}, {"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_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_out}}},
{"flush":{"chain":{"table":self.filter_table,"family":"ip6", "name":self.rules_chain_fwd}}}, {"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): def chain_to_firegex(self, chain:str, table:str):
match chain: if table == self.filter_table:
case "INPUT": return self.rules_chain_in match chain:
case "OUTPUT": return self.rules_chain_out case "INPUT": return self.rules_chain_in
case "FORWARD": return self.rules_chain_fwd 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 return None
def insert_firegex_chains(self): def insert_firegex_chains(self):
rules:list[dict] = list(self.list_rules(tables=[self.filter_table], chains=["INPUT", "OUTPUT", "FORWARD"])) rules:list[dict] = list(self.list_rules(tables=[self.filter_table, self.mangle_table], chains=["INPUT", "OUTPUT", "FORWARD", "PREROUTING", "POSTROUTING"]))
for family in ["ip", "ip6"]: for table in [self.filter_table, self.mangle_table]:
for chain in ["INPUT", "OUTPUT", "FORWARD"]: for family in ["ip", "ip6"]:
found = False for chain in ["INPUT", "OUTPUT", "FORWARD"] if table == self.filter_table else ["PREROUTING", "POSTROUTING"]:
rule_to_add = [{ "jump": { "target": self.chain_to_firegex(chain) }}] found = False
for r in rules: rule_to_add = [{ "jump": { "target": self.chain_to_firegex(chain, table) }}]
if r.get("family") == family and r.get("table") == "filter" and r.get("chain") == chain and r.get("expr") == rule_to_add: for r in rules:
found = True if r.get("family") == family and r.get("table") == table and r.get("chain") == chain and r.get("expr") == rule_to_add:
break found = True
if found: continue break
yield { "add":{ "rule": { if found: continue
"family": family, yield { "add":{ "rule": {
"table": self.filter_table, "family": family,
"chain": chain, "table": table,
"expr": rule_to_add "chain": chain,
}}} "expr": rule_to_add
}}}
def set(self, srvs:list[Rule], policy:str=Action.ACCEPT, opt:FirewallSettings = None): def set(self, srvs:list[Rule], policy:str=Action.ACCEPT, opt:FirewallSettings = None):
srvs = list(srvs) srvs = list(srvs)
@@ -209,7 +230,8 @@ class FiregexTables(NFTableManager):
port_src_to=65535, port_src_to=65535,
port_dst_to=65535, port_dst_to=65535,
action=Action.REJECT, 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) 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: for fam in families:
rules.append({ "add":{ "rule": { rules.append({ "add":{ "rule": {
"family": fam, "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, "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 "expr": ip_filters + port_filters + end_rules
}}}) }}})

View File

@@ -11,6 +11,7 @@ db = SQLite('db/firewall-rules.db', {
'rules': { 'rules': {
'rule_id': 'INT PRIMARY KEY CHECK (rule_id >= 0)', 'rule_id': 'INT PRIMARY KEY CHECK (rule_id >= 0)',
'mode': 'VARCHAR(10) NOT NULL CHECK (mode IN ("in", "out", "forward"))', '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', 'name': 'VARCHAR(100) NOT NULL',
'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1))', 'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1))',
'proto': 'VARCHAR(10) NOT NULL CHECK (proto IN ("tcp", "udp", "both", "any"))', '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""" """Get the list of existent firegex rules"""
return { return {
"policy": firewall.policy, "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 "enabled": firewall.enabled
} }
@@ -99,6 +100,9 @@ async def disable_firewall():
def parse_and_check_rule(rule:RuleModel): 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 is_src_ip = is_dst_ip = True
try: try:
@@ -137,14 +141,14 @@ async def add_new_service(form: RuleFormAdd):
src, dst, src, dst,
port_src_from, port_dst_from, port_src_from, port_dst_from,
port_src_to, port_dst_to, port_src_to, port_dst_to,
action, mode action, mode, `table`
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,?, ?)""", ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,?, ?, ?)""",
rid, ele.active, ele.name, rid, ele.active, ele.name,
ele.proto, ele.proto,
ele.src, ele.dst, ele.src, ele.dst,
ele.port_src_from, ele.port_dst_from, ele.port_src_from, ele.port_dst_from,
ele.port_src_to, ele.port_dst_to, ele.port_src_to, ele.port_dst_to,
ele.action, ele.mode ele.action, ele.mode, ele.table
) for rid, ele in enumerate(rules)] ) for rid, ele in enumerate(rules)]
) )
firewall.policy = form.policy.value 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__), '..')) ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers") ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
ON_DOCKER = len(sys.argv) > 1 and sys.argv[1] == "DOCKER" ON_DOCKER = "DOCKER" in sys.argv
DEBUG = len(sys.argv) > 1 and sys.argv[1] == "DEBUG" DEBUG = "DEBUG" in sys.argv
FIREGEX_PORT = int(os.getenv("PORT","4444")) FIREGEX_PORT = int(os.getenv("PORT","4444"))
JWT_ALGORITHM: str = "HS256" JWT_ALGORITHM: str = "HS256"
API_VERSION = "2.2.0" API_VERSION = "2.2.0"
@@ -44,9 +44,10 @@ class SysctlManager:
for name in ctl_table.keys(): for name in ctl_table.keys():
self.old_table[name] = read_sysctl(name) self.old_table[name] = read_sysctl(name)
def write_table(self, table): def write_table(self, table) -> bool:
for name, value in table.items(): for name, value in table.items():
write_sysctl(name, value) if read_sysctl(name) != value:
write_sysctl(name, value)
def set(self): def set(self):
self.write_table(self.new_table) self.write_table(self.new_table)
@@ -124,7 +125,6 @@ class NFTableManager(Singleton):
def cmd(self, *cmds): def cmd(self, *cmds):
code, out, err = self.raw_cmd(*cmds) code, out, err = self.raw_cmd(*cmds)
if code == 0: return out if code == 0: return out
else: raise Exception(err) else: raise Exception(err)

View File

@@ -1,13 +1,11 @@
import os, httpx import os, httpx
from typing import Callable from typing import Callable
from fastapi import APIRouter, WebSocket from fastapi import APIRouter
import asyncio
from starlette.responses import StreamingResponse from starlette.responses import StreamingResponse
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func
from utils.models import ResetRequest from utils.models import ResetRequest
from fastapi.middleware.cors import CORSMiddleware
REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/" REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/"
REACT_HTML_PATH: str = os.path.join(REACT_BUILD_DIR,"index.html") 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) return FileResponse(file_request)
def frontend_deploy(app): 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) @app.get("/{full_path:path}", include_in_schema=False)
async def catch_all(full_path:str): async def catch_all(full_path:str):
if DEBUG: if DEBUG:

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Combobox, TextInput, useCombobox } from "@mantine/core"; import { Combobox, TextInput, useCombobox } from "@mantine/core";
import { useState } from "react"; import { useEffect, useState } from "react";
import { ipInterfacesQuery } from "../js/utils"; import { ipInterfacesQuery } from "../js/utils";
interface ItemProps{ 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 [selectedValue, setSelectedValue] = useState<string | null>(null);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -52,12 +52,17 @@ export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames,
return a.value.localeCompare(b.value) 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}> <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> </Combobox.Option>
)); ));
useEffect(() => {
setSelectedValue(data.find((item) => item.value === defaultValue)?.value??null)
setSearch(defaultValue??'')
}, [])
return <> return <>
<Combobox <Combobox
store={combobox} store={combobox}
@@ -81,9 +86,8 @@ export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames,
<Combobox.Target> <Combobox.Target>
<TextInput <TextInput
style={{width:"100%"}} style={{width:"100%"}}
defaultValue={defaultValue}
rightSection={<Combobox.Chevron />} rightSection={<Combobox.Chevron />}
value={value??(defaultValue?undefined:search)} value={value??search}
placeholder="10.1.1.1" placeholder="10.1.1.1"
rightSectionPointerEvents="none" rightSectionPointerEvents="none"
onChange={(event) => { onChange={(event) => {
@@ -107,7 +111,7 @@ export const InterfaceInput = ({ initialCustomInterfaces, includeInterfaceNames,
</Combobox.Target> </Combobox.Target>
<Combobox.Dropdown> <Combobox.Dropdown>
<Combobox.Options mah={100} style={{ overflowY: 'auto' }}> <Combobox.Options mah={200} style={{ overflowY: 'auto' }}>
{options} {options}
{(exactOptionMatch==null) && search.trim().length > 0 && ( {(exactOptionMatch==null) && search.trim().length > 0 && (
<Combobox.Option value="$create">+ Use this: {search}</Combobox.Option> <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_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_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 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: { export const queryClient = new QueryClient({ defaultOptions: { queries: {
staleTime: Infinity 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 { useEffect, useState } from "react";
import { BsPlusLg, BsTrashFill } from "react-icons/bs" import { BsPlusLg, BsTrashFill } from "react-icons/bs"
import { rem } from '@mantine/core'; 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 { errorNotify, getErrorMessage, isMediumScreen, makeid, okNotify } from "../../js/utils";
import { useListState, useMediaQuery } from '@mantine/hooks'; import { useListState, useMediaQuery } from '@mantine/hooks';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
@@ -62,6 +62,8 @@ export const Firewall = () => {
const {rule_id, ...rest} = v const {rule_id, ...rest} = v
return rest return rest
})) || rules.data?.policy != currentPolicy })) || rules.data?.policy != currentPolicy
const [selectedTab, setSelectedTab] = useState<NFTables>(NFTables.FILTER)
const enableFirewall = () => { const enableFirewall = () => {
if (valuesChanged){ if (valuesChanged){
@@ -122,7 +124,8 @@ export const Firewall = () => {
port_src_to: 65535, port_src_to: 65535,
port_dst_to: 8080, port_dst_to: 8080,
action: ActionType.ACCEPT, action: ActionType.ACCEPT,
mode: RuleMode.IN mode: RuleMode.IN,
table: selectedTab
}) })
} }
@@ -145,7 +148,7 @@ export const Firewall = () => {
const items = state.map((item, index) => ( 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) => { {(provided, snapshot) => {
const customInt = [ const customInt = [
{ value: "0.0.0.0/0", netint: "ANY IPv4", label: "0.0.0.0/0" }, { 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} value={item.mode}
onChange={(value)=>{item.mode = value as RuleMode;updateMe()}} onChange={(value)=>{item.mode = value as RuleMode;updateMe()}}
style={{width:"100%"}} style={{width:"100%"}}
table={item.table}
/>, !isMedium)} />, !isMedium)}
<Space h="xs" /> <Space h="xs" />
{condDiv(<ProtocolSelector {condDiv(<ProtocolSelector
@@ -335,7 +339,7 @@ export const Firewall = () => {
</Box> </Box>
}} }}
</Draggable> </Draggable>
)); )).filter(v => v);
return <> return <>
@@ -389,6 +393,18 @@ export const Firewall = () => {
</Box> </Box>
<Space h="xl" /> <Space h="xl" />
<Divider /> <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 {items.length > 0?<DragDropContext
onDragEnd={({ destination, source }) => onDragEnd={({ destination, source }) =>
handlers.reorder({ from: source.index, to: destination?.index || 0 }) handlers.reorder({ from: source.index, to: destination?.index || 0 })