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

@@ -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):
@@ -31,6 +32,10 @@ class Mode(str, Enum):
IN = "in",
OUT = "out",
FORWARD = "forward"
class Table(str, Enum):
FILTER = "filter"
MANGLE = "mangle"
class Action(str, Enum):
ACCEPT = "accept",
@@ -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
@@ -157,43 +171,50 @@ class FiregexTables(NFTableManager):
return rules
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}}},
super().__init__(self.init_comands(),[
{"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,10 +44,11 @@ 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: