add: filtering table of firewall + InterfaceSelector frontend fixes and improves
This commit is contained in:
@@ -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'}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}}})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user