Merge branch 'dev' into dev-tmp

This commit is contained in:
Domingo Dirutigliano 2026-07-23 13:45:25 +02:00 committed by GitHub
commit d27831c9ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 136 additions and 27 deletions

View File

@ -17,7 +17,7 @@ from fastapi.middleware.cors import CORSMiddleware
import socketio
from socketio.exceptions import ConnectionRefusedError
import hashlib
from ipaddress import ip_network, ip_address
# DB init
db = SQLite('db/firegex.db')
sysctl = SysctlManager({
@ -35,6 +35,51 @@ async def lifespan(app):
yield
await shutdown_main()
ALLOWED_NETWORKS = [ip_network(ip.strip(), strict=False) for ip in os.getenv("ALLOWED_IPS", "").split(",") if ip.strip()]
PROXY_IP_HEADER = os.getenv("PROXY_IP_HEADER", "")
class IPFilterMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] in ["http", "websocket"]:
if ALLOWED_NETWORKS:
client_ip = None
if PROXY_IP_HEADER:
headers = dict(scope.get("headers", []))
header_val = headers.get(PROXY_IP_HEADER.lower().encode())
if header_val:
client_ip = header_val.decode().split(",")[0].strip()
if not client_ip and scope.get("client"):
client_ip = scope["client"][0]
if client_ip:
try:
ip_obj = ip_address(client_ip)
if not any(ip_obj in net for net in ALLOWED_NETWORKS):
if scope["type"] == "http":
await send({
"type": "http.response.start",
"status": 403,
"headers": [(b"content-type", b"text/plain")],
})
await send({
"type": "http.response.body",
"body": b"Forbidden",
})
elif scope["type"] == "websocket":
await send({
"type": "websocket.close",
"code": 1008
})
return
except ValueError:
pass
await self.app(scope, receive, send)
app = FastAPI(
debug=DEBUG,
redoc_url=None,
@ -43,6 +88,7 @@ app = FastAPI(
title="Firegex API",
version=API_VERSION,
)
app.add_middleware(IPFilterMiddleware)
if DEBUG:
app.add_middleware(
@ -65,17 +111,23 @@ app.mount("/sock", sio_app)
def APP_STATUS(): return "init" if db.get("password") is None else "run"
def JWT_SECRET(): return db.get("secret")
def hash_psw(psw: str):
def _hash_psw_sync(psw: str) -> str:
salt = secrets.token_hex(32)
return hashlib.pbkdf2_hmac("sha256", psw.encode(), salt.encode(), 500_000).hex()+"-"+salt
def verify_psw(psw: str, hashed: str) -> bool:
async def hash_psw(psw: str) -> str:
return await asyncio.to_thread(_hash_psw_sync, psw)
def _verify_psw_sync(psw: str, hashed: str) -> bool:
psw_hash, salt = hashed.split("-")
new_hashed = hashlib.pbkdf2_hmac("sha256", psw.encode(), salt.encode(), 500_000).hex()
return new_hashed == psw_hash
def set_psw(psw: str):
db.put("password", hash_psw(psw))
async def verify_psw(psw: str, hashed: str) -> bool:
return await asyncio.to_thread(_verify_psw_sync, psw, hashed)
async def set_psw(psw: str):
db.put("password", await hash_psw(psw))
def create_access_token(data: dict):
to_encode = data.copy()
@ -145,7 +197,7 @@ async def login_api(form: OAuth2PasswordRequestForm = Depends()):
if form.password == "":
return {"status":"Cannot insert an empty password!"}
await asyncio.sleep(0.3) # No bruteforce :)
if verify_psw(form.password, db.get("password")):
if await verify_psw(form.password, db.get("password")):
return {"access_token": create_access_token({"logged_in": True}), "token_type": "bearer"}
raise HTTPException(406,"Wrong password!")
@ -157,7 +209,7 @@ async def set_password(form: PasswordForm):
raise HTTPException(status_code=400)
if form.password == "":
return {"status":"Cannot insert an empty password!"}
set_psw(form.password)
await set_psw(form.password)
await refresh_frontend()
return {"status":"ok", "access_token": create_access_token({"logged_in": True})}
@ -173,7 +225,7 @@ async def change_password(form: PasswordChangeForm):
db.put("secret", secrets.token_hex(32))
await disconnect_all()
set_psw(form.password)
await set_psw(form.password)
await refresh_frontend()
return {"status":"ok", "access_token": create_access_token({"logged_in": True})}

View File

@ -27,7 +27,23 @@ public:
PktRequest<Derived>* pkt;
for(;;) {
queue.take(pkt);
static_cast<Derived*>(this)->handle_next_packet(pkt);
try {
static_cast<Derived*>(this)->handle_next_packet(pkt);
} catch (const std::exception& e) {
std::cerr << "[error] Exception handling packet: " << e.what() << std::endl;
if (pkt->get_action() == FilterAction::NOACTION) {
try {
pkt->accept();
} catch (...) {}
}
} catch (...) {
std::cerr << "[error] Unknown exception handling packet" << std::endl;
if (pkt->get_action() == FilterAction::NOACTION) {
try {
pkt->accept();
} catch (...) {}
}
}
delete pkt;
}
}

View File

@ -531,13 +531,21 @@ class NfQueue {
return MNL_CB_ERROR;
}
struct nfqnl_msg_packet_hdr *ph = (nfqnl_msg_packet_hdr*) mnl_attr_get_payload(attr[NFQA_PACKET_HDR]);
struct nfgenmsg *nfg = (nfgenmsg *)mnl_nlmsg_get_payload(nlh);
if (attr[NFQA_PAYLOAD] == nullptr) {
cerr << "[error] [NfQueue._real_queue_cb] payload not set" << endl;
char buf[MNL_SOCKET_BUFFER_SIZE];
struct nlmsghdr *nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(nfg->res_id));
nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT);
mnl_socket_sendto(info->nl, nlh_verdict, nlh_verdict->nlmsg_len);
return MNL_CB_OK;
}
//Get Payload
uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]);
char *payload = (char *)mnl_attr_get_payload(attr[NFQA_PAYLOAD]);
//Return result to the kernel
struct nfqnl_msg_packet_hdr *ph = (nfqnl_msg_packet_hdr*) mnl_attr_get_payload(attr[NFQA_PACKET_HDR]);
struct nfgenmsg *nfg = (nfgenmsg *)mnl_nlmsg_get_payload(nlh);
bool is_input = ntohl(mnl_attr_get_u32(attr[NFQA_MARK])) & 0x1; // == 0x1337 that is odd
handle_func(new PktRequest<D>(

View File

@ -3,7 +3,7 @@ import sqlite3
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from modules.nfproxy.nftables import FiregexTables
from modules.nfproxy.firewall import STATUS, FirewallManager
from modules.nfproxy.firewall import STATUS, FirewallManager, ServiceNotFoundException
from utils.sqlite import SQLite
from utils import ip_parse, refactor_name, socketio_emit, PortType
from modules.tls.service import activate_stream
@ -420,9 +420,13 @@ async def join_outstream(sid, data):
"""Client joins a room."""
srv = data.get("service")
if srv:
room = f"nfproxy-outstream-{srv}"
await utils.socketio.enter_room(sid, room)
await utils.socketio.emit(room, firewall.get(srv).read_outstrem_buffer(), room=sid)
try:
srv_manager = firewall.get(srv)
room = f"nfproxy-outstream-{srv}"
await utils.socketio.enter_room(sid, room)
await utils.socketio.emit(room, srv_manager.read_outstrem_buffer(), room=sid)
except ServiceNotFoundException:
pass
async def leave_outstream(sid, data):
"""Client leaves a room."""
@ -434,9 +438,13 @@ async def join_exception(sid, data):
"""Client joins a room."""
srv = data.get("service")
if srv:
room = f"nfproxy-exception-{srv}"
await utils.socketio.enter_room(sid, room)
await utils.socketio.emit(room, firewall.get(srv).last_exception_time, room=sid)
try:
srv_manager = firewall.get(srv)
room = f"nfproxy-exception-{srv}"
await utils.socketio.enter_room(sid, room)
await utils.socketio.emit(room, srv_manager.last_exception_time, room=sid)
except ServiceNotFoundException:
pass
async def leave_exception(sid, data):
"""Client leaves a room."""

View File

@ -9,13 +9,14 @@ import { ImExit } from 'react-icons/im';
import ResetPasswordModal from './ResetPasswordModal';
import ResetModal from './ResetModal';
import { MenuDropDownWithButton } from '../MainLayout';
import { useNavbarStore } from '../../js/store';
import { useNavbarStore, useSystemStore } from '../../js/store';
function HeaderPage(props: any) {
const navigator = useNavigate()
const { navOpened, toggleNav } = useNavbarStore()
const { version } = useSystemStore()
const logout_action = () => {
logout().then(r => {
@ -48,7 +49,7 @@ function HeaderPage(props: any) {
</Box>
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
<Title order={2} >[Fi]*regex</Title>
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a> {version && <span style={{marginLeft: "5px", color: "gray"}}>{version}</span>}</p>
</Box>
</Box>

View File

@ -5,16 +5,17 @@ import NavBar from './NavBar';
import HeaderPage from './Header';
import { getMainPath } from '../js/utils';
import { useLocation } from 'react-router';
import { useNavbarStore } from '../js/store';
import { useNavbarStore, useSessionStore } from '../js/store';
import { HiMenu } from "react-icons/hi";
function MainLayout({ children }:{ children:any }) {
const { navOpened } = useNavbarStore()
const { setHomeSection } = useSessionStore()
const location = useLocation()
useEffect(()=>{
if (location.pathname !== "/"){
sessionStorage.setItem('home_section', getMainPath())
setHomeSection(getMainPath())
}
},[location.pathname])
return <AppShell

29
run.py
View File

@ -103,7 +103,9 @@ def load_config():
"port": 4444,
# any allow to bind service also on ipv6 (see the main of backend to understand why)
"host": "any",
"socket_dir": None
"socket_dir": None,
"allowed_ips": None,
"proxy_ip_header": None
}
if os.path.isfile(g.configfile):
@ -156,6 +158,8 @@ def gen_args(args_to_parse: list[str]|None = None):
parser_start.add_argument('--port', "-p", type=int, required=False, help=f'Port where open the web service of the firewall (default from config: {config["port"]})', default=config["port"])
parser_start.add_argument('--host', required=False, help=f'Host IP address to bind the service to (default from config: {config["host"]})', default=config["host"])
parser_start.add_argument('--socket-dir', required=False, type=str, help=f'Listen on socket_dir/firegex.sock instead of TCP (default from config: {config["socket_dir"]})', default=config["socket_dir"])
parser_start.add_argument('--allowed-ips', required=False, type=str, help=f'Comma-separated list of CIDR addresses allowed to contact firegex (default from config: {config.get("allowed_ips")})', default=config.get("allowed_ips"))
parser_start.add_argument('--proxy-ip-header', required=False, type=str, help=f'Header name to read the client IP from (default from config: {config.get("proxy_ip_header")})', default=config.get("proxy_ip_header"))
parser_start.add_argument('--logs', required=False, action="store_true", help='Show firegex logs', default=False)
parser_start.add_argument('--version', '-v', required=False, type=str , help='Version of the firegex image to use', default=None)
parser_start.add_argument('--prebuilt', required=False, action="store_true", help='Use prebuilt docker image', default=False)
@ -170,6 +174,8 @@ def gen_args(args_to_parse: list[str]|None = None):
parser_restart.add_argument('--port', "-p", type=int, required=False, help=f'Port where open the web service of the firewall (default from config: {config["port"]})', default=config["port"])
parser_restart.add_argument('--host', required=False, help=f'Host IP address to bind the service to (default from config: {config["host"]})', default=config["host"])
parser_restart.add_argument('--socket-dir', required=False, type=str, help=f'Listen on socket_dir/firegex.sock instead of TCP (default from config: {config["socket_dir"]})', default=config["socket_dir"])
parser_restart.add_argument('--allowed-ips', required=False, type=str, help=f'Comma-separated list of CIDR addresses allowed to contact firegex (default from config: {config.get("allowed_ips")})', default=config.get("allowed_ips"))
parser_restart.add_argument('--proxy-ip-header', required=False, type=str, help=f'Header name to read the client IP from (default from config: {config.get("proxy_ip_header")})', default=config.get("proxy_ip_header"))
parser_restart.add_argument('--logs', required=False, action="store_true", help='Show firegex logs', default=False)
parser_restart.add_argument('--standalone', required=False, action="store_true", help='Force standalone mode', default=False)
@ -186,6 +192,8 @@ def gen_args(args_to_parse: list[str]|None = None):
parser_config.add_argument('--host', required=False, help='Set default host IP address to bind the service to')
parser_config.add_argument('--socket-dir', required=False, type=str, help=f'Listen on socket_dir/firegex.sock instead of TCP (default from config: {config["socket_dir"]})', default=config["socket_dir"])
parser_config.add_argument('--password', required=False, type=str, nargs='?', const='', help='Change the password of the firewall (omit the value to be prompted for it interactively)')
parser_config.add_argument('--allowed-ips', required=False, type=str, help=f'Comma-separated list of CIDR addresses allowed to contact firegex (default from config: {config.get("allowed_ips")})', default=config.get("allowed_ips"))
parser_config.add_argument('--proxy-ip-header', required=False, type=str, help=f'Header name to read the client IP from (default from config: {config.get("proxy_ip_header")})', default=config.get("proxy_ip_header"))
parser_config.add_argument('--show', required=False, action="store_true", help='Show current configuration', default=False)
args = parser.parse_args(args=args_to_parse)
@ -236,6 +244,12 @@ def gen_args(args_to_parse: list[str]|None = None):
if hasattr(args, 'socket_dir') and args.socket_dir != config["socket_dir"]:
config["socket_dir"] = args.socket_dir
config_changed = True
if hasattr(args, 'allowed_ips') and args.allowed_ips != config.get("allowed_ips"):
config["allowed_ips"] = args.allowed_ips
config_changed = True
if hasattr(args, 'proxy_ip_header') and args.proxy_ip_header != config.get("proxy_ip_header"):
config["proxy_ip_header"] = args.proxy_ip_header
config_changed = True
if config_changed:
save_config(config)
@ -337,7 +351,9 @@ def write_compose(skip_password = True):
f"NTHREADS={args.threads}",
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []),
*(["SOCKET_DIR=/run/firegex"] if args.socket_dir else []),
*([f"FIREGEX_VERSION={get_git_version()}"] if get_git_version() else [])
*([f"FIREGEX_VERSION={get_git_version()}"] if get_git_version() else []),
*([f"ALLOWED_IPS={args.allowed_ips}"] if getattr(args, 'allowed_ips', None) else []),
*([f"PROXY_IP_HEADER={args.proxy_ip_header}"] if getattr(args, 'proxy_ip_header', None) else [])
],
"volumes": [
"firegex_data:/execute/db",
@ -391,7 +407,9 @@ def write_compose(skip_password = True):
f"PORT={args.port}",
f"NTHREADS={args.threads}",
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []),
*([f"FIREGEX_VERSION={get_git_version()}"] if get_git_version() else [])
*([f"FIREGEX_VERSION={get_git_version()}"] if get_git_version() else []),
*([f"ALLOWED_IPS={args.allowed_ips}"] if getattr(args, 'allowed_ips', None) else []),
*([f"PROXY_IP_HEADER={args.proxy_ip_header}"] if getattr(args, 'proxy_ip_header', None) else [])
],
"volumes": [
"firegex_data:/execute/db"
@ -856,6 +874,11 @@ def run_standalone():
f"NTHREADS={args.threads}",
]
if getattr(args, 'allowed_ips', None):
env_vars.append(f"ALLOWED_IPS={args.allowed_ips}")
if getattr(args, 'proxy_ip_header', None):
env_vars.append(f"PROXY_IP_HEADER={args.proxy_ip_header}")
# Add password if set
psw_set = get_password()
if psw_set: