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 import socketio
from socketio.exceptions import ConnectionRefusedError from socketio.exceptions import ConnectionRefusedError
import hashlib import hashlib
from ipaddress import ip_network, ip_address
# DB init # DB init
db = SQLite('db/firegex.db') db = SQLite('db/firegex.db')
sysctl = SysctlManager({ sysctl = SysctlManager({
@ -35,6 +35,51 @@ async def lifespan(app):
yield yield
await shutdown_main() 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( app = FastAPI(
debug=DEBUG, debug=DEBUG,
redoc_url=None, redoc_url=None,
@ -43,6 +88,7 @@ app = FastAPI(
title="Firegex API", title="Firegex API",
version=API_VERSION, version=API_VERSION,
) )
app.add_middleware(IPFilterMiddleware)
if DEBUG: if DEBUG:
app.add_middleware( 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 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")
def hash_psw(psw: str): def _hash_psw_sync(psw: str) -> str:
salt = secrets.token_hex(32) salt = secrets.token_hex(32)
return hashlib.pbkdf2_hmac("sha256", psw.encode(), salt.encode(), 500_000).hex()+"-"+salt 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("-") psw_hash, salt = hashed.split("-")
new_hashed = hashlib.pbkdf2_hmac("sha256", psw.encode(), salt.encode(), 500_000).hex() new_hashed = hashlib.pbkdf2_hmac("sha256", psw.encode(), salt.encode(), 500_000).hex()
return new_hashed == psw_hash return new_hashed == psw_hash
def set_psw(psw: str): async def verify_psw(psw: str, hashed: str) -> bool:
db.put("password", hash_psw(psw)) 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): def create_access_token(data: dict):
to_encode = data.copy() to_encode = data.copy()
@ -145,7 +197,7 @@ async def login_api(form: OAuth2PasswordRequestForm = Depends()):
if form.password == "": if form.password == "":
return {"status":"Cannot insert an empty password!"} return {"status":"Cannot insert an empty password!"}
await asyncio.sleep(0.3) # No bruteforce :) 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"} return {"access_token": create_access_token({"logged_in": True}), "token_type": "bearer"}
raise HTTPException(406,"Wrong password!") raise HTTPException(406,"Wrong password!")
@ -157,7 +209,7 @@ async def set_password(form: PasswordForm):
raise HTTPException(status_code=400) raise HTTPException(status_code=400)
if form.password == "": if form.password == "":
return {"status":"Cannot insert an empty password!"} return {"status":"Cannot insert an empty password!"}
set_psw(form.password) await set_psw(form.password)
await refresh_frontend() await refresh_frontend()
return {"status":"ok", "access_token": create_access_token({"logged_in": True})} 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)) db.put("secret", secrets.token_hex(32))
await disconnect_all() await disconnect_all()
set_psw(form.password) await set_psw(form.password)
await refresh_frontend() await refresh_frontend()
return {"status":"ok", "access_token": create_access_token({"logged_in": True})} return {"status":"ok", "access_token": create_access_token({"logged_in": True})}

View File

@ -27,7 +27,23 @@ public:
PktRequest<Derived>* pkt; PktRequest<Derived>* pkt;
for(;;) { for(;;) {
queue.take(pkt); 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; delete pkt;
} }
} }

View File

@ -531,13 +531,21 @@ class NfQueue {
return MNL_CB_ERROR; 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 //Get Payload
uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]); uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]);
char *payload = (char *)mnl_attr_get_payload(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 bool is_input = ntohl(mnl_attr_get_u32(attr[NFQA_MARK])) & 0x1; // == 0x1337 that is odd
handle_func(new PktRequest<D>( handle_func(new PktRequest<D>(

View File

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

View File

@ -9,13 +9,14 @@ import { ImExit } from 'react-icons/im';
import ResetPasswordModal from './ResetPasswordModal'; import ResetPasswordModal from './ResetPasswordModal';
import ResetModal from './ResetModal'; import ResetModal from './ResetModal';
import { MenuDropDownWithButton } from '../MainLayout'; import { MenuDropDownWithButton } from '../MainLayout';
import { useNavbarStore } from '../../js/store'; import { useNavbarStore, useSystemStore } from '../../js/store';
function HeaderPage(props: any) { function HeaderPage(props: any) {
const navigator = useNavigate() const navigator = useNavigate()
const { navOpened, toggleNav } = useNavbarStore() const { navOpened, toggleNav } = useNavbarStore()
const { version } = useSystemStore()
const logout_action = () => { const logout_action = () => {
logout().then(r => { logout().then(r => {
@ -48,7 +49,7 @@ function HeaderPage(props: any) {
</Box> </Box>
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'> <Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
<Title order={2} >[Fi]*regex</Title> <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>
</Box> </Box>

View File

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

29
run.py
View File

@ -103,7 +103,9 @@ def load_config():
"port": 4444, "port": 4444,
# any allow to bind service also on ipv6 (see the main of backend to understand why) # any allow to bind service also on ipv6 (see the main of backend to understand why)
"host": "any", "host": "any",
"socket_dir": None "socket_dir": None,
"allowed_ips": None,
"proxy_ip_header": None
} }
if os.path.isfile(g.configfile): 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('--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('--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('--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('--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('--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) 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('--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('--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('--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('--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) 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('--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('--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('--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) parser_config.add_argument('--show', required=False, action="store_true", help='Show current configuration', default=False)
args = parser.parse_args(args=args_to_parse) 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"]: if hasattr(args, 'socket_dir') and args.socket_dir != config["socket_dir"]:
config["socket_dir"] = args.socket_dir config["socket_dir"] = args.socket_dir
config_changed = True 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: if config_changed:
save_config(config) save_config(config)
@ -337,7 +351,9 @@ def write_compose(skip_password = True):
f"NTHREADS={args.threads}", f"NTHREADS={args.threads}",
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []), *([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []),
*(["SOCKET_DIR=/run/firegex"] if args.socket_dir 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": [ "volumes": [
"firegex_data:/execute/db", "firegex_data:/execute/db",
@ -391,7 +407,9 @@ def write_compose(skip_password = True):
f"PORT={args.port}", f"PORT={args.port}",
f"NTHREADS={args.threads}", f"NTHREADS={args.threads}",
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []), *([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": [ "volumes": [
"firegex_data:/execute/db" "firegex_data:/execute/db"
@ -856,6 +874,11 @@ def run_standalone():
f"NTHREADS={args.threads}", 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 # Add password if set
psw_set = get_password() psw_set = get_password()
if psw_set: if psw_set: