mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 15:30:59 +00:00
minor adjustments + network address based access control
This commit is contained in:
parent
4f62a7c7c9
commit
4125eff93f
@ -16,7 +16,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({
|
||||
@ -34,6 +34,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,
|
||||
@ -42,6 +87,7 @@ app = FastAPI(
|
||||
title="Firegex API",
|
||||
version=API_VERSION,
|
||||
)
|
||||
app.add_middleware(IPFilterMiddleware)
|
||||
|
||||
if DEBUG:
|
||||
app.add_middleware(
|
||||
@ -64,17 +110,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()
|
||||
@ -144,7 +196,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!")
|
||||
|
||||
@ -156,7 +208,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})}
|
||||
|
||||
@ -172,7 +224,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})}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,13 +530,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>(
|
||||
|
||||
@ -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 utils.models import ResetRequest, StatusMessageModel
|
||||
@ -373,9 +373,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."""
|
||||
@ -387,9 +391,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."""
|
||||
|
||||
@ -13,7 +13,7 @@ import { Firewall } from './pages/Firewall';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import NFProxy from './pages/NFProxy';
|
||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||
import { useAuthStore } from './js/store';
|
||||
import { useAuthStore, useSystemStore } from './js/store';
|
||||
|
||||
function App() {
|
||||
|
||||
@ -24,6 +24,7 @@ function App() {
|
||||
const [loadinBtn, setLoadingBtn] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const { access_token } = useAuthStore()
|
||||
const { setVersion } = useSystemStore()
|
||||
|
||||
useEffect(()=>{
|
||||
socketio.auth = { token: access_token || "" }
|
||||
@ -48,6 +49,7 @@ function App() {
|
||||
const getStatus = () =>{
|
||||
getstatus().then( res =>{
|
||||
setSystemStatus(res)
|
||||
setVersion(res.version || "unknown")
|
||||
setReqError(undefined)
|
||||
}).catch(err=>{
|
||||
setReqError(err.toString())
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -15,7 +15,8 @@ export type LoginResponse = {
|
||||
|
||||
export type ServerStatusResponse = {
|
||||
status:string,
|
||||
loggined:boolean
|
||||
loggined:boolean,
|
||||
version?:string
|
||||
}
|
||||
|
||||
export type PasswordSend = {
|
||||
|
||||
@ -73,3 +73,13 @@ export const useSession = () => {
|
||||
getHomeSection,
|
||||
};
|
||||
};
|
||||
|
||||
interface SystemState {
|
||||
version: string | null;
|
||||
setVersion: (version: string | null) => void;
|
||||
}
|
||||
|
||||
export const useSystemStore = create<SystemState>()((set) => ({
|
||||
version: null,
|
||||
setVersion: (version) => set({ version }),
|
||||
}));
|
||||
|
||||
29
run.py
29
run.py
@ -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)
|
||||
|
||||
@ -185,6 +191,8 @@ def gen_args(args_to_parse: list[str]|None = None):
|
||||
parser_config.add_argument('--port', "-p", type=int, required=False, help='Set default port for web service')
|
||||
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('--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)
|
||||
|
||||
@ -234,6 +242,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)
|
||||
@ -335,7 +349,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",
|
||||
@ -389,7 +405,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"
|
||||
@ -854,6 +872,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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user