Merge pull request #20 from Pwnzer0tt1/dev-nfproxy

New feature: nfproxy
This commit is contained in:
Domingo Dirutigliano
2025-03-07 22:24:51 +01:00
committed by GitHub
89 changed files with 4836 additions and 776 deletions

9
.gitignore vendored
View File

@@ -11,10 +11,11 @@
# testing # testing
/frontend/coverage /frontend/coverage
/proxy-client/firegex.egg-info /fgex-lib/firegex.egg-info
/proxy-client/dist /fgex-lib/dist
/proxy-client/fgex-pip/fgex.egg-info /fgex-lib/build
/proxy-client/fgex-pip/dist /fgex-lib/fgex-pip/fgex.egg-info
/fgex-lib/fgex-pip/dist
/backend/db/ /backend/db/
/backend/db/** /backend/db/**
/frontend/build/ /frontend/build/

View File

@@ -12,22 +12,23 @@ RUN bun i
COPY ./frontend/ . COPY ./frontend/ .
RUN bun run build RUN bun run build
#Building main conteiner #Building main conteiner
FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest
RUN dnf -y update && dnf install -y python3.13-devel python3-pip @development-tools gcc-c++ \ RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils nftables \ libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils nftables \
vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel uv
RUN mkdir -p /execute/modules RUN mkdir -p /execute/modules
WORKDIR /execute WORKDIR /execute
ADD ./backend/requirements.txt /execute/requirements.txt ADD ./backend/requirements.txt /execute/requirements.txt
RUN pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt --no-warn-script-location RUN uv pip install --no-cache --system -r /execute/requirements.txt
COPY ./fgex-lib /execute/fgex-lib
RUN uv pip install --no-cache --system ./fgex-lib
COPY ./backend/binsrc /execute/binsrc COPY ./backend/binsrc /execute/binsrc
RUN g++ binsrc/nfregex.cpp -o modules/cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) RUN g++ binsrc/nfregex.cpp -o modules/cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
#RUN g++ binsrc/nfproxy.cpp -o modules/cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.13 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3) RUN g++ binsrc/nfproxy.cpp -o modules/cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.13 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
COPY ./backend/ /execute/ COPY ./backend/ /execute/
COPY --from=frontend /app/dist/ ./frontend/ COPY --from=frontend /app/dist/ ./frontend/

View File

@@ -9,12 +9,13 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt from jose import jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager, NORELOAD
from utils.loader import frontend_deploy, load_routers from utils.loader import frontend_deploy, load_routers
from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import socketio import socketio
from socketio.exceptions import ConnectionRefusedError
# DB init # DB init
db = SQLite('db/firegex.db') db = SQLite('db/firegex.db')
@@ -52,7 +53,6 @@ if DEBUG:
allow_headers=["*"], allow_headers=["*"],
) )
utils.socketio = socketio.AsyncServer( utils.socketio = socketio.AsyncServer(
async_mode="asgi", async_mode="asgi",
cors_allowed_origins=[], cors_allowed_origins=[],
@@ -69,9 +69,6 @@ def set_psw(psw: str):
hash_psw = crypto.hash(psw) hash_psw = crypto.hash(psw)
db.put("password",hash_psw) db.put("password",hash_psw)
@utils.socketio.on("update")
async def updater(): pass
def create_access_token(data: dict): def create_access_token(data: dict):
to_encode = data.copy() to_encode = data.copy()
encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=JWT_ALGORITHM) encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=JWT_ALGORITHM)
@@ -90,6 +87,28 @@ async def check_login(token: str = Depends(oauth2_scheme)):
return False return False
return logged_in return logged_in
@utils.socketio.on("connect")
async def sio_connect(sid, environ, auth):
if not auth or not await check_login(auth.get("token")):
raise ConnectionRefusedError("Unauthorized")
utils.sid_list.add(sid)
@utils.socketio.on("disconnect")
async def sio_disconnect(sid):
try:
utils.sid_list.remove(sid)
except KeyError:
pass
async def disconnect_all():
while True:
if len(utils.sid_list) == 0:
break
await utils.socketio.disconnect(utils.sid_list.pop())
@utils.socketio.on("update")
async def updater(): pass
async def is_loggined(auth: bool = Depends(check_login)): async def is_loggined(auth: bool = Depends(check_login)):
if not auth: if not auth:
raise HTTPException( raise HTTPException(
@@ -122,6 +141,7 @@ async def login_api(form: OAuth2PasswordRequestForm = Depends()):
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!")
@app.post('/api/set-password', response_model=ChangePasswordModel) @app.post('/api/set-password', response_model=ChangePasswordModel)
async def set_password(form: PasswordForm): async def set_password(form: PasswordForm):
"""Set the password of firegex""" """Set the password of firegex"""
@@ -143,6 +163,7 @@ async def change_password(form: PasswordChangeForm):
return {"status":"Cannot insert an empty password!"} return {"status":"Cannot insert an empty password!"}
if form.expire: if form.expire:
db.put("secret", secrets.token_hex(32)) db.put("secret", secrets.token_hex(32))
await disconnect_all()
set_psw(form.password) set_psw(form.password)
await refresh_frontend() await refresh_frontend()
@@ -198,9 +219,9 @@ if __name__ == '__main__':
os.chdir(os.path.dirname(os.path.realpath(__file__))) os.chdir(os.path.dirname(os.path.realpath(__file__)))
uvicorn.run( uvicorn.run(
"app:app", "app:app",
host="::" if DEBUG else None, host="0.0.0.0" if DEBUG else None,
port=FIREGEX_PORT, port=FIREGEX_PORT,
reload=DEBUG, reload=DEBUG and not NORELOAD,
access_log=True, access_log=True,
workers=1, # Firewall module can't be replicated in multiple workers workers=1, # Firewall module can't be replicated in multiple workers
# Later the firewall module will be moved to a separate process # Later the firewall module will be moved to a separate process

View File

@@ -7,6 +7,7 @@
#include <tins/tcp_ip/stream_identifier.h> #include <tins/tcp_ip/stream_identifier.h>
#include <libmnl/libmnl.h> #include <libmnl/libmnl.h>
#include <tins/tins.h> #include <tins/tins.h>
#include <map>
using namespace std; using namespace std;
@@ -17,6 +18,17 @@ enum class FilterAction{ DROP, ACCEPT, MANGLE, NOACTION };
enum class L4Proto { TCP, UDP, RAW }; enum class L4Proto { TCP, UDP, RAW };
typedef Tins::TCPIP::StreamIdentifier stream_id; typedef Tins::TCPIP::StreamIdentifier stream_id;
struct tcp_ack_seq_ctx{
int64_t in = 0;
int64_t out = 0;
tcp_ack_seq_ctx(){}
void reset(){
in = 0;
out = 0;
}
};
typedef map<stream_id, tcp_ack_seq_ctx*> tcp_ack_map;
template<typename T> template<typename T>
class PktRequest { class PktRequest {
@@ -25,6 +37,10 @@ class PktRequest {
mnl_socket* nl = nullptr; mnl_socket* nl = nullptr;
uint16_t res_id; uint16_t res_id;
uint32_t packet_id; uint32_t packet_id;
size_t _original_size;
size_t _data_original_size;
size_t _header_size;
bool need_tcp_fixing = false;
public: public:
bool is_ipv6; bool is_ipv6;
Tins::IP* ipv4 = nullptr; Tins::IP* ipv4 = nullptr;
@@ -35,21 +51,28 @@ class PktRequest {
bool is_input; bool is_input;
string packet; string packet;
char* data;
size_t data_size;
stream_id sid; stream_id sid;
T* ctx; tcp_ack_seq_ctx* ack_seq_offset = nullptr;
T* ctx = nullptr;
private: private:
inline void fetch_data_size(Tins::PDU* pdu){ static inline size_t inner_data_size(Tins::PDU* pdu){
if (pdu == nullptr){
return 0;
}
auto inner = pdu->inner_pdu(); auto inner = pdu->inner_pdu();
if (inner == nullptr){ if (inner == nullptr){
data_size = 0; return 0;
}else{
data_size = inner->size();
} }
return inner->size();
}
inline void __internal_fetch_data_size(Tins::PDU* pdu){
_data_original_size = inner_data_size(pdu);
_header_size = _original_size - _data_original_size;
} }
L4Proto fill_l4_info(){ L4Proto fill_l4_info(){
@@ -58,14 +81,14 @@ class PktRequest {
if (tcp == nullptr){ if (tcp == nullptr){
udp = ipv6->find_pdu<Tins::UDP>(); udp = ipv6->find_pdu<Tins::UDP>();
if (udp == nullptr){ if (udp == nullptr){
fetch_data_size(ipv6); __internal_fetch_data_size(ipv6);
return L4Proto::RAW; return L4Proto::RAW;
}else{ }else{
fetch_data_size(udp); __internal_fetch_data_size(udp);
return L4Proto::UDP; return L4Proto::UDP;
} }
}else{ }else{
fetch_data_size(tcp); __internal_fetch_data_size(tcp);
return L4Proto::TCP; return L4Proto::TCP;
} }
}else{ }else{
@@ -73,36 +96,164 @@ class PktRequest {
if (tcp == nullptr){ if (tcp == nullptr){
udp = ipv4->find_pdu<Tins::UDP>(); udp = ipv4->find_pdu<Tins::UDP>();
if (udp == nullptr){ if (udp == nullptr){
fetch_data_size(ipv4); __internal_fetch_data_size(ipv4);
return L4Proto::RAW; return L4Proto::RAW;
}else{ }else{
fetch_data_size(udp); __internal_fetch_data_size(udp);
return L4Proto::UDP; return L4Proto::UDP;
} }
}else{ }else{
fetch_data_size(tcp); __internal_fetch_data_size(tcp);
return L4Proto::TCP; return L4Proto::TCP;
} }
} }
} }
bool need_tcp_fix(){
return tcp && ack_seq_offset != nullptr && (ack_seq_offset->in != 0 || ack_seq_offset->out != 0);
}
public: public:
PktRequest(const char* payload, size_t plen, T* ctx, mnl_socket* nl, nfgenmsg *nfg, nfqnl_msg_packet_hdr *ph, bool is_input): PktRequest(const char* payload, size_t plen, T* ctx, mnl_socket* nl, nfgenmsg *nfg, nfqnl_msg_packet_hdr *ph, bool is_input):
ctx(ctx), nl(nl), res_id(nfg->res_id), ctx(ctx), nl(nl), res_id(nfg->res_id),
packet_id(ph->packet_id), is_input(is_input), packet_id(ph->packet_id), is_input(is_input),
packet(string(payload, plen)), packet(string(payload, plen)),
is_ipv6((payload[0] & 0xf0) == 0x60){ action(FilterAction::NOACTION),
if (is_ipv6){ is_ipv6((payload[0] & 0xf0) == 0x60)
ipv6 = new Tins::IPv6((uint8_t*)packet.c_str(), plen); {
sid = stream_id::make_identifier(*ipv6); if (is_ipv6){
}else{ ipv6 = new Tins::IPv6((uint8_t*)packet.c_str(), plen);
ipv4 = new Tins::IP((uint8_t*)packet.c_str(), plen); sid = stream_id::make_identifier(*ipv6);
sid = stream_id::make_identifier(*ipv4); _original_size = ipv6->size();
} }else{
l4_proto = fill_l4_info(); ipv4 = new Tins::IP((uint8_t*)packet.c_str(), plen);
data = packet.data()+(plen-data_size); sid = stream_id::make_identifier(*ipv4);
_original_size = ipv4->size();
} }
l4_proto = fill_l4_info();
#ifdef DEBUG
if (tcp){
cerr << "[DEBUG] NEW_PACKET " << (is_input?"-> IN ":"<- OUT") << " [SIZE: " << data_size() << "] FLAGS: " << (tcp->get_flag(Tins::TCP::FIN)?"FIN ":"") << (tcp->get_flag(Tins::TCP::SYN)?"SYN ":"") << (tcp->get_flag(Tins::TCP::RST)?"RST ":"") << (tcp->get_flag(Tins::TCP::ACK)?"ACK ":"") << (tcp->get_flag(Tins::TCP::PSH)?"PSH ":"") << endl;
cerr << "[SEQ: " << tcp->seq() << "] [ACK: " << tcp->ack_seq() << "]" << " [WIN: " << tcp->window() << "] [FLAGS: " << tcp->flags() << "]\n" << endl;
}
#endif
}
inline size_t header_size(){
return _header_size;
}
char* data(){
return packet.data()+_header_size;
}
size_t data_size(){
return packet.size()-_header_size;
}
size_t data_original_size(){
return _data_original_size;
}
void reserialize(){
auto data = serialize();
packet.resize(data.size());
memcpy(packet.data(), data.data(), data.size());
}
void set_data(const char* data, const size_t& data_size){
auto bef_raw = before_raw_pdu_ptr();
if (bef_raw){
delete before_raw_pdu_ptr()->release_inner_pdu();
if (data_size > 0){
before_raw_pdu_ptr() /= move(Tins::RawPDU((uint8_t*)data, data_size));
}
}
}
Tins::PDU* before_raw_pdu_ptr(){
if (tcp){
return tcp;
}else if (udp){
return udp;
}else if (ipv4){
return ipv4;
}else if (ipv6){
return ipv6;
}
return nullptr;
}
void set_packet(const char* data, size_t data_size){
// Parsing only the header with libtins
Tins::PDU *data_pdu = nullptr;
size_t total_size;
if (is_ipv6){
delete ipv6;
ipv6 = new Tins::IPv6((uint8_t*)data, data_size);
if (tcp){
tcp = ipv6->find_pdu<Tins::TCP>();
data_pdu = tcp;
}else if (udp){
udp = ipv6->find_pdu<Tins::UDP>();
data_pdu = udp;
}else{
data_pdu = ipv6;
}
total_size = ipv6->size();
}else{
delete ipv4;
ipv4 = new Tins::IP((uint8_t*)data, data_size);
if (tcp){
tcp = ipv4->find_pdu<Tins::TCP>();
data_pdu = tcp;
}else if(udp){
udp = ipv4->find_pdu<Tins::UDP>();
data_pdu = udp;
}else{
data_pdu = ipv4;
}
total_size = ipv4->size();
}
_header_size = total_size - inner_data_size(data_pdu);
// Libtins can skip data if the lenght is changed to a bigger len (due to ip header total lenght), so we need to specify the data section manually
set_data(data+_header_size, data_size-_header_size);
}
void fix_tcp_ack(){
need_tcp_fixing = need_tcp_fix();
if(!need_tcp_fixing){
return;
}
#ifdef DEBUG
cerr << "[DEBUG] Fixing ack_seq with offsets " << ((int32_t)ack_seq_offset->in) << " " << ((int32_t)ack_seq_offset->out) << endl;
#endif
if (is_input){
tcp->seq(tcp->seq() + ack_seq_offset->in);
tcp->ack_seq(tcp->ack_seq() - ack_seq_offset->out);
}else{
tcp->ack_seq(tcp->ack_seq() - ack_seq_offset->in);
tcp->seq(tcp->seq() + ack_seq_offset->out);
}
#ifdef DEBUG
size_t new_size = inner_data_size(tcp);
cerr << "[DEBUG] FIXED PKT " << (is_input?"-> IN ":"<- OUT") << " [SIZE: " << data_size() << "] FLAGS: " << (tcp->get_flag(Tins::TCP::FIN)?"FIN ":"") << (tcp->get_flag(Tins::TCP::SYN)?"SYN ":"") << (tcp->get_flag(Tins::TCP::RST)?"RST ":"") << (tcp->get_flag(Tins::TCP::ACK)?"ACK ":"") << (tcp->get_flag(Tins::TCP::PSH)?"PSH ":"") << endl;
cerr << "[SEQ: " << tcp->seq() << "] [ACK: " << tcp->ack_seq() << "]" << " [WIN: " << tcp->window() << "] [FLAGS: " << tcp->flags() << "]\n" << endl;
#endif
}
void fix_data_payload(){
//Stream follower move the payload data, so we need to reinizialize RawPDU
auto bef_raw = before_raw_pdu_ptr();
if (bef_raw){
delete bef_raw->release_inner_pdu();
auto new_data_size = packet.size()-_header_size;
if (new_data_size > 0){
bef_raw /= move(Tins::RawPDU((uint8_t*)packet.data()+_header_size, new_data_size));
}
}
}
void drop(){ void drop(){
if (action == FilterAction::NOACTION){ if (action == FilterAction::NOACTION){
@@ -113,6 +264,10 @@ class PktRequest {
} }
} }
size_t original_size(){
return _original_size;
}
void accept(){ void accept(){
if (action == FilterAction::NOACTION){ if (action == FilterAction::NOACTION){
action = FilterAction::ACCEPT; action = FilterAction::ACCEPT;
@@ -131,6 +286,40 @@ class PktRequest {
} }
} }
void reject(){
if (tcp){
//If the packet has data, we have to remove it
set_data(nullptr, 0);
//For the first matched data or only for data packets, we set FIN bit
//This only for client packets, because this will trigger server to close the connection
//Packets will be filtered anyway also if client don't send packets
if (_data_original_size != 0){
tcp->set_flag(Tins::TCP::FIN,1);
tcp->set_flag(Tins::TCP::ACK,1);
tcp->set_flag(Tins::TCP::SYN,0);
}
//Send the edited packet to the kernel
mangle();
}else{
drop();
}
}
void mangle_custom_pkt(const char* raw_pkt, size_t raw_pkt_size){
if (action == FilterAction::NOACTION){
try{
set_packet(raw_pkt, raw_pkt_size);
reserialize();
action = FilterAction::MANGLE;
}catch(...){
action = FilterAction::DROP;
}
perfrom_action(false);
}else{
throw invalid_argument("Cannot mangle a packet that has already been accepted or dropped");
}
}
FilterAction get_action(){ FilterAction get_action(){
return action; return action;
} }
@@ -140,23 +329,51 @@ class PktRequest {
delete ipv6; delete ipv6;
} }
Tins::PDU::serialization_type serialize(){
if (is_ipv6){
return ipv6->serialize();
}else{
return ipv4->serialize();
}
}
private: private:
void perfrom_action(){ void perfrom_action(bool do_serialize = true){
char buf[MNL_SOCKET_BUFFER_SIZE]; char buf[MNL_SOCKET_BUFFER_SIZE];
struct nlmsghdr *nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(res_id)); struct nlmsghdr *nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(res_id));
switch (action) switch (action)
{ {
case FilterAction::ACCEPT: case FilterAction::ACCEPT:
if (need_tcp_fixing){
if (do_serialize){
fix_data_payload();
reserialize();
}
nfq_nlmsg_verdict_put_pkt(nlh_verdict, packet.data(), packet.size());
}
nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_ACCEPT ); nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_ACCEPT );
break; break;
case FilterAction::DROP: case FilterAction::DROP:
nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_DROP ); nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_DROP );
break; break;
case FilterAction::MANGLE:{ case FilterAction::MANGLE:{
if (is_ipv6){ //If not custom data, use the data in the packets
nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv6->serialize().data(), ipv6->size()); if(do_serialize){
}else{ reserialize();
nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv4->serialize().data(), ipv4->size()); }
nfq_nlmsg_verdict_put_pkt(nlh_verdict, packet.data(), packet.size());
#ifdef DEBUG
if (tcp){
cerr << "[DEBUG] MANGLEDPKT " << (is_input?"-> IN ":"<- OUT") << " [SIZE: " << data_size() << "] FLAGS: " << (tcp->get_flag(Tins::TCP::FIN)?"FIN ":"") << (tcp->get_flag(Tins::TCP::SYN)?"SYN ":"") << (tcp->get_flag(Tins::TCP::RST)?"RST ":"") << (tcp->get_flag(Tins::TCP::ACK)?"ACK ":"") << (tcp->get_flag(Tins::TCP::PSH)?"PSH ":"") << endl;
cerr << "[SEQ: " << tcp->seq() << "] [ACK: " << tcp->ack_seq() << "]" << " [WIN: " << tcp->window() << "] [FLAGS: " << tcp->flags() << "]\n" << endl;
}
#endif
if (tcp && ack_seq_offset && packet.size() != _original_size){
if (is_input){
ack_seq_offset->in += data_size() - _data_original_size;
}else{
ack_seq_offset->out += data_size() - _data_original_size;
}
} }
nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_ACCEPT ); nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_ACCEPT );
break; break;

View File

@@ -1,67 +1,134 @@
#define PY_SSIZE_T_CLEAN #define PY_SSIZE_T_CLEAN
#include <Python.h> #include <Python.h>
#include "proxytun/settings.cpp" #include "pyproxy/settings.cpp"
#include "proxytun/proxytun.cpp" #include "pyproxy/pyproxy.cpp"
#include "classes/netfilter.cpp" #include "classes/netfilter.cpp"
#include <syncstream>
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <cstdlib> #include <cstdlib>
#include <endian.h>
#include "utils.cpp"
using namespace std; using namespace std;
using namespace Firegex::PyProxy; using namespace Firegex::PyProxy;
using Firegex::NfQueue::MultiThreadQueue; using Firegex::NfQueue::MultiThreadQueue;
ssize_t read_check(int __fd, void *__buf, size_t __nbytes){ /*
ssize_t bytes = read(__fd, __buf, __nbytes);
if (bytes == 0){ How python code is handles:
cerr << "[fatal] [updater] read() returned EOF" << endl;
throw invalid_argument("read() returned EOF"); User code example:
} ```python
if (bytes < 0){
cerr << "[fatal] [updater] read() returned an error" << bytes << endl; from firegex.nfproxy import DROP, ACCEPT, pyfilter
throw invalid_argument("read() returned an error");
} @pyfilter
return bytes; def invalid_curl_agent(http):
if "curl" in http.headers.get("User-Agent", ""):
return DROP
return ACCEPT
```
The code is now edited adding an intestation and a end statement:
```python
<user_code>
__firegex_pyfilter_enabled = ["invalid_curl_agent", "func3"] # This list is dynamically generated by firegex backend
__firegex_proto = "http"
import firegex.nfproxy.internals
firegex.nfproxy.internals.compile(globals(), locals()) # This function can save other global variables, to use by the packet handler and is used generally to check and optimize the code
````
(First lines are the same to keep line of code consistent on exceptions messages)
This code will be executed only once, and is needed to build the global and local context to use
The globals and locals generated here are copied for each connection, and are used to handle the packets
Using C API will be injected in global context the following informations:
__firegex_packet_info = {
"data" = b"raw data found on L4",
"raw_packet" = b"raw packet",
"is_input" = True, # If the packet is incoming from a client
"is_ipv6" = False, # If the packet is ipv6
"is_tcp" = True, # If the packet is tcp
} }
As result the packet handler is responsible to return a dictionary in the global context with the following dictionary:
__firegex_pyfilter_result = {
"action": REJECT, # One of PyFilterResponse
"matched_by": "invalid_curl_agent", # The function that matched the packet (used if action = DROP or REJECT or MANGLE)
"mangled_packet": b"new packet" # The new packet to send to the kernel (used if action = MANGLE)
}
PyFilterResponse {
ACCEPT = 0,
DROP = 1,
REJECT = 2,
MANGLE = 3,
EXCEPTION = 4,
INVALID = 5
};
Every time a packet is received, the packet handler will execute the following code:
```python
firegex.nfproxy.internals.handle_packet(globals())
````
The TCP stream is sorted by libtins using c++ code, but the c++ code is not responsabile di buffer the stream, but only to sort those
So firegex handle_packet has to implement a way to limit memory usage, this dipends on what methods you choose to use to filter packets
firegex lib will give you all the needed possibilities to do this is many ways
Final note: is not raccomanded to use variables that starts with __firegex_ in your code, because they may break the nfproxy
*/
void config_updater (){ void config_updater (){
while (true){ while (true){
PyThreadState* state = PyEval_SaveThread(); // Release GIL while doing IO operation
uint32_t code_size; uint32_t code_size;
read_check(STDIN_FILENO, &code_size, 4); memcpy(&code_size, control_socket.recv(4).c_str(), 4);
vector<uint8_t> code(code_size); code_size = be32toh(code_size);
read_check(STDIN_FILENO, code.data(), code_size); string code = control_socket.recv(code_size);
#ifdef DEBUG
cerr << "[DEBUG] [updater] Received code: " << code << endl;
#endif
cerr << "[info] [updater] Updating configuration" << endl; cerr << "[info] [updater] Updating configuration" << endl;
PyEval_AcquireThread(state); //Restore GIL before executing python code
try{ try{
config.reset(new PyCodeConfig(code)); config.reset(new PyCodeConfig(code));
cerr << "[info] [updater] Config update done" << endl; cerr << "[info] [updater] Config update done" << endl;
osyncstream(cout) << "ACK OK" << endl; control_socket << "ACK OK" << endl;
}catch(const std::exception& e){ }catch(const std::exception& e){
cerr << "[error] [updater] Failed to build new configuration!" << endl; cerr << "[error] [updater] Failed to build new configuration!" << endl;
osyncstream(cout) << "ACK FAIL " << e.what() << endl; control_socket << "ACK FAIL " << e.what() << endl;
} }
} }
} }
int main(int argc, char *argv[]){
int main(int argc, char *argv[]) {
// Connect to the python backend using the unix socket
init_control_socket();
// Initialize the python interpreter
Py_Initialize(); Py_Initialize();
atexit(Py_Finalize); atexit(Py_Finalize);
init_handle_packet_code(); //Compile the static code used to handle packets
if (freopen(nullptr, "rb", stdin) == nullptr){ // We need to read from stdin binary data
cerr << "[fatal] [main] Failed to reopen stdin in binary mode" << endl;
return 1;
}
int n_of_threads = 1; int n_of_threads = 1;
char * n_threads_str = getenv("NTHREADS"); char * n_threads_str = getenv("NTHREADS");
if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str); if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str);
if(n_of_threads <= 0) n_of_threads = 1; if(n_of_threads <= 0) n_of_threads = 1;
config.reset(new PyCodeConfig()); config.reset(new PyCodeConfig());
MultiThreadQueue<PyProxyQueue> queue(n_of_threads); MultiThreadQueue<PyProxyQueue> queue(n_of_threads);
osyncstream(cout) << "QUEUE " << queue.queue_num() << endl; control_socket << "QUEUE " << queue.queue_num() << endl;
cerr << "[info] [main] Queue: " << queue.queue_num() << " threads assigned: " << n_of_threads << endl; cerr << "[info] [main] Queue: " << queue.queue_num() << " threads assigned: " << n_of_threads << endl;
thread qthr([&](){ thread qthr([&](){

View File

@@ -1,165 +0,0 @@
#ifndef PROXY_TUNNEL_CLASS_CPP
#define PROXY_TUNNEL_CLASS_CPP
#include <linux/netfilter/nfnetlink_queue.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <linux/netfilter/nfnetlink_conntrack.h>
#include <tins/tins.h>
#include <tins/tcp_ip/stream_follower.h>
#include <tins/tcp_ip/stream_identifier.h>
#include <libmnl/libmnl.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/types.h>
#include <stdexcept>
#include <thread>
#include <syncstream>
#include <iostream>
#include "../classes/netfilter.cpp"
#include "stream_ctx.cpp"
#include "settings.cpp"
using Tins::TCPIP::Stream;
using Tins::TCPIP::StreamFollower;
using namespace std;
namespace Firegex {
namespace PyProxy {
class PyProxyQueue: public NfQueue::ThreadNfQueue<PyProxyQueue> {
public:
stream_ctx sctx;
StreamFollower follower;
struct {
bool matching_has_been_called = false;
bool already_closed = false;
bool result;
NfQueue::PktRequest<PyProxyQueue>* pkt;
} match_ctx;
void before_loop() override {
follower.new_stream_callback(bind(on_new_stream, placeholders::_1, this));
follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, this));
}
bool filter_action(NfQueue::PktRequest<PyProxyQueue>* pkt){
shared_ptr<PyCodeConfig> conf = config;
auto stream_search = sctx.streams_ctx.find(pkt->sid);
pyfilter_ctx* stream_match;
if (stream_search == sctx.streams_ctx.end()){
// TODO: New pyfilter_ctx
}else{
stream_match = stream_search->second;
}
bool has_matched = false;
//TODO exec filtering action
if (has_matched){
// Say to firegex what filter has matched
//osyncstream(cout) << "BLOCKED " << rules_vector[match_res.matched] << endl;
return false;
}
return true;
}
//If the stream has already been matched, drop all data, and try to close the connection
static void keep_fin_packet(PyProxyQueue* pkt){
pkt->match_ctx.matching_has_been_called = true;
pkt->match_ctx.already_closed = true;
}
static void on_data_recv(Stream& stream, PyProxyQueue* pkt, string data) {
pkt->match_ctx.matching_has_been_called = true;
pkt->match_ctx.already_closed = false;
bool result = pkt->filter_action(pkt->match_ctx.pkt);
if (!result){
pkt->sctx.clean_stream_by_id(pkt->match_ctx.pkt->sid);
stream.client_data_callback(bind(keep_fin_packet, pkt));
stream.server_data_callback(bind(keep_fin_packet, pkt));
}
pkt->match_ctx.result = result;
}
//Input data filtering
static void on_client_data(Stream& stream, PyProxyQueue* pkt) {
on_data_recv(stream, pkt, string(stream.client_payload().begin(), stream.client_payload().end()));
}
//Server data filtering
static void on_server_data(Stream& stream, PyProxyQueue* pkt) {
on_data_recv(stream, pkt, string(stream.server_payload().begin(), stream.server_payload().end()));
}
// A stream was terminated. The second argument is the reason why it was terminated
static void on_stream_close(Stream& stream, PyProxyQueue* pkt) {
stream_id stream_id = stream_id::make_identifier(stream);
pkt->sctx.clean_stream_by_id(stream_id);
}
static void on_new_stream(Stream& stream, PyProxyQueue* pkt) {
stream.auto_cleanup_payloads(true);
if (stream.is_partial_stream()) {
//TODO take a decision about this...
stream.enable_recovery_mode(10 * 1024);
}
stream.client_data_callback(bind(on_client_data, placeholders::_1, pkt));
stream.server_data_callback(bind(on_server_data, placeholders::_1, pkt));
stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, pkt));
}
void handle_next_packet(NfQueue::PktRequest<PyProxyQueue>* pkt) override{
if (pkt->l4_proto != NfQueue::L4Proto::TCP){
throw invalid_argument("Only TCP and UDP are supported");
}
Tins::PDU* application_layer = pkt->tcp->inner_pdu();
u_int16_t payload_size = 0;
if (application_layer != nullptr){
payload_size = application_layer->size();
}
match_ctx.matching_has_been_called = false;
match_ctx.pkt = pkt;
if (pkt->is_ipv6){
follower.process_packet(*pkt->ipv6);
}else{
follower.process_packet(*pkt->ipv4);
}
// Do an action only is an ordered packet has been received
if (match_ctx.matching_has_been_called){
bool empty_payload = payload_size == 0;
//In this 2 cases we have to remove all data about the stream
if (!match_ctx.result || match_ctx.already_closed){
sctx.clean_stream_by_id(pkt->sid);
//If the packet has data, we have to remove it
if (!empty_payload){
Tins::PDU* data_layer = pkt->tcp->release_inner_pdu();
if (data_layer != nullptr){
delete data_layer;
}
}
//For the first matched data or only for data packets, we set FIN bit
//This only for client packets, because this will trigger server to close the connection
//Packets will be filtered anyway also if client don't send packets
if ((!match_ctx.result || !empty_payload) && pkt->is_input){
pkt->tcp->set_flag(Tins::TCP::FIN,1);
pkt->tcp->set_flag(Tins::TCP::ACK,1);
pkt->tcp->set_flag(Tins::TCP::SYN,0);
}
//Send the edited packet to the kernel
return pkt->mangle();
}
}
return pkt->accept();
}
~PyProxyQueue() {
sctx.clean();
}
};
}}
#endif // PROXY_TUNNEL_CLASS_CPP

View File

@@ -1,22 +0,0 @@
#ifndef PROXY_TUNNEL_SETTINGS_CPP
#define PROXY_TUNNEL_SETTINGS_CPP
#include <vector>
#include <memory>
using namespace std;
class PyCodeConfig{
public:
const vector<uint8_t> code;
public:
PyCodeConfig(vector<uint8_t> pycode): code(pycode){}
PyCodeConfig(): code(vector<uint8_t>()){}
~PyCodeConfig(){}
};
shared_ptr<PyCodeConfig> config;
#endif // PROXY_TUNNEL_SETTINGS_CPP

View File

@@ -1,39 +0,0 @@
#ifndef STREAM_CTX_CPP
#define STREAM_CTX_CPP
#include <iostream>
#include <tins/tcp_ip/stream_identifier.h>
#include <map>
using namespace std;
typedef Tins::TCPIP::StreamIdentifier stream_id;
struct pyfilter_ctx {
void * pyglob; // TODO python glob???
string pycode;
};
typedef map<stream_id, pyfilter_ctx*> matching_map;
struct stream_ctx {
matching_map streams_ctx;
void clean_stream_by_id(stream_id sid){
auto stream_search = streams_ctx.find(sid);
if (stream_search != streams_ctx.end()){
auto stream_match = stream_search->second;
//DEALLOC PY GLOB TODO
delete stream_match;
}
}
void clean(){
for (auto ele: streams_ctx){
//TODO dealloc ele.second.pyglob
delete ele.second;
}
}
};
#endif // STREAM_CTX_CPP

View File

@@ -0,0 +1,266 @@
#ifndef PROXY_TUNNEL_CLASS_CPP
#define PROXY_TUNNEL_CLASS_CPP
#include <linux/netfilter/nfnetlink_queue.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <linux/netfilter/nfnetlink_conntrack.h>
#include <tins/tins.h>
#include <tins/tcp_ip/stream_follower.h>
#include <tins/tcp_ip/stream_identifier.h>
#include <libmnl/libmnl.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/types.h>
#include <stdexcept>
#include <thread>
#include <syncstream>
#include <iostream>
#include "../classes/netfilter.cpp"
#include "../classes/nfqueue.cpp"
#include "stream_ctx.cpp"
#include "settings.cpp"
#include <Python.h>
using Tins::TCPIP::Stream;
using Tins::TCPIP::StreamFollower;
using namespace std;
namespace Firegex {
namespace PyProxy {
class PyProxyQueue: public NfQueue::ThreadNfQueue<PyProxyQueue> {
private:
u_int16_t latest_config_ver = 0;
public:
stream_ctx sctx;
StreamFollower follower;
PyThreadState * tstate = nullptr;
PyInterpreterConfig py_thread_config = {
.use_main_obmalloc = 0,
.allow_fork = 0,
.allow_exec = 0,
.allow_threads = 0,
.allow_daemon_threads = 0,
.check_multi_interp_extensions = 1,
.gil = PyInterpreterConfig_OWN_GIL,
};
NfQueue::PktRequest<PyProxyQueue>* pkt;
NfQueue::tcp_ack_seq_ctx* current_tcp_ack = nullptr;
PyObject* handle_packet_code = nullptr;
void before_loop() override {
PyStatus pystatus;
// Create a new interpreter for the thread
tstate = PyThreadState_New(PyInterpreterState_Main());
PyEval_AcquireThread(tstate);
pystatus = Py_NewInterpreterFromConfig(&tstate, &py_thread_config);
if(tstate == nullptr){
cerr << "[fatal] [main] Failed to create new interpreter" << endl;
throw invalid_argument("Failed to create new interpreter (null tstate)");
}
if (PyStatus_Exception(pystatus)) {
cerr << "[fatal] [main] Failed to create new interpreter" << endl;
Py_ExitStatusException(pystatus);
throw invalid_argument("Failed to create new interpreter (pystatus exc)");
}
if(!PyGC_IsEnabled()){
PyGC_Enable();
}
handle_packet_code = unmarshal_code(py_handle_packet_code);
// Setting callbacks for the stream follower
follower.new_stream_callback(bind(on_new_stream, placeholders::_1, this));
follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, this));
}
inline void print_blocked_reason(const string& func_name){
control_socket << "BLOCKED " << func_name << endl;
}
inline void print_mangle_reason(const string& func_name){
control_socket << "MANGLED " << func_name << endl;
}
inline void print_exception_reason(){
control_socket << "EXCEPTION" << endl;
}
//If the stream has already been matched, drop all data, and try to close the connection
static void keep_fin_packet(PyProxyQueue* pyq){
pyq->pkt->reject();// This is needed because the callback has to take the updated pkt pointer!
}
static void keep_dropped(PyProxyQueue* pyq){
pyq->pkt->drop();// This is needed because the callback has to take the updated pkt pointer!
}
void filter_action(NfQueue::PktRequest<PyProxyQueue>* pkt, Stream& stream, const string& data){
auto stream_search = sctx.streams_ctx.find(pkt->sid);
pyfilter_ctx* stream_match;
if (stream_search == sctx.streams_ctx.end()){
shared_ptr<PyCodeConfig> conf = config;
//If config is not set, ignore the stream
PyObject* compiled_code = conf->compiled_code();
if (compiled_code == nullptr){
stream.client_data_callback(nullptr);
stream.server_data_callback(nullptr);
stream.ignore_client_data();
stream.ignore_server_data();
return pkt->accept();
}else{
try{
stream_match = new pyfilter_ctx(compiled_code, handle_packet_code);
}catch(invalid_argument& e){
cerr << "[error] [filter_action] Failed to create the filter context" << endl;
print_exception_reason();
sctx.clean_stream_by_id(pkt->sid);
stream.client_data_callback(nullptr);
stream.server_data_callback(nullptr);
stream.ignore_client_data();
stream.ignore_server_data();
return pkt->accept();
}
sctx.streams_ctx.insert_or_assign(pkt->sid, stream_match);
}
}else{
stream_match = stream_search->second;
}
auto result = stream_match->handle_packet(pkt, data);
switch(result.action){
case PyFilterResponse::ACCEPT:
return pkt->accept();
case PyFilterResponse::DROP:
print_blocked_reason(*result.filter_match_by);
sctx.clean_stream_by_id(pkt->sid);
stream.client_data_callback(bind(keep_dropped, this));
stream.server_data_callback(bind(keep_dropped, this));
return pkt->drop();
case PyFilterResponse::REJECT:
print_blocked_reason(*result.filter_match_by);
sctx.clean_stream_by_id(pkt->sid);
stream.client_data_callback(bind(keep_fin_packet, this));
stream.server_data_callback(bind(keep_fin_packet, this));
return pkt->reject();
case PyFilterResponse::MANGLE:
pkt->mangle_custom_pkt(result.mangled_packet->c_str(), result.mangled_packet->size());
if (pkt->get_action() == NfQueue::FilterAction::DROP){
cerr << "[error] [filter_action] Failed to mangle: the packet sent is not serializzable... the packet was dropped" << endl;
print_blocked_reason(*result.filter_match_by);
print_exception_reason();
}else{
print_mangle_reason(*result.filter_match_by);
}
return;
case PyFilterResponse::EXCEPTION:
case PyFilterResponse::INVALID:
print_exception_reason();
sctx.clean_stream_by_id(pkt->sid);
//Free the packet data
stream.ignore_client_data();
stream.ignore_server_data();
stream.client_data_callback(nullptr);
stream.server_data_callback(nullptr);
return pkt->accept();
}
}
static void on_data_recv(Stream& stream, PyProxyQueue* pyq, const string& data) {
pyq->pkt->fix_data_payload();
pyq->filter_action(pyq->pkt, stream, data); //Only here the rebuilt_tcp_data is set
}
//Input data filtering
static void on_client_data(Stream& stream, PyProxyQueue* pyq) {
auto data = stream.client_payload();
on_data_recv(stream, pyq, string((char*)data.data(), data.size()));
}
//Server data filtering
static void on_server_data(Stream& stream, PyProxyQueue* pyq) {
auto data = stream.server_payload();
on_data_recv(stream, pyq, string((char*)data.data(), data.size()));
}
// A stream was terminated. The second argument is the reason why it was terminated
static void on_stream_close(Stream& stream, PyProxyQueue* pyq) {
stream_id stream_id = stream_id::make_identifier(stream);
pyq->sctx.clean_stream_by_id(stream_id);
pyq->sctx.clean_tcp_ack_by_id(stream_id);
}
static void on_new_stream(Stream& stream, PyProxyQueue* pyq) {
stream.auto_cleanup_payloads(true);
if (stream.is_partial_stream()) {
stream.enable_recovery_mode(10 * 1024);
}
if (pyq->current_tcp_ack != nullptr){
pyq->current_tcp_ack->reset();
}else{
pyq->current_tcp_ack = new NfQueue::tcp_ack_seq_ctx();
pyq->sctx.tcp_ack_ctx.insert_or_assign(pyq->pkt->sid, pyq->current_tcp_ack);
pyq->pkt->ack_seq_offset = pyq->current_tcp_ack; // Set ack context
}
//Should not happen, but with this we can be sure about this
auto tcp_ack_search = pyq->sctx.tcp_ack_ctx.find(pyq->pkt->sid);
if (tcp_ack_search != pyq->sctx.tcp_ack_ctx.end()){
tcp_ack_search->second->reset();
}
stream.client_data_callback(bind(on_client_data, placeholders::_1, pyq));
stream.server_data_callback(bind(on_server_data, placeholders::_1, pyq));
stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, pyq));
}
void handle_next_packet(NfQueue::PktRequest<PyProxyQueue>* _pkt) override{
pkt = _pkt; // Setting packet context
if (pkt->l4_proto != NfQueue::L4Proto::TCP){
throw invalid_argument("Only TCP and UDP are supported");
}
auto tcp_ack_search = sctx.tcp_ack_ctx.find(pkt->sid);
if (tcp_ack_search != sctx.tcp_ack_ctx.end()){
current_tcp_ack = tcp_ack_search->second;
pkt->ack_seq_offset = current_tcp_ack;
}else{
current_tcp_ack = nullptr;
//If necessary will be created by libtis new_stream callback
}
pkt->fix_tcp_ack();
if (pkt->is_ipv6){
follower.process_packet(*pkt->ipv6);
}else{
follower.process_packet(*pkt->ipv4);
}
//Fallback to the default action
if (pkt->get_action() == NfQueue::FilterAction::NOACTION){
return pkt->accept();
}
}
~PyProxyQueue() {
// Closing first the interpreter
Py_EndInterpreter(tstate);
PyEval_ReleaseThread(tstate);
PyThreadState_Clear(tstate);
PyThreadState_Delete(tstate);
Py_DECREF(handle_packet_code);
sctx.clean();
}
};
}}
#endif // PROXY_TUNNEL_CLASS_CPP

View File

@@ -0,0 +1,102 @@
#ifndef PROXY_TUNNEL_SETTINGS_CPP
#define PROXY_TUNNEL_SETTINGS_CPP
#include <Python.h>
#include <marshal.h>
#include <vector>
#include <memory>
#include <iostream>
#include "../utils.cpp"
using namespace std;
namespace Firegex {
namespace PyProxy {
class PyCodeConfig;
shared_ptr<PyCodeConfig> config;
UnixClientConnection control_socket;
PyObject* unmarshal_code(string encoded_code){
if (encoded_code.empty()) return nullptr;
return PyMarshal_ReadObjectFromString(encoded_code.c_str(), encoded_code.size());
}
class PyCodeConfig{
public:
string encoded_code;
PyCodeConfig(const string& pycode){
PyObject* compiled_code = Py_CompileStringExFlags(pycode.c_str(), "<pyfilter>", Py_file_input, NULL, 2);
if (compiled_code == nullptr){
std::cerr << "[fatal] [main] Failed to compile the code" << endl;
throw invalid_argument("Failed to compile the code");
}
PyObject* glob = PyDict_New();
PyObject* result = PyEval_EvalCode(compiled_code, glob, glob);
Py_DECREF(glob);
if (PyErr_Occurred()){
PyErr_Print();
Py_DECREF(compiled_code);
std::cerr << "[fatal] [main] Failed to execute the code" << endl;
throw invalid_argument("Failed to execute the code, maybe an invalid filter code has been provided");
}
Py_XDECREF(result);
PyObject* code_dump = PyMarshal_WriteObjectToString(compiled_code, 4);
Py_DECREF(compiled_code);
if (code_dump == nullptr){
if (PyErr_Occurred())
PyErr_Print();
std::cerr << "[fatal] [main] Failed to dump the code" << endl;
throw invalid_argument("Failed to dump the code");
}
if (!PyBytes_Check(code_dump)){
std::cerr << "[fatal] [main] Failed to dump the code" << endl;
Py_DECREF(code_dump);
throw invalid_argument("Failed to dump the code");
}
encoded_code = string(PyBytes_AsString(code_dump), PyBytes_Size(code_dump));
Py_DECREF(code_dump);
}
PyObject* compiled_code(){
return unmarshal_code(encoded_code);
}
PyCodeConfig(){}
};
void init_control_socket(){
char * socket_path = getenv("FIREGEX_NFPROXY_SOCK");
if (socket_path == nullptr) throw invalid_argument("FIREGEX_NFPROXY_SOCK not set");
if (strlen(socket_path) >= 108) throw invalid_argument("FIREGEX_NFPROXY_SOCK too long");
control_socket = UnixClientConnection(socket_path);
}
string py_handle_packet_code;
void init_handle_packet_code(){
PyObject* compiled_code = Py_CompileStringExFlags(
"firegex.nfproxy.internals.handle_packet(globals())\n", "<pyfilter>",
Py_file_input, NULL, 2);
PyObject* code_dump = PyMarshal_WriteObjectToString(compiled_code, 4);
Py_DECREF(compiled_code);
if (code_dump == nullptr){
if (PyErr_Occurred())
PyErr_Print();
std::cerr << "[fatal] [main] Failed to dump the code" << endl;
throw invalid_argument("Failed to dump the code");
}
if (!PyBytes_Check(code_dump)){
std::cerr << "[fatal] [main] Failed to dump the code" << endl;
Py_DECREF(code_dump);
throw invalid_argument("Failed to dump the code");
}
py_handle_packet_code = string(PyBytes_AsString(code_dump), PyBytes_Size(code_dump));
Py_DECREF(code_dump);
}
}}
#endif // PROXY_TUNNEL_SETTINGS_CPP

View File

@@ -0,0 +1,280 @@
#ifndef STREAM_CTX_CPP
#define STREAM_CTX_CPP
#include <iostream>
#include <tins/tcp_ip/stream_identifier.h>
#include <map>
#include <Python.h>
#include "../classes/netfilter.cpp"
#include "../classes/nfqueue.cpp"
#include "settings.cpp"
#include "../utils.cpp"
using namespace std;
namespace Firegex {
namespace PyProxy {
class PyCodeConfig;
class PyProxyQueue;
enum PyFilterResponse {
ACCEPT = 0,
DROP = 1,
REJECT = 2,
MANGLE = 3,
EXCEPTION = 4,
INVALID = 5
};
const PyFilterResponse VALID_PYTHON_RESPONSE[4] = {
PyFilterResponse::ACCEPT,
PyFilterResponse::DROP,
PyFilterResponse::REJECT,
PyFilterResponse::MANGLE
};
struct py_filter_response {
PyFilterResponse action;
string* filter_match_by = nullptr;
string* mangled_packet = nullptr;
py_filter_response(PyFilterResponse action, string* filter_match_by = nullptr, string* mangled_packet = nullptr):
action(action), filter_match_by(filter_match_by), mangled_packet(mangled_packet){}
~py_filter_response(){
delete mangled_packet;
delete filter_match_by;
}
};
typedef Tins::TCPIP::StreamIdentifier stream_id;
struct pyfilter_ctx {
PyObject * glob = nullptr;
PyObject * py_handle_packet = nullptr;
pyfilter_ctx(PyObject * compiled_code, PyObject * handle_packet_code){
py_handle_packet = handle_packet_code;
Py_INCREF(py_handle_packet);
glob = PyDict_New();
PyObject* result = PyEval_EvalCode(compiled_code, glob, glob);
Py_XDECREF(compiled_code);
if (PyErr_Occurred()){
PyErr_Print();
Py_XDECREF(glob);
std::cerr << "[fatal] [main] Failed to compile the code" << endl;
throw invalid_argument("Failed to execute the code, maybe an invalid filter code has been provided");
}
Py_XDECREF(result);
}
~pyfilter_ctx(){
Py_DECREF(glob);
Py_DECREF(py_handle_packet);
PyGC_Collect();
}
inline void set_item_to_glob(const char* key, PyObject* value){
set_item_to_dict(glob, key, value);
}
inline PyObject* get_item_from_glob(const char* key){
return PyDict_GetItemString(glob, key);
}
void del_item_from_glob(const char* key){
if (PyDict_DelItemString(glob, key) != 0){
if (PyErr_Occurred())
PyErr_Print();
throw invalid_argument("Failed to delete item from dict");
}
}
inline void set_item_to_dict(PyObject* dict, const char* key, PyObject* value){
if (PyDict_SetItemString(dict, key, value) != 0){
if (PyErr_Occurred())
PyErr_Print();
throw invalid_argument("Failed to set item to dict");
}
Py_DECREF(value);
}
py_filter_response handle_packet(
NfQueue::PktRequest<PyProxyQueue>* pkt,
const string& data
){
PyObject * packet_info = PyDict_New();
pkt->reserialize();
set_item_to_dict(packet_info, "data", PyBytes_FromStringAndSize(data.c_str(), data.size()));
set_item_to_dict(packet_info, "l4_size", PyLong_FromLong(pkt->data_size()));
set_item_to_dict(packet_info, "raw_packet", PyBytes_FromStringAndSize(pkt->packet.c_str(), pkt->packet.size()));
set_item_to_dict(packet_info, "is_input", PyBool_FromLong(pkt->is_input));
set_item_to_dict(packet_info, "is_ipv6", PyBool_FromLong(pkt->is_ipv6));
set_item_to_dict(packet_info, "is_tcp", PyBool_FromLong(pkt->l4_proto == NfQueue::L4Proto::TCP));
// Set packet info to the global context
set_item_to_glob("__firegex_packet_info", packet_info);
PyObject * result = PyEval_EvalCode(py_handle_packet, glob, glob);
PyGC_Collect();
del_item_from_glob("__firegex_packet_info");
if (PyErr_Occurred()){
cerr << "[error] [handle_packet] Failed to execute the code " << result << endl;
PyErr_Print();
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] Exception raised" << endl;
#endif
return py_filter_response(PyFilterResponse::EXCEPTION);
}
Py_DECREF(result);
result = get_item_from_glob("__firegex_pyfilter_result");
if (result == nullptr){
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] No result found" << endl;
#endif
return py_filter_response(PyFilterResponse::INVALID);
}
if (!PyDict_Check(result)){
if (PyErr_Occurred()){
PyErr_Print();
}
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] Result is not a dict" << endl;
#endif
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(PyFilterResponse::INVALID);
}
PyObject* action = PyDict_GetItemString(result, "action");
if (action == nullptr){
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] No result action found" << endl;
#endif
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(PyFilterResponse::INVALID);
}
if (!PyLong_Check(action)){
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] Action is not a long" << endl;
#endif
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(PyFilterResponse::INVALID);
}
PyFilterResponse action_enum = (PyFilterResponse)PyLong_AsLong(action);
//Check action_enum
bool valid = false;
for (auto valid_action: VALID_PYTHON_RESPONSE){
if (action_enum == valid_action){
valid = true;
break;
}
}
if (!valid){
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] Invalid action" << endl;
#endif
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(PyFilterResponse::INVALID);
}
if (action_enum == PyFilterResponse::ACCEPT){
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(action_enum);
}
PyObject *func_name_py = PyDict_GetItemString(result, "matched_by");
if (func_name_py == nullptr){
del_item_from_glob("__firegex_pyfilter_result");
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] No result matched_by found" << endl;
#endif
return py_filter_response(PyFilterResponse::INVALID);
}
if (!PyUnicode_Check(func_name_py)){
del_item_from_glob("__firegex_pyfilter_result");
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] matched_by is not a string" << endl;
#endif
return py_filter_response(PyFilterResponse::INVALID);
}
string* func_name = new string(PyUnicode_AsUTF8(func_name_py));
if (action_enum == PyFilterResponse::DROP || action_enum == PyFilterResponse::REJECT){
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(action_enum, func_name);
}
if (action_enum == PyFilterResponse::MANGLE){
PyObject* mangled_packet = PyDict_GetItemString(result, "mangled_packet");
if (mangled_packet == nullptr){
del_item_from_glob("__firegex_pyfilter_result");
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] No result mangled_packet found" << endl;
#endif
return py_filter_response(PyFilterResponse::INVALID);
}
if (!PyBytes_Check(mangled_packet)){
#ifdef DEBUG
cerr << "[DEBUG] [handle_packet] mangled_packet is not a bytes" << endl;
#endif
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(PyFilterResponse::INVALID);
}
string* pkt_str = new string(PyBytes_AsString(mangled_packet), PyBytes_Size(mangled_packet));
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(PyFilterResponse::MANGLE, func_name, pkt_str);
}
//Should never reach this point, but just in case of new action not managed...
del_item_from_glob("__firegex_pyfilter_result");
return py_filter_response(PyFilterResponse::INVALID);
}
};
typedef map<stream_id, pyfilter_ctx*> matching_map;
struct stream_ctx {
matching_map streams_ctx;
NfQueue::tcp_ack_map tcp_ack_ctx;
void clean_stream_by_id(stream_id sid){
auto stream_search = streams_ctx.find(sid);
if (stream_search != streams_ctx.end()){
auto stream_match = stream_search->second;
delete stream_match;
streams_ctx.erase(stream_search->first);
}
}
void clean_tcp_ack_by_id(stream_id sid){
auto tcp_ack_search = tcp_ack_ctx.find(sid);
if (tcp_ack_search != tcp_ack_ctx.end()){
auto tcp_ack = tcp_ack_search->second;
delete tcp_ack;
tcp_ack_ctx.erase(tcp_ack_search->first);
}
}
void clean(){
for (auto ele: streams_ctx){
delete ele.second;
}
for (auto ele: tcp_ack_ctx){
delete ele.second;
}
tcp_ack_ctx.clear();
streams_ctx.clear();
}
};
}}
#endif // STREAM_CTX_CPP

View File

@@ -76,12 +76,11 @@ class RegexRules{
}else{ }else{
hs_free_database(db); hs_free_database(db);
} }
} }
private: private:
static inline u_int16_t glob_seq = 0; static inline uint16_t glob_seq = 0;
u_int16_t version; uint16_t version;
vector<pair<string, decoded_regex>> decoded_input_rules; vector<pair<string, decoded_regex>> decoded_input_rules;
vector<pair<string, decoded_regex>> decoded_output_rules; vector<pair<string, decoded_regex>> decoded_output_rules;
bool is_stream = true; bool is_stream = true;
@@ -96,9 +95,7 @@ class RegexRules{
input_ruleset.hs_db = nullptr; input_ruleset.hs_db = nullptr;
} }
} }
void fill_ruleset(vector<pair<string, decoded_regex>> & decoded, regex_ruleset & ruleset){ void fill_ruleset(vector<pair<string, decoded_regex>> & decoded, regex_ruleset & ruleset){
size_t n_of_regex = decoded.size(); size_t n_of_regex = decoded.size();
if (n_of_regex == 0){ if (n_of_regex == 0){
@@ -150,7 +147,6 @@ class RegexRules{
public: public:
RegexRules(vector<string> raw_rules, bool is_stream){ RegexRules(vector<string> raw_rules, bool is_stream){
this->is_stream = is_stream; this->is_stream = is_stream;
this->version = ++glob_seq; // 0 version is a invalid version (useful for some logics)
for(string ele : raw_rules){ for(string ele : raw_rules){
try{ try{
decoded_regex rule = decode_regex(ele); decoded_regex rule = decode_regex(ele);
@@ -170,6 +166,7 @@ class RegexRules{
free_dbs(); free_dbs();
throw current_exception(); throw current_exception();
} }
this->version = ++glob_seq; // 0 version is the null version
} }
u_int16_t ver(){ u_int16_t ver(){

View File

@@ -20,6 +20,7 @@
#include "../classes/netfilter.cpp" #include "../classes/netfilter.cpp"
#include "stream_ctx.cpp" #include "stream_ctx.cpp"
#include "regex_rules.cpp" #include "regex_rules.cpp"
#include "../utils.cpp"
using namespace std; using namespace std;
@@ -30,22 +31,14 @@ namespace Regex {
using Tins::TCPIP::Stream; using Tins::TCPIP::Stream;
using Tins::TCPIP::StreamFollower; using Tins::TCPIP::StreamFollower;
class RegexNfQueue : public NfQueue::ThreadNfQueue<RegexNfQueue> { class RegexNfQueue : public NfQueue::ThreadNfQueue<RegexNfQueue> {
public: public:
stream_ctx sctx; stream_ctx sctx;
u_int16_t latest_config_ver = 0; u_int16_t latest_config_ver = 0;
StreamFollower follower; StreamFollower follower;
struct { NfQueue::PktRequest<RegexNfQueue>* pkt;
bool matching_has_been_called = false;
bool already_closed = false;
bool result;
NfQueue::PktRequest<RegexNfQueue>* pkt;
} match_ctx;
bool filter_action(NfQueue::PktRequest<RegexNfQueue>* pkt){ bool filter_action(NfQueue::PktRequest<RegexNfQueue>* pkt, const string& data){
shared_ptr<RegexRules> conf = regex_config; shared_ptr<RegexRules> conf = regex_config;
auto current_version = conf->ver(); auto current_version = conf->ver();
@@ -91,12 +84,12 @@ public:
stream_match = stream_search->second; stream_match = stream_search->second;
} }
err = hs_scan_stream( err = hs_scan_stream(
stream_match,pkt->data, pkt->data_size, stream_match, data.c_str(), data.size(),
0, scratch_space, match_func, &match_res 0, scratch_space, match_func, &match_res
); );
}else{ }else{
err = hs_scan( err = hs_scan(
regex_matcher,pkt->data, pkt->data_size, regex_matcher, data.c_str(), data.size(),
0, scratch_space, match_func, &match_res 0, scratch_space, match_func, &match_res
); );
} }
@@ -108,7 +101,7 @@ public:
throw invalid_argument("Cannot close stream match on hyperscan"); throw invalid_argument("Cannot close stream match on hyperscan");
} }
if (err != HS_SUCCESS && err != HS_SCAN_TERMINATED) { if (err != HS_SUCCESS && err != HS_SCAN_TERMINATED) {
cerr << "[error] [filter_callback] Error while matching the stream (hs)" << endl; cerr << "[error] [filter_callback] Error while matching the stream (hs) " << err << endl;
throw invalid_argument("Error while matching the stream with hyperscan"); throw invalid_argument("Error while matching the stream with hyperscan");
} }
if (match_res.has_matched){ if (match_res.has_matched){
@@ -119,85 +112,30 @@ public:
return true; return true;
} }
void handle_next_packet(NfQueue::PktRequest<RegexNfQueue>* pkt) override{
bool empty_payload = pkt->data_size == 0;
if (pkt->tcp){
match_ctx.matching_has_been_called = false;
match_ctx.pkt = pkt;
if (pkt->ipv4){
follower.process_packet(*pkt->ipv4);
}else{
follower.process_packet(*pkt->ipv6);
}
// Do an action only is an ordered packet has been received
if (match_ctx.matching_has_been_called){
//In this 2 cases we have to remove all data about the stream
if (!match_ctx.result || match_ctx.already_closed){
sctx.clean_stream_by_id(pkt->sid);
//If the packet has data, we have to remove it
if (!empty_payload){
Tins::PDU* data_layer = pkt->tcp->release_inner_pdu();
if (data_layer != nullptr){
delete data_layer;
}
}
//For the first matched data or only for data packets, we set FIN bit
//This only for client packets, because this will trigger server to close the connection
//Packets will be filtered anyway also if client don't send packets
if ((!match_ctx.result || !empty_payload) && pkt->is_input){
pkt->tcp->set_flag(Tins::TCP::FIN,1);
pkt->tcp->set_flag(Tins::TCP::ACK,1);
pkt->tcp->set_flag(Tins::TCP::SYN,0);
}
//Send the edited packet to the kernel
return pkt->mangle();
}
}
return pkt->accept();
}else{
if (!pkt->udp){
throw invalid_argument("Only TCP and UDP are supported");
}
if(empty_payload){
return pkt->accept();
}else if (filter_action(pkt)){
return pkt->accept();
}else{
return pkt->drop();
}
}
}
//If the stream has already been matched, drop all data, and try to close the connection //If the stream has already been matched, drop all data, and try to close the connection
static void keep_fin_packet(RegexNfQueue* nfq){ static void keep_fin_packet(RegexNfQueue* nfq){
nfq->match_ctx.matching_has_been_called = true; nfq->pkt->reject(); // This is needed because the callback has to take the updated pkt pointer!
nfq->match_ctx.already_closed = true;
} }
static void on_data_recv(Stream& stream, RegexNfQueue* nfq, string data) { static void on_data_recv(Stream& stream, RegexNfQueue* nfq, const string& data) {
nfq->match_ctx.matching_has_been_called = true; if (!nfq->filter_action(nfq->pkt, data)){
nfq->match_ctx.already_closed = false; nfq->sctx.clean_stream_by_id(nfq->pkt->sid);
nfq->match_ctx.pkt->data = data.data();
nfq->match_ctx.pkt->data_size = data.size();
bool result = nfq->filter_action(nfq->match_ctx.pkt);
if (!result){
nfq->sctx.clean_stream_by_id(nfq->match_ctx.pkt->sid);
stream.client_data_callback(bind(keep_fin_packet, nfq)); stream.client_data_callback(bind(keep_fin_packet, nfq));
stream.server_data_callback(bind(keep_fin_packet, nfq)); stream.server_data_callback(bind(keep_fin_packet, nfq));
nfq->pkt->reject();
} }
nfq->match_ctx.result = result;
} }
//Input data filtering //Input data filtering
static void on_client_data(Stream& stream, RegexNfQueue* nfq) { static void on_client_data(Stream& stream, RegexNfQueue* nfq) {
on_data_recv(stream, nfq, string(stream.client_payload().begin(), stream.client_payload().end())); auto data = stream.client_payload();
on_data_recv(stream, nfq, string((char*)data.data(), data.size()));
} }
//Server data filtering //Server data filtering
static void on_server_data(Stream& stream, RegexNfQueue* nfq) { static void on_server_data(Stream& stream, RegexNfQueue* nfq) {
on_data_recv(stream, nfq, string(stream.server_payload().begin(), stream.server_payload().end())); auto data = stream.server_payload();
on_data_recv(stream, nfq, string((char*)data.data(), data.size()));
} }
// A stream was terminated. The second argument is the reason why it was terminated // A stream was terminated. The second argument is the reason why it was terminated
@@ -216,6 +154,32 @@ public:
stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, nfq)); stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, nfq));
} }
void handle_next_packet(NfQueue::PktRequest<RegexNfQueue>* _pkt) override{
pkt = _pkt; // Setting packet context
if (pkt->tcp){
if (pkt->ipv4){
follower.process_packet(*pkt->ipv4);
}else{
follower.process_packet(*pkt->ipv6);
}
//Fallback to the default action
if (pkt->get_action() == NfQueue::FilterAction::NOACTION){
return pkt->accept();
}
}else{
if (!pkt->udp){
throw invalid_argument("Only TCP and UDP are supported");
}
if(pkt->data_size() == 0){
return pkt->accept();
}else if (filter_action(pkt, string(pkt->data(), pkt->data_size()))){
return pkt->accept();
}else{
return pkt->drop();
}
}
}
void before_loop() override{ void before_loop() override{
follower.new_stream_callback(bind(on_new_stream, placeholders::_1, this)); follower.new_stream_callback(bind(on_new_stream, placeholders::_1, this));
follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, this)); follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, this));

View File

@@ -17,7 +17,6 @@ namespace Regex {
typedef Tins::TCPIP::StreamIdentifier stream_id; typedef Tins::TCPIP::StreamIdentifier stream_id;
typedef map<stream_id, hs_stream_t*> matching_map; typedef map<stream_id, hs_stream_t*> matching_map;
#ifdef DEBUG
ostream& operator<<(ostream& os, const Tins::TCPIP::StreamIdentifier::address_type &sid){ ostream& operator<<(ostream& os, const Tins::TCPIP::StreamIdentifier::address_type &sid){
bool first_print = false; bool first_print = false;
for (auto ele: sid){ for (auto ele: sid){
@@ -33,7 +32,6 @@ ostream& operator<<(ostream& os, const stream_id &sid){
os << sid.max_address << ":" << sid.max_address_port << " -> " << sid.min_address << ":" << sid.min_address_port; os << sid.max_address << ":" << sid.max_address_port << " -> " << sid.min_address << ":" << sid.min_address_port;
return os; return os;
} }
#endif
struct stream_ctx { struct stream_ctx {
matching_map in_hs_streams; matching_map in_hs_streams;

View File

@@ -1,10 +1,17 @@
#ifndef UTILS_CPP
#define UTILS_CPP
#include <string> #include <string>
#include <unistd.h> #include <unistd.h>
#include <queue> #include <queue>
#include <condition_variable> #include <condition_variable>
#include <sys/socket.h>
#ifndef UTILS_CPP #include <sys/un.h>
#define UTILS_CPP #include <stdexcept>
#include <cstring>
#include <iostream>
#include <cerrno>
#include <sstream>
bool unhexlify(std::string const &hex, std::string &newString) { bool unhexlify(std::string const &hex, std::string &newString) {
try{ try{
@@ -22,6 +29,113 @@ bool unhexlify(std::string const &hex, std::string &newString) {
} }
} }
class UnixClientConnection {
public:
int sockfd = -1;
struct sockaddr_un addr;
private:
// Internal buffer to accumulate the output until flush
std::ostringstream streamBuffer;
public:
UnixClientConnection(){};
UnixClientConnection(const char* path) {
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd == -1) {
throw std::runtime_error(std::string("socket error: ") + std::strerror(errno));
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
if (connect(sockfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) != 0) {
throw std::runtime_error(std::string("connect error: ") + std::strerror(errno));
}
}
// Delete copy constructor and assignment operator to avoid resource duplication
UnixClientConnection(const UnixClientConnection&) = delete;
UnixClientConnection& operator=(const UnixClientConnection&) = delete;
// Move constructor
UnixClientConnection(UnixClientConnection&& other) noexcept
: sockfd(other.sockfd), addr(other.addr) {
other.sockfd = -1;
}
// Move assignment operator
UnixClientConnection& operator=(UnixClientConnection&& other) noexcept {
if (this != &other) {
if (sockfd != -1) {
close(sockfd);
}
sockfd = other.sockfd;
addr = other.addr;
other.sockfd = -1;
}
return *this;
}
void send(const std::string& data) {
if (::write(sockfd, data.c_str(), data.size()) == -1) {
throw std::runtime_error(std::string("write error: ") + std::strerror(errno));
}
}
std::string recv(size_t size) {
std::string buffer(size, '\0');
ssize_t bytesRead = ::read(sockfd, &buffer[0], size);
if (bytesRead <= 0) {
throw std::runtime_error(std::string("read error: ") + std::strerror(errno));
}
buffer.resize(bytesRead); // resize to actual bytes read
return buffer;
}
// Template overload for generic types
template<typename T>
UnixClientConnection& operator<<(const T& data) {
streamBuffer << data;
return *this;
}
// Overload for manipulators (e.g., std::endl)
UnixClientConnection& operator<<(std::ostream& (*manip)(std::ostream&)) {
// Check if the manipulator is std::endl (or equivalent flush)
if (manip == static_cast<std::ostream& (*)(std::ostream&)>(std::endl)){
streamBuffer << '\n'; // Add a newline
std::string packet = streamBuffer.str();
streamBuffer.str(""); // Clear the buffer
// Send the accumulated data as one packet
send(packet);
}
if (static_cast<std::ostream& (*)(std::ostream&)>(std::flush)) {
std::string packet = streamBuffer.str();
streamBuffer.str(""); // Clear the buffer
// Send the accumulated data as one packet
send(packet);
} else {
// For other manipulators, simply pass them to the buffer
streamBuffer << manip;
}
return *this;
}
// Overload operator<< to allow printing connection info
friend std::ostream& operator<<(std::ostream& os, const UnixClientConnection& conn) {
os << "UnixClientConnection(sockfd=" << conn.sockfd
<< ", path=" << conn.addr.sun_path << ")";
return os;
}
~UnixClientConnection() {
if (sockfd != -1) {
close(sockfd);
}
}
};
#ifdef USE_PIPES_FOR_BLOKING_QUEUE #ifdef USE_PIPES_FOR_BLOKING_QUEUE
template<typename T> template<typename T>

View File

@@ -1,4 +1,4 @@
from modules.firewall.models import * from modules.firewall.models import FirewallSettings, Action, Rule, Protocol, Mode, Table
from utils import nftables_int_to_json, ip_family, NFTableManager, is_ip_parse from utils import nftables_int_to_json, ip_family, NFTableManager, is_ip_parse
import copy import copy
@@ -9,7 +9,8 @@ class FiregexTables(NFTableManager):
filter_table = "filter" filter_table = "filter"
mangle_table = "mangle" mangle_table = "mangle"
def init_comands(self, policy:str=Action.ACCEPT, opt: FirewallSettings|None = None): def init_comands(self, policy:str=Action.ACCEPT, opt:
FirewallSettings|None = None):
rules = [ rules = [
{"add":{"table":{"name":self.filter_table,"family":"ip"}}}, {"add":{"table":{"name":self.filter_table,"family":"ip"}}},
{"add":{"table":{"name":self.filter_table,"family":"ip6"}}}, {"add":{"table":{"name":self.filter_table,"family":"ip6"}}},
@@ -41,7 +42,8 @@ class FiregexTables(NFTableManager):
{"add":{"chain":{"family":"ip","table":self.mangle_table,"name":self.rules_chain_out}}}, {"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}}}, {"add":{"chain":{"family":"ip6","table":self.mangle_table,"name":self.rules_chain_out}}},
] ]
if opt is None: return rules if opt is None:
return rules
if opt.allow_loopback: if opt.allow_loopback:
rules.extend([ rules.extend([
@@ -194,13 +196,18 @@ class FiregexTables(NFTableManager):
def chain_to_firegex(self, chain:str, table:str): def chain_to_firegex(self, chain:str, table:str):
if table == self.filter_table: if table == self.filter_table:
match chain: match chain:
case "INPUT": return self.rules_chain_in case "INPUT":
case "OUTPUT": return self.rules_chain_out return self.rules_chain_in
case "FORWARD": return self.rules_chain_fwd case "OUTPUT":
return self.rules_chain_out
case "FORWARD":
return self.rules_chain_fwd
elif table == self.mangle_table: elif table == self.mangle_table:
match chain: match chain:
case "PREROUTING": return self.rules_chain_in case "PREROUTING":
case "POSTROUTING": return self.rules_chain_out return self.rules_chain_in
case "POSTROUTING":
return self.rules_chain_out
return None return None
def insert_firegex_chains(self): def insert_firegex_chains(self):
@@ -214,7 +221,8 @@ class FiregexTables(NFTableManager):
if r.get("family") == family and r.get("table") == table and r.get("chain") == chain and r.get("expr") == rule_to_add: if r.get("family") == family and r.get("table") == table and r.get("chain") == chain and r.get("expr") == rule_to_add:
found = True found = True
break break
if found: continue if found:
continue
yield { "add":{ "rule": { yield { "add":{ "rule": {
"family": family, "family": family,
"table": table, "table": table,
@@ -274,7 +282,7 @@ class FiregexTables(NFTableManager):
ip_filters.append({"match": { "op": "==", "left": { "meta": { "key": "oifname" } }, "right": srv.dst} }) ip_filters.append({"match": { "op": "==", "left": { "meta": { "key": "oifname" } }, "right": srv.dst} })
port_filters = [] port_filters = []
if not srv.proto in [Protocol.ANY, Protocol.BOTH]: if srv.proto not in [Protocol.ANY, Protocol.BOTH]:
if srv.port_src_from != 1 or srv.port_src_to != 65535: #Any Port if srv.port_src_from != 1 or srv.port_src_to != 65535: #Any Port
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '>=', 'right': int(srv.port_src_from)}}) port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '>=', 'right': int(srv.port_src_from)}})
port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '<=', 'right': int(srv.port_src_to)}}) port_filters.append({'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'op': '<=', 'right': int(srv.port_src_to)}})

View File

@@ -1,41 +1,58 @@
from modules.nfproxy.nftables import FiregexTables from modules.nfproxy.nftables import FiregexTables
from utils import run_func
from modules.nfproxy.models import Service, PyFilter from modules.nfproxy.models import Service, PyFilter
import os import os
import asyncio import asyncio
from utils import DEBUG
import traceback import traceback
from fastapi import HTTPException from fastapi import HTTPException
import time
from utils import run_func
from utils import DEBUG
nft = FiregexTables() nft = FiregexTables()
OUTSTREAM_BUFFER_SIZE = 1024*10
class FiregexInterceptor: class FiregexInterceptor:
def __init__(self): def __init__(self):
self.srv:Service self.srv:Service
self._stats_updater_cb:callable
self.filter_map_lock:asyncio.Lock self.filter_map_lock:asyncio.Lock
self.filter_map: dict[str, PyFilter] self.filter_map: dict[str, PyFilter]
self.pyfilters: set[PyFilter]
self.update_config_lock:asyncio.Lock self.update_config_lock:asyncio.Lock
self.process:asyncio.subprocess.Process self.process:asyncio.subprocess.Process
self.update_task: asyncio.Task self.update_task: asyncio.Task
self.server_task: asyncio.Task
self.sock_path: str
self.unix_sock: asyncio.Server
self.ack_arrived = False self.ack_arrived = False
self.ack_status = None self.ack_status = None
self.ack_fail_what = "" self.ack_fail_what = "Queue response timed-out"
self.ack_lock = asyncio.Lock() self.ack_lock = asyncio.Lock()
self.sock_reader:asyncio.StreamReader = None
async def _call_stats_updater_callback(self, filter: PyFilter): self.sock_writer:asyncio.StreamWriter = None
if self._stats_updater_cb: self.sock_conn_lock:asyncio.Lock
await run_func(self._stats_updater_cb(filter)) self.last_time_exception = 0
self.outstrem_function = None
self.expection_function = None
self.outstrem_task: asyncio.Task
self.outstrem_buffer = ""
@classmethod @classmethod
async def start(cls, srv: Service, stats_updater_cb:callable): async def start(cls, srv: Service, outstream_func=None, exception_func=None):
self = cls() self = cls()
self._stats_updater_cb = stats_updater_cb
self.srv = srv self.srv = srv
self.filter_map_lock = asyncio.Lock() self.filter_map_lock = asyncio.Lock()
self.update_config_lock = asyncio.Lock() self.update_config_lock = asyncio.Lock()
self.sock_conn_lock = asyncio.Lock()
self.outstrem_function = outstream_func
self.expection_function = exception_func
if not self.sock_conn_lock.locked():
await self.sock_conn_lock.acquire()
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
if os.path.exists(self.sock_path):
os.remove(self.sock_path)
self.unix_sock = await asyncio.start_unix_server(self._server_listener,path=self.sock_path)
self.server_task = asyncio.create_task(self.unix_sock.serve_forever())
queue_range = await self._start_binary() queue_range = await self._start_binary()
self.update_task = asyncio.create_task(self.update_stats()) self.update_task = asyncio.create_task(self.update_stats())
nft.add(self.srv, queue_range) nft.add(self.srv, queue_range)
@@ -43,19 +60,49 @@ class FiregexInterceptor:
await self.ack_lock.acquire() await self.ack_lock.acquire()
return self return self
async def _stream_handler(self):
while True:
try:
out_data = (await self.process.stdout.read(1024*10)).decode(errors="ignore")
if DEBUG:
print(out_data, end="")
except asyncio.exceptions.LimitOverrunError:
self.outstrem_buffer = ""
continue
except Exception as e:
self.ack_arrived = False
self.ack_status = False
self.ack_fail_what = "Can't read from nfq client"
self.ack_lock.release()
await self.stop()
traceback.print_exc() # Python can't print it alone? nope it's python... wasted 1 day :)
raise HTTPException(status_code=500, detail="Can't read from nfq client") from e
self.outstrem_buffer+=out_data
if len(self.outstrem_buffer) > OUTSTREAM_BUFFER_SIZE:
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
if self.outstrem_function:
await run_func(self.outstrem_function, self.srv.id, out_data)
async def _start_binary(self): async def _start_binary(self):
proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../cpproxy") proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
self.process = await asyncio.create_subprocess_exec( self.process = await asyncio.create_subprocess_exec(
proxy_binary_path, proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
env={"NTHREADS": os.getenv("NTHREADS","1")}, stderr=asyncio.subprocess.STDOUT,
env={
"NTHREADS": os.getenv("NTHREADS","1"),
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
"FIREGEX_NFPROXY_SOCK": self.sock_path
},
) )
line_fut = self.process.stdout.readuntil() self.outstrem_task = asyncio.create_task(self._stream_handler())
try: try:
line_fut = await asyncio.wait_for(line_fut, timeout=3) async with asyncio.timeout(3):
await self.sock_conn_lock.acquire()
line_fut = await self.sock_reader.readuntil()
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.process.kill() self.process.kill()
raise Exception("Invalid binary output") raise Exception("Binary don't returned queue number until timeout")
line = line_fut.decode() line = line_fut.decode()
if line.startswith("QUEUE "): if line.startswith("QUEUE "):
params = line.split() params = line.split()
@@ -64,24 +111,45 @@ class FiregexInterceptor:
self.process.kill() self.process.kill()
raise Exception("Invalid binary output") raise Exception("Invalid binary output")
async def _server_listener(self, reader:asyncio.StreamReader, writer:asyncio.StreamWriter):
if self.sock_reader or self.sock_writer:
writer.write_eof() # Technically never reached
writer.close()
reader.feed_eof()
return
self.sock_reader = reader
self.sock_writer = writer
self.sock_conn_lock.release()
async def update_stats(self): async def update_stats(self):
try: try:
while True: while True:
line = (await self.process.stdout.readuntil()).decode() try:
if DEBUG: line = (await self.sock_reader.readuntil()).decode()
print(line) except Exception as e:
self.ack_arrived = False
self.ack_status = False
self.ack_fail_what = "Can't read from nfq client"
self.ack_lock.release()
await self.stop()
raise HTTPException(status_code=500, detail="Can't read from nfq client") from e
if line.startswith("BLOCKED "): if line.startswith("BLOCKED "):
filter_id = line.split()[1] filter_name = line.split()[1]
print("BLOCKED", filter_name)
async with self.filter_map_lock: async with self.filter_map_lock:
if filter_id in self.filter_map: if filter_name in self.filter_map:
self.filter_map[filter_id].blocked_packets+=1 self.filter_map[filter_name].blocked_packets+=1
await self.filter_map[filter_id].update() await self.filter_map[filter_name].update()
if line.startswith("EDITED "): if line.startswith("MANGLED "):
filter_id = line.split()[1] filter_name = line.split()[1]
async with self.filter_map_lock: async with self.filter_map_lock:
if filter_id in self.filter_map: if filter_name in self.filter_map:
self.filter_map[filter_id].edited_packets+=1 self.filter_map[filter_name].edited_packets+=1
await self.filter_map[filter_id].update() await self.filter_map[filter_name].update()
if line.startswith("EXCEPTION"):
self.last_time_exception = int(time.time()*1000) #ms timestamp
if self.expection_function:
await run_func(self.expection_function, self.srv.id, self.last_time_exception)
if line.startswith("ACK "): if line.startswith("ACK "):
self.ack_arrived = True self.ack_arrived = True
self.ack_status = line.split()[1].upper() == "OK" self.ack_status = line.split()[1].upper() == "OK"
@@ -96,26 +164,44 @@ class FiregexInterceptor:
traceback.print_exc() traceback.print_exc()
async def stop(self): async def stop(self):
self.server_task.cancel()
self.update_task.cancel() self.update_task.cancel()
self.unix_sock.close()
self.outstrem_task.cancel()
if os.path.exists(self.sock_path):
os.remove(self.sock_path)
if self.process and self.process.returncode is None: if self.process and self.process.returncode is None:
self.process.kill() self.process.kill()
async def _update_config(self, filters_codes): async def _update_config(self, code):
async with self.update_config_lock: async with self.update_config_lock:
# TODO write compiled code correctly if self.sock_writer:
# self.process.stdin.write((" ".join(filters_codes)+"\n").encode()) self.sock_writer.write(len(code).to_bytes(4, byteorder='big')+code.encode())
await self.process.stdin.drain() await self.sock_writer.drain()
try: try:
async with asyncio.timeout(3): async with asyncio.timeout(3):
await self.ack_lock.acquire() await self.ack_lock.acquire()
except TimeoutError: except TimeoutError:
pass self.ack_fail_what = "Queue response timed-out"
if not self.ack_arrived or not self.ack_status: if not self.ack_arrived or not self.ack_status:
raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}") await self.stop()
raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}")
else:
raise HTTPException(status_code=400, detail="Socket not ready")
async def reload(self, filters:list[PyFilter]): async def reload(self, filters:list[PyFilter]):
async with self.filter_map_lock: async with self.filter_map_lock:
self.filter_map = self.compile_filters(filters) if os.path.exists(f"db/nfproxy_filters/{self.srv.id}.py"):
# TODO COMPILE CODE with open(f"db/nfproxy_filters/{self.srv.id}.py") as f:
#await self._update_config(filters_codes) TODO pass the compiled code filter_file = f.read()
else:
filter_file = ""
self.filter_map = {ele.name: ele for ele in filters}
await self._update_config(
filter_file + "\n\n" +
"__firegex_pyfilter_enabled = [" + ", ".join([repr(f.name) for f in filters]) + "]\n" +
"__firegex_proto = " + repr(self.srv.proto) + "\n" +
"import firegex.nfproxy.internals\n" +
"firegex.nfproxy.internals.compile(globals())\n"
)

View File

@@ -3,6 +3,7 @@ from modules.nfproxy.firegex import FiregexInterceptor
from modules.nfproxy.nftables import FiregexTables, FiregexFilter from modules.nfproxy.nftables import FiregexTables, FiregexFilter
from modules.nfproxy.models import Service, PyFilter from modules.nfproxy.models import Service, PyFilter
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils import run_func
class STATUS: class STATUS:
STOP = "stop" STOP = "stop"
@@ -11,22 +12,29 @@ class STATUS:
nft = FiregexTables() nft = FiregexTables()
class ServiceManager: class ServiceManager:
def __init__(self, srv: Service, db): def __init__(self, srv: Service, db, outstream_func=None, exception_func=None):
self.srv = srv self.srv = srv
self.db = db self.db = db
self.status = STATUS.STOP self.status = STATUS.STOP
self.filters: dict[int, FiregexFilter] = {} self.filters: dict[str, FiregexFilter] = {}
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self.interceptor = None self.interceptor = None
self.outstream_function = outstream_func
self.last_exception_time = 0
async def excep_internal_handler(srv, exc_time):
self.last_exception_time = exc_time
if exception_func:
await run_func(exception_func, srv, exc_time)
self.exception_function = excep_internal_handler
async def _update_filters_from_db(self): async def _update_filters_from_db(self):
pyfilters = [ pyfilters = [
PyFilter.from_dict(ele) for ele in PyFilter.from_dict(ele, self.db) for ele in
self.db.query("SELECT * FROM pyfilter WHERE service_id = ? AND active=1;", self.srv.id) self.db.query("SELECT * FROM pyfilter WHERE service_id = ? AND active=1;", self.srv.id)
] ]
#Filter check #Filter check
old_filters = set(self.filters.keys()) old_filters = set(self.filters.keys())
new_filters = set([f.id for f in pyfilters]) new_filters = set([f.name for f in pyfilters])
#remove old filters #remove old filters
for f in old_filters: for f in old_filters:
if f not in new_filters: if f not in new_filters:
@@ -34,7 +42,7 @@ class ServiceManager:
#add new filters #add new filters
for f in new_filters: for f in new_filters:
if f not in old_filters: if f not in old_filters:
self.filters[f] = [ele for ele in pyfilters if ele.id == f][0] self.filters[f] = [ele for ele in pyfilters if ele.name == f][0]
if self.interceptor: if self.interceptor:
await self.interceptor.reload(self.filters.values()) await self.interceptor.reload(self.filters.values())
@@ -43,24 +51,25 @@ class ServiceManager:
async def next(self,to): async def next(self,to):
async with self.lock: async with self.lock:
if (self.status, to) == (STATUS.ACTIVE, STATUS.STOP): if to == STATUS.STOP:
await self.stop() await self.stop()
self._set_status(to) if to == STATUS.ACTIVE:
# PAUSE -> ACTIVE
elif (self.status, to) == (STATUS.STOP, STATUS.ACTIVE):
await self.restart() await self.restart()
def _stats_updater(self,filter:PyFilter):
self.db.query("UPDATE pyfilter SET blocked_packets = ?, edited_packets = ? WHERE filter_id = ?;", filter.blocked_packets, filter.edited_packets, filter.id)
def _set_status(self,status): def _set_status(self,status):
self.status = status self.status = status
self.__update_status_db(status) self.__update_status_db(status)
def read_outstrem_buffer(self):
if self.interceptor:
return self.interceptor.outstrem_buffer
else:
return ""
async def start(self): async def start(self):
if not self.interceptor: if not self.interceptor:
nft.delete(self.srv) nft.delete(self.srv)
self.interceptor = await FiregexInterceptor.start(self.srv, self._stats_updater) self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function)
await self._update_filters_from_db() await self._update_filters_from_db()
self._set_status(STATUS.ACTIVE) self._set_status(STATUS.ACTIVE)
@@ -69,6 +78,7 @@ class ServiceManager:
if self.interceptor: if self.interceptor:
await self.interceptor.stop() await self.interceptor.stop()
self.interceptor = None self.interceptor = None
self._set_status(STATUS.STOP)
async def restart(self): async def restart(self):
await self.stop() await self.stop()
@@ -79,10 +89,12 @@ class ServiceManager:
await self._update_filters_from_db() await self._update_filters_from_db()
class FirewallManager: class FirewallManager:
def __init__(self, db:SQLite): def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
self.db = db self.db = db
self.service_table: dict[str, ServiceManager] = {} self.service_table: dict[str, ServiceManager] = {}
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self.outstream_function = outstream_func
self.exception_function = exception_func
async def close(self): async def close(self):
for key in list(self.service_table.keys()): for key in list(self.service_table.keys()):
@@ -104,7 +116,7 @@ class FirewallManager:
srv = Service.from_dict(srv) srv = Service.from_dict(srv)
if srv.id in self.service_table: if srv.id in self.service_table:
continue continue
self.service_table[srv.id] = ServiceManager(srv, self.db) self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
await self.service_table[srv.id].next(srv.status) await self.service_table[srv.id].next(srv.status)
def get(self,srv_id) -> ServiceManager: def get(self,srv_id) -> ServiceManager:

View File

@@ -1,12 +1,13 @@
class Service: class Service:
def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, **other): def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, fail_open: bool, **other):
self.id = service_id self.id = service_id
self.status = status self.status = status
self.port = port self.port = port
self.name = name self.name = name
self.proto = proto self.proto = proto
self.ip_int = ip_int self.ip_int = ip_int
self.fail_open = fail_open
@classmethod @classmethod
def from_dict(cls, var: dict): def from_dict(cls, var: dict):
@@ -14,13 +15,19 @@ class Service:
class PyFilter: class PyFilter:
def __init__(self, filter_id:int, name: str, blocked_packets: int, edited_packets: int, active: bool, **other): def __init__(self, name: str, blocked_packets: int, edited_packets: int, active: bool, db, **other):
self.id = filter_id
self.name = name self.name = name
self.blocked_packets = blocked_packets self.blocked_packets = blocked_packets
self.edited_packets = edited_packets self.edited_packets = edited_packets
self.active = active self.active = active
self.__db = db
async def update(self):
self.__db.query("UPDATE pyfilter SET blocked_packets = ?, edited_packets = ? WHERE name = ?;", self.blocked_packets, self.edited_packets, self.name)
def __repr__(self):
return f"<PyFilter {self.name}>"
@classmethod @classmethod
def from_dict(cls, var: dict): def from_dict(cls, var: dict, db):
return cls(**var) return cls(**var, db=db)

View File

@@ -1,6 +1,14 @@
from modules.nfproxy.models import Service from modules.nfproxy.models import Service
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
def convert_protocol_to_l4(proto:str):
if proto == "tcp":
return "tcp"
elif proto == "http":
return "tcp"
else:
raise Exception("Invalid protocol")
class FiregexFilter: class FiregexFilter:
def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int): def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int):
self.id = id self.id = id
@@ -11,7 +19,7 @@ class FiregexFilter:
def __eq__(self, o: object) -> bool: def __eq__(self, o: object) -> bool:
if isinstance(o, FiregexFilter) or isinstance(o, Service): if isinstance(o, FiregexFilter) or isinstance(o, Service):
return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) return self.port == o.port and self.proto == convert_protocol_to_l4(o.proto) and ip_parse(self.ip_int) == ip_parse(o.ip_int)
return False return False
class FiregexTables(NFTableManager): class FiregexTables(NFTableManager):
@@ -20,22 +28,22 @@ class FiregexTables(NFTableManager):
def __init__(self): def __init__(self):
super().__init__([ super().__init__([
{"add":{"chain":{ {"add":{"chain":{ #Input chain attached before conntrack see it
"family":"inet", "family":"inet",
"table":self.table_name, "table":self.table_name,
"name":self.input_chain, "name":self.input_chain,
"type":"filter", "type":"filter",
"hook":"prerouting", "hook":"prerouting",
"prio":-150, "prio":-310,
"policy":"accept" "policy":"accept"
}}}, }}},
{"add":{"chain":{ {"add":{"chain":{ #Output chain attached after conntrack saw it
"family":"inet", "family":"inet",
"table":self.table_name, "table":self.table_name,
"name":self.output_chain, "name":self.output_chain,
"type":"filter", "type":"filter",
"hook":"postrouting", "hook":"postrouting",
"prio":-150, "prio":-310,
"policy":"accept" "policy":"accept"
}}} }}}
],[ ],[
@@ -61,7 +69,7 @@ class FiregexTables(NFTableManager):
"chain": self.output_chain, "chain": self.output_chain,
"expr": [ "expr": [
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}}, {'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "sport"}}, "op": "==", "right": int(srv.port)}},
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}}, {"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
] ]
@@ -72,7 +80,7 @@ class FiregexTables(NFTableManager):
"chain": self.input_chain, "chain": self.input_chain,
"expr": [ "expr": [
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}}, {'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "dport"}}, "op": "==", "right": int(srv.port)}},
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}}, {"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
] ]

View File

@@ -79,7 +79,7 @@ class FiregexInterceptor:
self.update_task: asyncio.Task self.update_task: asyncio.Task
self.ack_arrived = False self.ack_arrived = False
self.ack_status = None self.ack_status = None
self.ack_fail_what = "" self.ack_fail_what = "Queue response timed-out"
self.ack_lock = asyncio.Lock() self.ack_lock = asyncio.Lock()
@classmethod @classmethod
@@ -158,8 +158,9 @@ class FiregexInterceptor:
async with asyncio.timeout(3): async with asyncio.timeout(3):
await self.ack_lock.acquire() await self.ack_lock.acquire()
except TimeoutError: except TimeoutError:
pass self.ack_fail_what = "Queue response timed-out"
if not self.ack_arrived or not self.ack_status: if not self.ack_arrived or not self.ack_status:
await self.stop()
raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}") raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}")

View File

@@ -45,11 +45,9 @@ class ServiceManager:
async def next(self,to): async def next(self,to):
async with self.lock: async with self.lock:
if (self.status, to) == (STATUS.ACTIVE, STATUS.STOP): if to == STATUS.STOP:
await self.stop() await self.stop()
self._set_status(to) if to == STATUS.ACTIVE:
# PAUSE -> ACTIVE
elif (self.status, to) == (STATUS.STOP, STATUS.ACTIVE):
await self.restart() await self.restart()
def _stats_updater(self,filter:RegexFilter): def _stats_updater(self,filter:RegexFilter):
@@ -71,6 +69,7 @@ class ServiceManager:
if self.interceptor: if self.interceptor:
await self.interceptor.stop() await self.interceptor.stop()
self.interceptor = None self.interceptor = None
self._set_status(STATUS.STOP)
async def restart(self): async def restart(self):
await self.stop() await self.stop()

View File

@@ -26,7 +26,7 @@ class FiregexTables(NFTableManager):
"name":self.input_chain, "name":self.input_chain,
"type":"filter", "type":"filter",
"hook":"prerouting", "hook":"prerouting",
"prio":-150, "prio":-301,
"policy":"accept" "policy":"accept"
}}}, }}},
{"add":{"chain":{ {"add":{"chain":{
@@ -35,7 +35,7 @@ class FiregexTables(NFTableManager):
"name":self.output_chain, "name":self.output_chain,
"type":"filter", "type":"filter",
"hook":"postrouting", "hook":"postrouting",
"prio":-150, "prio":-301,
"policy":"accept" "policy":"accept"
}}} }}}
],[ ],[

View File

@@ -28,7 +28,7 @@ class FiregexTables(NFTableManager):
"name":self.prerouting_porthijack, "name":self.prerouting_porthijack,
"type":"filter", "type":"filter",
"hook":"prerouting", "hook":"prerouting",
"prio":-300, "prio":-310,
"policy":"accept" "policy":"accept"
}}}, }}},
{"add":{"chain":{ {"add":{"chain":{
@@ -37,7 +37,7 @@ class FiregexTables(NFTableManager):
"name":self.postrouting_porthijack, "name":self.postrouting_porthijack,
"type":"filter", "type":"filter",
"hook":"postrouting", "hook":"postrouting",
"prio":-300, "prio":-310,
"policy":"accept" "policy":"accept"
}}} }}}
],[ ],[

View File

@@ -7,6 +7,14 @@ from modules.nfproxy.firewall import STATUS, FirewallManager
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 utils.models import ResetRequest, StatusMessageModel from utils.models import ResetRequest, StatusMessageModel
import os
from firegex.nfproxy.internals import get_filter_names
from fastapi.responses import PlainTextResponse
from modules.nfproxy.nftables import convert_protocol_to_l4
import asyncio
import traceback
from utils import DEBUG
import utils
class ServiceModel(BaseModel): class ServiceModel(BaseModel):
service_id: str service_id: str
@@ -18,12 +26,17 @@ class ServiceModel(BaseModel):
n_filters: int n_filters: int
edited_packets: int edited_packets: int
blocked_packets: int blocked_packets: int
fail_open: bool
class RenameForm(BaseModel): class RenameForm(BaseModel):
name:str name:str
class SettingsForm(BaseModel):
port: PortType|None = None
ip_int: str|None = None
fail_open: bool|None = None
class PyFilterModel(BaseModel): class PyFilterModel(BaseModel):
filter_id: int
name: str name: str
blocked_packets: int blocked_packets: int
edited_packets: int edited_packets: int
@@ -34,12 +47,17 @@ class ServiceAddForm(BaseModel):
port: PortType port: PortType
proto: str proto: str
ip_int: str ip_int: str
fail_open: bool = True
class ServiceAddResponse(BaseModel): class ServiceAddResponse(BaseModel):
status:str status:str
service_id: str|None = None service_id: str|None = None
#app = APIRouter() Not released in this version class SetPyFilterForm(BaseModel):
code: str
sid: str|None = None
app = APIRouter()
db = SQLite('db/nft-pyfilters.db', { db = SQLite('db/nft-pyfilters.db', {
'services': { 'services': {
@@ -48,11 +66,12 @@ db = SQLite('db/nft-pyfilters.db', {
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)', 'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
'name': 'VARCHAR(100) NOT NULL UNIQUE', 'name': 'VARCHAR(100) NOT NULL UNIQUE',
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))', 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))',
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
'ip_int': 'VARCHAR(100) NOT NULL', 'ip_int': 'VARCHAR(100) NOT NULL',
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
}, },
'pyfilter': { 'pyfilter': {
'filter_id': 'INTEGER PRIMARY KEY', 'name': 'VARCHAR(100) PRIMARY KEY',
'name': 'VARCHAR(100) NOT NULL',
'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0',
'edited_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', 'edited_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0',
'service_id': 'VARCHAR(100) NOT NULL', 'service_id': 'VARCHAR(100) NOT NULL',
@@ -60,8 +79,8 @@ db = SQLite('db/nft-pyfilters.db', {
'FOREIGN KEY (service_id)':'REFERENCES services (service_id)', 'FOREIGN KEY (service_id)':'REFERENCES services (service_id)',
}, },
'QUERY':[ 'QUERY':[
"CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (port, ip_int, proto);", "CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (port, ip_int, l4_proto);",
"CREATE UNIQUE INDEX IF NOT EXISTS unique_pyfilter_service ON pyfilter (name, service_id);" "CREATE UNIQUE INDEX IF NOT EXISTS unique_pyfilter_service ON pyfilter (name, service_id);"
] ]
}) })
@@ -89,6 +108,10 @@ async def startup():
await firewall.init() await firewall.init()
except Exception as e: except Exception as e:
print("WARNING cannot start firewall:", e) print("WARNING cannot start firewall:", e)
utils.socketio.on("nfproxy-outstream-join", join_outstream)
utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
utils.socketio.on("nfproxy-exception-join", join_exception)
utils.socketio.on("nfproxy-exception-leave", leave_exception)
async def shutdown(): async def shutdown():
db.backup() db.backup()
@@ -103,7 +126,13 @@ def gen_service_id():
break break
return res return res
firewall = FirewallManager(db) async def outstream_func(service_id, data):
await utils.socketio.emit(f"nfproxy-outstream-{service_id}", data, room=f"nfproxy-outstream-{service_id}")
async def exception_func(service_id, timestamp):
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func)
@app.get('/services', response_model=list[ServiceModel]) @app.get('/services', response_model=list[ServiceModel])
async def get_service_list(): async def get_service_list():
@@ -116,7 +145,8 @@ async def get_service_list():
s.name name, s.name name,
s.proto proto, s.proto proto,
s.ip_int ip_int, s.ip_int ip_int,
COUNT(f.filter_id) n_filters, s.fail_open fail_open,
COUNT(f.name) n_filters,
COALESCE(SUM(f.blocked_packets),0) blocked_packets, COALESCE(SUM(f.blocked_packets),0) blocked_packets,
COALESCE(SUM(f.edited_packets),0) edited_packets COALESCE(SUM(f.edited_packets),0) edited_packets
FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id
@@ -134,7 +164,8 @@ async def get_service_by_id(service_id: str):
s.name name, s.name name,
s.proto proto, s.proto proto,
s.ip_int ip_int, s.ip_int ip_int,
COUNT(f.filter_id) n_filters, s.fail_open fail_open,
COUNT(f.name) n_filters,
COALESCE(SUM(f.blocked_packets),0) blocked_packets, COALESCE(SUM(f.blocked_packets),0) blocked_packets,
COALESCE(SUM(f.edited_packets),0) edited_packets COALESCE(SUM(f.edited_packets),0) edited_packets
FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id
@@ -163,6 +194,8 @@ async def service_delete(service_id: str):
"""Request the deletion of a specific service""" """Request the deletion of a specific service"""
db.query('DELETE FROM services WHERE service_id = ?;', service_id) db.query('DELETE FROM services WHERE service_id = ?;', service_id)
db.query('DELETE FROM pyfilter WHERE service_id = ?;', service_id) db.query('DELETE FROM pyfilter WHERE service_id = ?;', service_id)
if os.path.exists(f"db/nfproxy_filters/{service_id}.py"):
os.remove(f"db/nfproxy_filters/{service_id}.py")
await firewall.remove(service_id) await firewall.remove(service_id)
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@@ -180,6 +213,42 @@ async def service_rename(service_id: str, form: RenameForm):
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@app.put('/services/{service_id}/settings', response_model=StatusMessageModel)
async def service_settings(service_id: str, form: SettingsForm):
"""Request to change the settings of a specific service (will cause a restart)"""
if form.port is not None and (form.port < 1 or form.port > 65535):
raise HTTPException(status_code=400, detail="Invalid port")
if form.ip_int is not None:
try:
form.ip_int = ip_parse(form.ip_int)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid address")
keys = []
values = []
for key, value in form.model_dump(exclude_none=True).items():
keys.append(key)
values.append(value)
if len(keys) == 0:
raise HTTPException(status_code=400, detail="No settings to change provided")
try:
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
except sqlite3.IntegrityError:
raise HTTPException(status_code=400, detail="A service with these settings already exists")
old_status = firewall.get(service_id).status
await firewall.remove(service_id)
await firewall.reload()
await firewall.get(service_id).next(old_status)
await refresh_frontend()
return {'status': 'ok'}
@app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel]) @app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel])
async def get_service_pyfilter_list(service_id: str): async def get_service_pyfilter_list(service_id: str):
"""Get the list of the pyfilters of a service""" """Get the list of the pyfilters of a service"""
@@ -187,49 +256,38 @@ async def get_service_pyfilter_list(service_id: str):
raise HTTPException(status_code=400, detail="This service does not exists!") raise HTTPException(status_code=400, detail="This service does not exists!")
return db.query(""" return db.query("""
SELECT SELECT
filter_id, name, blocked_packets, edited_packets, active name, blocked_packets, edited_packets, active
FROM pyfilter WHERE service_id = ?; FROM pyfilter WHERE service_id = ?;
""", service_id) """, service_id)
@app.get('/pyfilters/{filter_id}', response_model=PyFilterModel) @app.get('/pyfilters/{filter_name}', response_model=PyFilterModel)
async def get_pyfilter_by_id(filter_id: int): async def get_pyfilter_by_id(filter_name: str):
"""Get pyfilter info using his id""" """Get pyfilter info using his id"""
res = db.query(""" res = db.query("""
SELECT SELECT
filter_id, name, blocked_packets, edited_packets, active name, blocked_packets, edited_packets, active
FROM pyfilter WHERE filter_id = ?; FROM pyfilter WHERE name = ?;
""", filter_id) """, filter_name)
if len(res) == 0: if len(res) == 0:
raise HTTPException(status_code=400, detail="This filter does not exists!") raise HTTPException(status_code=400, detail="This filter does not exists!")
return res[0] return res[0]
@app.delete('/pyfilters/{filter_id}', response_model=StatusMessageModel) @app.post('/pyfilters/{filter_name}/enable', response_model=StatusMessageModel)
async def pyfilter_delete(filter_id: int): async def pyfilter_enable(filter_name: str):
"""Delete a pyfilter using his id"""
res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id)
if len(res) != 0:
db.query('DELETE FROM pyfilter WHERE filter_id = ?;', filter_id)
await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend()
return {'status': 'ok'}
@app.post('/pyfilters/{filter_id}/enable', response_model=StatusMessageModel)
async def pyfilter_enable(filter_id: int):
"""Request the enabling of a pyfilter""" """Request the enabling of a pyfilter"""
res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) res = db.query('SELECT * FROM pyfilter WHERE name = ?;', filter_name)
if len(res) != 0: if len(res) != 0:
db.query('UPDATE pyfilter SET active=1 WHERE filter_id = ?;', filter_id) db.query('UPDATE pyfilter SET active=1 WHERE name = ?;', filter_name)
await firewall.get(res[0]["service_id"]).update_filters() await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@app.post('/pyfilters/{filter_id}/disable', response_model=StatusMessageModel) @app.post('/pyfilters/{filter_name}/disable', response_model=StatusMessageModel)
async def pyfilter_disable(filter_id: int): async def pyfilter_disable(filter_name: str):
"""Request the deactivation of a pyfilter""" """Request the deactivation of a pyfilter"""
res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) res = db.query('SELECT * FROM pyfilter WHERE name = ?;', filter_name)
if len(res) != 0: if len(res) != 0:
db.query('UPDATE pyfilter SET active=0 WHERE filter_id = ?;', filter_id) db.query('UPDATE pyfilter SET active=0 WHERE name = ?;', filter_name)
await firewall.get(res[0]["service_id"]).update_filters() await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@@ -246,14 +304,95 @@ async def add_new_service(form: ServiceAddForm):
srv_id = None srv_id = None
try: try:
srv_id = gen_service_id() srv_id = gen_service_id()
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?)", db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open, l4_proto) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int) srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open, convert_protocol_to_l4(form.proto))
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
raise HTTPException(status_code=400, detail="This type of service already exists") raise HTTPException(status_code=400, detail="This type of service already exists")
await firewall.reload() await firewall.reload()
await refresh_frontend() await refresh_frontend()
return {'status': 'ok', 'service_id': srv_id} return {'status': 'ok', 'service_id': srv_id}
#TODO check all the APIs and add @app.put('/services/{service_id}/pyfilters/code', response_model=StatusMessageModel)
# 1. API to change the python filter file async def set_pyfilters(service_id: str, form: SetPyFilterForm):
# 2. a socketio mechanism to lock the previous feature """Set the python filter for a service"""
service = db.query("SELECT service_id, proto FROM services WHERE service_id = ?;", service_id)
if len(service) == 0:
raise HTTPException(status_code=400, detail="This service does not exists!")
service = service[0]
service_id = service["service_id"]
srv_proto = service["proto"]
try:
async with asyncio.timeout(8):
try:
found_filters = get_filter_names(form.code, srv_proto)
except Exception as e:
if DEBUG:
traceback.print_exc()
raise HTTPException(status_code=400, detail="Compile error: "+str(e))
# Remove filters that are not in the new code
existing_filters = db.query("SELECT name FROM pyfilter WHERE service_id = ?;", service_id)
existing_filters = [ele["name"] for ele in existing_filters]
for filter in existing_filters:
if filter not in found_filters:
db.query("DELETE FROM pyfilter WHERE name = ?;", filter)
# Add filters that are in the new code but not in the database
for filter in found_filters:
if not db.query("SELECT 1 FROM pyfilter WHERE service_id = ? AND name = ?;", service_id, filter):
db.query("INSERT INTO pyfilter (name, service_id) VALUES (?, ?);", filter, service["service_id"])
# Eventually edited filters will be reloaded
os.makedirs("db/nfproxy_filters", exist_ok=True)
with open(f"db/nfproxy_filters/{service_id}.py", "w") as f:
f.write(form.code)
await firewall.get(service_id).update_filters()
await refresh_frontend()
except asyncio.TimeoutError:
if DEBUG:
traceback.print_exc()
raise HTTPException(status_code=400, detail="The operation took too long")
return {'status': 'ok'}
@app.get('/services/{service_id}/pyfilters/code', response_class=PlainTextResponse)
async def get_pyfilters(service_id: str):
"""Get the python filter for a service"""
if not db.query("SELECT 1 FROM services s WHERE s.service_id = ?;", service_id):
raise HTTPException(status_code=400, detail="This service does not exists!")
try:
with open(f"db/nfproxy_filters/{service_id}.py") as f:
return f.read()
except FileNotFoundError:
return ""
#Socket io events
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)
async def leave_outstream(sid, data):
"""Client leaves a room."""
srv = data.get("service")
if srv:
await utils.socketio.leave_room(sid, f"nfproxy-outstream-{srv}")
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)
async def leave_exception(sid, data):
"""Client leaves a room."""
srv = data.get("service")
if srv:
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")

View File

@@ -8,15 +8,22 @@ import nftables
from socketio import AsyncServer from socketio import AsyncServer
from fastapi import Path from fastapi import Path
from typing import Annotated from typing import Annotated
from functools import wraps
from pydantic import BaseModel, ValidationError
import traceback
from utils.models import StatusMessageModel
from typing import List
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
socketio:AsyncServer = None socketio:AsyncServer = None
sid_list:set = set()
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers") ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
ON_DOCKER = "DOCKER" in sys.argv ON_DOCKER = "DOCKER" in sys.argv
DEBUG = "DEBUG" in sys.argv DEBUG = "DEBUG" in sys.argv
NORELOAD = "NORELOAD" in sys.argv
FIREGEX_PORT = int(os.getenv("PORT","4444")) FIREGEX_PORT = int(os.getenv("PORT","4444"))
JWT_ALGORITHM: str = "HS256" JWT_ALGORITHM: str = "HS256"
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
@@ -153,4 +160,50 @@ class NFTableManager(Singleton):
def raw_list(self): def raw_list(self):
return self.cmd({"list": {"ruleset": None}})["nftables"] return self.cmd({"list": {"ruleset": None}})["nftables"]
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
res = obj.model_dump(mode=mode, exclude_unset=not unset)
if convert_keys:
for from_k, to_k in convert_keys.items():
if from_k in res:
res[to_k] = res.pop(from_k)
if exclude:
for ele in exclude:
if ele in res:
del res[ele]
return res
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
if isinstance(obj, list):
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
def decorator(func):
@sio_server.on(event_name) # Automatically registers the event
@wraps(func)
async def wrapper(sid, data):
try:
# Parse and validate incoming data
parsed_data = model.model_validate(data)
except ValidationError:
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
# Call the original function with the parsed data
result = await func(sid, parsed_data)
# If a response model is provided, validate the output
if response_model:
try:
parsed_result = response_model.model_validate(result)
except ValidationError:
traceback.print_exc()
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
else:
parsed_result = result
# Emit the validated result
if parsed_result:
if isinstance(parsed_result, BaseModel):
return json_like(parsed_result)
return parsed_result
return wrapper
return decorator

View File

@@ -7,6 +7,8 @@ from starlette.responses import StreamingResponse
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func
from utils.models import ResetRequest from utils.models import ResetRequest
import asyncio
import traceback
REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/" REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/"
REACT_HTML_PATH: str = os.path.join(REACT_BUILD_DIR,"index.html") REACT_HTML_PATH: str = os.path.join(REACT_BUILD_DIR,"index.html")
@@ -69,6 +71,7 @@ def get_router_modules():
name=route name=route
)) ))
except Exception as e: except Exception as e:
traceback.print_exc()
print(f"Router {route} failed to load: {e}") print(f"Router {route} failed to load: {e}")
return res return res
@@ -87,12 +90,9 @@ def load_routers(app):
if router.shutdown: if router.shutdown:
shutdowns.append(router.shutdown) shutdowns.append(router.shutdown)
async def reset(reset_option:ResetRequest): async def reset(reset_option:ResetRequest):
for func in resets: await asyncio.gather(*[run_func(func, reset_option) for func in resets])
await run_func(func, reset_option)
async def startup(): async def startup():
for func in startups: await asyncio.gather(*[run_func(func) for func in startups])
await run_func(func)
async def shutdown(): async def shutdown():
for func in shutdowns: await asyncio.gather(*[run_func(func) for func in shutdowns])
await run_func(func)
return reset, startup, shutdown return reset, startup, shutdown

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 327 KiB

View File

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# TODO implement cli start function
from firegex.cli import run from firegex.cli import run
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -5,11 +5,11 @@ with open("README.md", "r", encoding="utf-8") as fh:
setuptools.setup( setuptools.setup(
name="fgex", name="fgex",
version="0.0.0", version="0.0.1",
author="Pwnzer0tt1", author="Pwnzer0tt1",
author_email="pwnzer0tt1@poliba.it", author_email="pwnzer0tt1@poliba.it",
py_modules=["fgex"], py_modules=["fgex"],
install_requires=["fgex"], install_requires=["firegex"],
include_package_data=True, include_package_data=True,
description="Firegex client", description="Firegex client",
long_description=long_description, long_description=long_description,

View File

@@ -2,6 +2,4 @@
__version__ = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" __version__ = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
#Exported functions #Exported functions
__all__ = [ __all__ = []
]

1
proxy-client/fgex → fgex-lib/firegex/__main__.py Executable file → Normal file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# TODO implement cli start function
from firegex.cli import run from firegex.cli import run
if __name__ == "__main__": if __name__ == "__main__":

76
fgex-lib/firegex/cli.py Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
import typer
from rich import print
from rich.markup import escape
from typer import Exit
from firegex import __version__
from firegex.nfproxy.proxysim import run_proxy_simulation
from firegex.nfproxy.models import Protocols
import os
import socket
app = typer.Typer(
no_args_is_help=True,
context_settings={"help_option_names": ["-h", "--help"]}
)
def close_cli(code:int=1):
raise Exit(code)
DEV_MODE = __version__ == "0.0.0"
def test_connection(host, port, use_ipv6=False):
family = socket.AF_INET6 if use_ipv6 else socket.AF_INET
sock = socket.socket(family, socket.SOCK_STREAM)
try:
sock.settimeout(3)
sock.connect((host, port))
return True
except Exception:
return False
finally:
sock.close()
@app.command(help="Run an nfproxy simulation")
def nfproxy(
filter_file: str = typer.Argument(..., help="The path to the filter file"),
address: str = typer.Argument(..., help="The address of the target to proxy"),
port: int = typer.Argument(..., help="The port of the target to proxy"),
proto: Protocols = typer.Option(Protocols.TCP.value, help="The protocol to proxy"),
from_address: str = typer.Option(None, help="The address of the local server"),
from_port: int = typer.Option(7474, help="The port of the local server"),
ipv6: bool = typer.Option(False, "-6", help="Use IPv6 for the connection"),
):
if from_address is None:
from_address = "::1" if ipv6 else "127.0.0.1"
if not os.path.isfile(filter_file):
print(f"[bold red]'{escape(os.path.abspath(filter_file))}' not found[/]")
close_cli()
if not test_connection(address, port, ipv6):
print(f"[bold red]Can't connect to {escape(address)}:{port}[/]")
close_cli()
run_proxy_simulation(filter_file, proto.value, address, port, from_address, from_port, ipv6)
def version_callback(verison: bool):
if verison:
print(__version__, "Development Mode" if DEV_MODE else "Release")
raise typer.Exit()
@app.callback()
def main(
verison: bool = typer.Option(False, "--version", "-v", help="Show the version of the client", callback=version_callback),
):
pass
def run():
try:
app()
except KeyboardInterrupt:
print("[bold yellow]Operation cancelled[/]")
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,39 @@
import functools
from firegex.nfproxy.models import RawPacket, TCPInputStream, TCPOutputStream, TCPClientStream, TCPServerStream
from firegex.nfproxy.internals.models import Action, FullStreamAction
ACCEPT = Action.ACCEPT
DROP = Action.DROP
REJECT = Action.REJECT
UNSTABLE_MANGLE = Action.MANGLE
def pyfilter(func):
"""
Decorator to mark functions that will be used in the proxy.
Stores the function reference in a global registry.
"""
if not hasattr(pyfilter, "registry"):
pyfilter.registry = set()
pyfilter.registry.add(func.__name__)
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def get_pyfilters():
"""Returns the list of functions marked with @pyfilter."""
return list(pyfilter.registry)
def clear_pyfilter_registry():
"""Clears the pyfilter registry."""
if hasattr(pyfilter, "registry"):
pyfilter.registry.clear()
__all__ = [
"ACCEPT", "DROP", "REJECT", "UNSTABLE_MANGLE"
"Action", "FullStreamAction", "pyfilter",
"RawPacket", "TCPInputStream", "TCPOutputStream", "TCPClientStream", "TCPServerStream"
]

View File

@@ -0,0 +1,160 @@
from inspect import signature
from firegex.nfproxy.internals.models import Action, FullStreamAction
from firegex.nfproxy.internals.models import FilterHandler, PacketHandlerResult
import functools
from firegex.nfproxy.internals.data import DataStreamCtx
from firegex.nfproxy.internals.exceptions import NotReadyToRun, StreamFullReject, DropPacket, RejectConnection, StreamFullDrop
from firegex.nfproxy.internals.data import RawPacket
def context_call(glob, func, *args, **kargs):
glob["__firegex_tmp_args"] = args
glob["__firegex_tmp_kargs"] = kargs
glob["__firege_tmp_call"] = func
res = eval("__firege_tmp_call(*__firegex_tmp_args, **__firegex_tmp_kargs)", glob, glob)
if "__firegex_tmp_args" in glob.keys():
del glob["__firegex_tmp_args"]
if "__firegex_tmp_kargs" in glob.keys():
del glob["__firegex_tmp_kargs"]
if "__firege_tmp_call" in glob.keys():
del glob["__firege_tmp_call"]
return res
def generate_filter_structure(filters: list[str], proto:str, glob:dict) -> list[FilterHandler]:
from firegex.nfproxy.models import type_annotations_associations
if proto not in type_annotations_associations.keys():
raise Exception("Invalid protocol")
res = []
valid_annotation_type = type_annotations_associations[proto]
def add_func_to_list(func):
if not callable(func):
raise Exception(f"{func} is not a function")
sig = signature(func)
params_function = {}
for k, v in sig.parameters.items():
if v.annotation in valid_annotation_type.keys():
params_function[v.annotation] = valid_annotation_type[v.annotation]
else:
raise Exception(f"Invalid type annotation {v.annotation} for function {func.__name__}")
res.append(
FilterHandler(
func=func,
name=func.__name__,
params=params_function,
proto=proto
)
)
for filter in filters:
if not isinstance(filter, str):
raise Exception("Invalid filter list: must be a list of strings")
if filter in glob.keys():
add_func_to_list(glob[filter])
else:
raise Exception(f"Filter {filter} not found")
return res
def get_filters_info(code:str, proto:str) -> list[FilterHandler]:
glob = {}
exec("import firegex.nfproxy", glob, glob)
exec("firegex.nfproxy.clear_pyfilter_registry()", glob, glob)
exec(code, glob, glob)
filters = eval("firegex.nfproxy.get_pyfilters()", glob, glob)
try:
return generate_filter_structure(filters, proto, glob)
finally:
exec("firegex.nfproxy.clear_pyfilter_registry()", glob, glob)
def get_filter_names(code:str, proto:str) -> list[str]:
return [ele.name for ele in get_filters_info(code, proto)]
def handle_packet(glob: dict) -> None:
internal_data = DataStreamCtx(glob)
cache_call = {} # Cache of the data handler calls
cache_call[RawPacket] = internal_data.current_pkt
final_result = Action.ACCEPT
result = PacketHandlerResult(glob)
func_name = None
mangled_packet = None
for filter in internal_data.filter_call_info:
final_params = []
skip_call = False
for data_type, data_func in filter.params.items():
if data_type not in cache_call.keys():
try:
cache_call[data_type] = data_func(internal_data)
except NotReadyToRun:
cache_call[data_type] = None
skip_call = True
break
except StreamFullDrop:
result.action = Action.DROP
result.matched_by = "@MAX_STREAM_SIZE_REACHED"
return result.set_result()
except StreamFullReject:
result.action = Action.REJECT
result.matched_by = "@MAX_STREAM_SIZE_REACHED"
return result.set_result()
except DropPacket:
result.action = Action.DROP
result.matched_by = filter.name
return result.set_result()
except RejectConnection:
result.action = Action.REJECT
result.matched_by = filter.name
return result.set_result()
if cache_call[data_type] is None:
skip_call = True
break
final_params.append(cache_call[data_type])
if skip_call:
continue
res = context_call(glob, filter.func, *final_params)
if res is None:
continue #ACCEPTED
if not isinstance(res, Action):
raise Exception(f"Invalid return type {type(res)} for function {filter.name}")
if res == Action.MANGLE:
mangled_packet = internal_data.current_pkt.raw_packet
if res != Action.ACCEPT:
func_name = filter.name
final_result = res
break
result.action = final_result
result.matched_by = func_name
result.mangled_packet = mangled_packet
return result.set_result()
def compile(glob:dict) -> None:
internal_data = DataStreamCtx(glob, init_pkt=False)
glob["print"] = functools.partial(print, flush = True)
filters = glob["__firegex_pyfilter_enabled"]
proto = glob["__firegex_proto"]
internal_data.filter_call_info = generate_filter_structure(filters, proto, glob)
if "FGEX_STREAM_MAX_SIZE" in glob and int(glob["FGEX_STREAM_MAX_SIZE"]) > 0:
internal_data.stream_max_size = int(glob["FGEX_STREAM_MAX_SIZE"])
else:
internal_data.stream_max_size = 1*8e20 # 1MB default value
if "FGEX_FULL_STREAM_ACTION" in glob and isinstance(glob["FGEX_FULL_STREAM_ACTION"], FullStreamAction):
internal_data.full_stream_action = glob["FGEX_FULL_STREAM_ACTION"]
else:
internal_data.full_stream_action = FullStreamAction.FLUSH
PacketHandlerResult(glob).reset_result()

View File

@@ -0,0 +1,142 @@
from firegex.nfproxy.internals.models import FilterHandler
from firegex.nfproxy.internals.models import FullStreamAction
class RawPacket:
"""
class rapresentation of the nfqueue packet sent in this context by the c++ core
"""
def __init__(self,
data: bytes,
raw_packet: bytes,
is_input: bool,
is_ipv6: bool,
is_tcp: bool,
l4_size: int,
):
self.__data = bytes(data)
self.__raw_packet = bytes(raw_packet)
self.__is_input = bool(is_input)
self.__is_ipv6 = bool(is_ipv6)
self.__is_tcp = bool(is_tcp)
self.__l4_size = int(l4_size)
self.__raw_packet_header_size = len(self.__raw_packet)-self.__l4_size
@property
def is_input(self) -> bool:
return self.__is_input
@property
def is_ipv6(self) -> bool:
return self.__is_ipv6
@property
def is_tcp(self) -> bool:
return self.__is_tcp
@property
def data(self) -> bytes:
return self.__data
@property
def l4_size(self) -> int:
return self.__l4_size
@property
def raw_packet_header_len(self) -> int:
return self.__raw_packet_header_size
@property
def l4_data(self) -> bytes:
return self.__raw_packet[self.raw_packet_header_len:]
@l4_data.setter
def l4_data(self, v:bytes):
if not isinstance(v, bytes):
raise Exception("Invalid data type, data MUST be of type bytes")
#if len(v) != self.__l4_size:
# raise Exception("Invalid data size, must be equal to the original packet header size (due to a technical limitation)")
self.raw_packet = self.__raw_packet[:self.raw_packet_header_len]+v
@property
def raw_packet(self) -> bytes:
return self.__raw_packet
@raw_packet.setter
def raw_packet(self, v:bytes):
if not isinstance(v, bytes):
raise Exception("Invalid data type, data MUST be of type bytes")
if len(v) > 2**16:
raise Exception("Invalid data size, must be less than 2^16 bytes")
#if len(v) != len(self.__raw_packet):
# raise Exception("Invalid data size, must be equal to the original packet size (due to a technical limitation)")
if len(v) < self.raw_packet_header_len:
raise Exception("Invalid data size, must be greater than the original packet header size")
self.__raw_packet = v
self.__l4_size = len(v)-self.raw_packet_header_len
@classmethod
def _fetch_packet(cls, internal_data:"DataStreamCtx"):
if not isinstance(internal_data, DataStreamCtx):
if isinstance(internal_data, dict):
internal_data = DataStreamCtx(internal_data)
else:
raise Exception("Invalid data type, data MUST be of type DataStream, or glob dict")
if "__firegex_packet_info" not in internal_data.filter_glob.keys():
raise Exception("Packet info not found")
return cls(**internal_data.filter_glob["__firegex_packet_info"])
def __repr__(self):
return f"RawPacket(data={self.data}, raw_packet={self.raw_packet}, is_input={self.is_input}, is_ipv6={self.is_ipv6}, is_tcp={self.is_tcp}, l4_size={self.l4_size})"
class DataStreamCtx:
def __init__(self, glob: dict, init_pkt: bool = True):
if "__firegex_pyfilter_ctx" not in glob.keys():
glob["__firegex_pyfilter_ctx"] = {}
self.__data = glob["__firegex_pyfilter_ctx"]
self.filter_glob = glob
self.current_pkt = RawPacket._fetch_packet(self) if init_pkt else None
@property
def filter_call_info(self) -> list[FilterHandler]:
if "filter_call_info" not in self.__data.keys():
self.__data["filter_call_info"] = []
return self.__data.get("filter_call_info")
@filter_call_info.setter
def filter_call_info(self, v: list[FilterHandler]):
self.__data["filter_call_info"] = v
@property
def stream_max_size(self) -> int:
if "stream_max_size" not in self.__data.keys():
self.__data["stream_max_size"] = 1*8e20
return self.__data.get("stream_max_size")
@stream_max_size.setter
def stream_max_size(self, v: int):
self.__data["stream_max_size"] = v
@property
def full_stream_action(self) -> FullStreamAction:
if "full_stream_action" not in self.__data.keys():
self.__data["full_stream_action"] = "flush"
return self.__data.get("full_stream_action")
@full_stream_action.setter
def full_stream_action(self, v: FullStreamAction):
self.__data["full_stream_action"] = v
@property
def data_handler_context(self) -> dict:
if "data_handler_context" not in self.__data.keys():
self.__data["data_handler_context"] = {}
return self.__data.get("data_handler_context")
@data_handler_context.setter
def data_handler_context(self, v: dict):
self.__data["data_handler_context"] = v

View File

@@ -0,0 +1,15 @@
class NotReadyToRun(Exception):
"raise this exception if the stream state is not ready to parse this object, the call will be skipped"
class DropPacket(Exception):
"raise this exception if you want to drop the packet"
class StreamFullDrop(Exception):
"raise this exception if you want to drop the packet due to full stream"
class RejectConnection(Exception):
"raise this exception if you want to reject the connection"
class StreamFullReject(Exception):
"raise this exception if you want to reject the connection due to full stream"

View File

@@ -0,0 +1,40 @@
from dataclasses import dataclass, field
from enum import Enum
class Action(Enum):
ACCEPT = 0
DROP = 1
REJECT = 2
MANGLE = 3
class FullStreamAction(Enum):
FLUSH = 0
ACCEPT = 1
REJECT = 2
DROP = 3
@dataclass
class FilterHandler:
func: callable
name: str
params: dict[type, callable]
proto: str
@dataclass
class PacketHandlerResult:
glob: dict = field(repr=False)
action: Action = Action.ACCEPT
matched_by: str = None
mangled_packet: bytes = None
def set_result(self) -> None:
self.glob["__firegex_pyfilter_result"] = {
"action": self.action.value,
"matched_by": self.matched_by,
"mangled_packet": self.mangled_packet
}
def reset_result(self) -> None:
self.glob["__firegex_pyfilter_result"] = None

View File

@@ -0,0 +1,31 @@
from firegex.nfproxy.models.tcp import TCPInputStream, TCPOutputStream, TCPClientStream, TCPServerStream
from firegex.nfproxy.models.http import HttpRequest, HttpResponse, HttpRequestHeader, HttpResponseHeader
from firegex.nfproxy.internals.data import RawPacket
from enum import Enum
type_annotations_associations = {
"tcp": {
RawPacket: RawPacket._fetch_packet,
TCPInputStream: TCPInputStream._fetch_packet,
TCPOutputStream: TCPOutputStream._fetch_packet,
},
"http": {
RawPacket: RawPacket._fetch_packet,
TCPInputStream: TCPInputStream._fetch_packet,
TCPOutputStream: TCPOutputStream._fetch_packet,
HttpRequest: HttpRequest._fetch_packet,
HttpResponse: HttpResponse._fetch_packet,
HttpRequestHeader: HttpRequestHeader._fetch_packet,
HttpResponseHeader: HttpResponseHeader._fetch_packet,
}
}
class Protocols(Enum):
TCP = "tcp"
HTTP = "http"
__all__ = [
"RawPacket",
"TCPInputStream", "TCPOutputStream", "TCPClientStream", "TCPServerStream",
"HttpRequest", "HttpResponse", "HttpRequestHeader", "HttpResponseHeader", "Protocols"
]

View File

@@ -0,0 +1,320 @@
import pyllhttp
from firegex.nfproxy.internals.exceptions import NotReadyToRun
from firegex.nfproxy.internals.data import DataStreamCtx
from firegex.nfproxy.internals.exceptions import StreamFullDrop, StreamFullReject
from firegex.nfproxy.internals.models import FullStreamAction
class InternalCallbackHandler():
url: str|None = None
_url_buffer: bytes = b""
headers: dict[str, str] = {}
lheaders: dict[str, str] = {} # Lowercase headers
_header_fields: dict[bytes, bytes] = {}
has_begun: bool = False
body: bytes = None
_body_buffer: bytes = b""
headers_complete: bool = False
message_complete: bool = False
status: str|None = None
_status_buffer: bytes = b""
_current_header_field = b""
_current_header_value = b""
_save_body = True
total_size = 0
def on_message_begin(self):
self.has_begun = True
def on_url(self, url):
self.total_size += len(url)
self._url_buffer += url
def on_url_complete(self):
self.url = self._url_buffer.decode(errors="ignore")
self._url_buffer = None
def on_header_field(self, field):
self.total_size += len(field)
self._current_header_field += field
def on_header_field_complete(self):
self._current_header_field = self._current_header_field
def on_header_value(self, value):
self.total_size += len(value)
self._current_header_value += value
def on_header_value_complete(self):
if self._current_header_value is not None and self._current_header_field is not None:
self._header_fields[self._current_header_field.decode(errors="ignore")] = self._current_header_value.decode(errors="ignore")
self._current_header_field = b""
self._current_header_value = b""
def on_headers_complete(self):
self.headers_complete = True
self.headers = self._header_fields
self.lheaders = {k.lower(): v for k, v in self._header_fields.items()}
self._header_fields = {}
self._current_header_field = b""
self._current_header_value = b""
def on_body(self, body: bytes):
if self._save_body:
self.total_size += len(body)
self._body_buffer += body
def on_message_complete(self):
self.body = self._body_buffer
self._body_buffer = b""
try:
if "gzip" in self.content_encoding.lower():
import gzip
import io
with gzip.GzipFile(fileobj=io.BytesIO(self.body)) as f:
self.body = f.read()
except Exception as e:
print(f"Error decompressing gzip: {e}: skipping", flush=True)
self.message_complete = True
def on_status(self, status: bytes):
self.total_size += len(status)
self._status_buffer += status
def on_status_complete(self):
self.status = self._status_buffer.decode(errors="ignore")
self._status_buffer = b""
@property
def user_agent(self) -> str:
return self.lheaders.get("user-agent", "")
@property
def content_encoding(self) -> str:
return self.lheaders.get("content-encoding", "")
@property
def content_type(self) -> str:
return self.lheaders.get("content-type", "")
@property
def keep_alive(self) -> bool:
return self.should_keep_alive
@property
def should_upgrade(self) -> bool:
return self.is_upgrading
@property
def http_version(self) -> str:
return f"{self.major}.{self.minor}"
@property
def method_parsed(self) -> str:
return self.method.decode(errors="ignore")
@property
def content_length_parsed(self) -> int:
return self.content_length
class InternalHttpRequest(InternalCallbackHandler, pyllhttp.Request):
def __init__(self):
super(InternalCallbackHandler, self).__init__()
super(pyllhttp.Request, self).__init__()
class InternalHttpResponse(InternalCallbackHandler, pyllhttp.Response):
def __init__(self):
super(InternalCallbackHandler, self).__init__()
super(pyllhttp.Response, self).__init__()
class InternalBasicHttpMetaClass:
def __init__(self):
self._parser: InternalHttpRequest|InternalHttpResponse
self._headers_were_set = False
self.stream = b""
self.raised_error = False
@property
def total_size(self) -> int:
return self._parser.total_size
@property
def url(self) -> str|None:
return self._parser.url
@property
def headers(self) -> dict[str, str]:
return self._parser.headers
@property
def user_agent(self) -> str:
return self._parser.user_agent
@property
def content_encoding(self) -> str:
return self._parser.content_encoding
@property
def has_begun(self) -> bool:
return self._parser.has_begun
@property
def body(self) -> bytes:
return self._parser.body
@property
def headers_complete(self) -> bool:
return self._parser.headers_complete
@property
def message_complete(self) -> bool:
return self._parser.message_complete
@property
def http_version(self) -> str:
return self._parser.http_version
@property
def keep_alive(self) -> bool:
return self._parser.keep_alive
@property
def should_upgrade(self) -> bool:
return self._parser.should_upgrade
@property
def content_length(self) -> int|None:
return self._parser.content_length_parsed
def get_header(self, header: str, default=None) -> str:
return self._parser.lheaders.get(header.lower(), default)
def _packet_to_stream(self, internal_data: DataStreamCtx):
return self.should_upgrade and self._parser._save_body
def _fetch_current_packet(self, internal_data: DataStreamCtx):
if self._packet_to_stream(internal_data): # This is a websocket upgrade!
self._parser.total_size += len(internal_data.current_pkt.data)
self.stream += internal_data.current_pkt.data
else:
try:
self._parser.execute(internal_data.current_pkt.data)
if not self._parser.message_complete and self._parser.headers_complete and len(self._parser._body_buffer) == self._parser.content_length_parsed:
self._parser.on_message_complete()
except Exception as e:
self.raised_error = True
print(f"Error parsing HTTP packet: {e} {internal_data.current_pkt}", self, flush=True)
raise e
#It's called the first time if the headers are complete, and second time with body complete
def _after_fetch_callable_checks(self, internal_data: DataStreamCtx):
if self._parser.headers_complete and not self._headers_were_set:
self._headers_were_set = True
return True
return self._parser.message_complete or self.should_upgrade
def _before_fetch_callable_checks(self, internal_data: DataStreamCtx):
return True
def _trigger_remove_data(self, internal_data: DataStreamCtx):
return self.message_complete and not self.should_upgrade
@classmethod
def _fetch_packet(cls, internal_data: DataStreamCtx):
if internal_data.current_pkt is None or internal_data.current_pkt.is_tcp is False:
raise NotReadyToRun()
datahandler:InternalBasicHttpMetaClass = internal_data.data_handler_context.get(cls, None)
if datahandler is None or datahandler.raised_error:
datahandler = cls()
internal_data.data_handler_context[cls] = datahandler
if not datahandler._before_fetch_callable_checks(internal_data):
raise NotReadyToRun()
# Memory size managment
if datahandler.total_size+len(internal_data.current_pkt.data) > internal_data.stream_max_size:
match internal_data.full_stream_action:
case FullStreamAction.FLUSH:
datahandler = cls()
internal_data.data_handler_context[cls] = datahandler
case FullStreamAction.REJECT:
raise StreamFullReject()
case FullStreamAction.DROP:
raise StreamFullDrop()
case FullStreamAction.ACCEPT:
raise NotReadyToRun()
datahandler._fetch_current_packet(internal_data)
if not datahandler._after_fetch_callable_checks(internal_data):
raise NotReadyToRun()
if datahandler._trigger_remove_data(internal_data):
if internal_data.data_handler_context.get(cls):
del internal_data.data_handler_context[cls]
return datahandler
class HttpRequest(InternalBasicHttpMetaClass):
def __init__(self):
super().__init__()
# These will be used in the metaclass
self._parser: InternalHttpRequest = InternalHttpRequest()
self._headers_were_set = False
@property
def method(self) -> bytes:
return self._parser.method_parsed
def _before_fetch_callable_checks(self, internal_data: DataStreamCtx):
return internal_data.current_pkt.is_input
def __repr__(self):
return f"<HttpRequest method={self.method} url={self.url} headers={self.headers} body={self.body} http_version={self.http_version} keep_alive={self.keep_alive} should_upgrade={self.should_upgrade} headers_complete={self.headers_complete} message_complete={self.message_complete} has_begun={self.has_begun} content_length={self.content_length} stream={self.stream}>"
class HttpResponse(InternalBasicHttpMetaClass):
def __init__(self):
super().__init__()
self._parser: InternalHttpResponse = InternalHttpResponse()
self._headers_were_set = False
@property
def status_code(self) -> int:
return self._parser.status
def _before_fetch_callable_checks(self, internal_data: DataStreamCtx):
return not internal_data.current_pkt.is_input
def __repr__(self):
return f"<HttpResponse status_code={self.status_code} url={self.url} headers={self.headers} body={self.body} http_version={self.http_version} keep_alive={self.keep_alive} should_upgrade={self.should_upgrade} headers_complete={self.headers_complete} message_complete={self.message_complete} has_begun={self.has_begun} content_length={self.content_length} stream={self.stream}>"
class HttpRequestHeader(HttpRequest):
def __init__(self):
super().__init__()
self._parser._save_body = False
def _before_fetch_callable_checks(self, internal_data: DataStreamCtx):
return internal_data.current_pkt.is_input and not self._headers_were_set
def _after_fetch_callable_checks(self, internal_data: DataStreamCtx):
if self._parser.headers_complete and not self._headers_were_set:
self._headers_were_set = True
return True
return False
class HttpResponseHeader(HttpResponse):
def __init__(self):
super().__init__()
self._parser._save_body = False
def _before_fetch_callable_checks(self, internal_data: DataStreamCtx):
return not internal_data.current_pkt.is_input and not self._headers_were_set
def _after_fetch_callable_checks(self, internal_data: DataStreamCtx):
if self._parser.headers_complete and not self._headers_were_set:
self._headers_were_set = True
return True
return False

View File

@@ -0,0 +1,80 @@
from firegex.nfproxy.internals.data import DataStreamCtx
from firegex.nfproxy.internals.exceptions import NotReadyToRun, StreamFullDrop, StreamFullReject
from firegex.nfproxy.internals.models import FullStreamAction
class InternalTCPStream:
def __init__(self,
data: bytes,
is_ipv6: bool,
):
self.data = bytes(data)
self.__is_ipv6 = bool(is_ipv6)
self.__total_stream_size = len(data)
@property
def is_ipv6(self) -> bool:
return self.__is_ipv6
@property
def total_stream_size(self) -> int:
return self.__total_stream_size
def _push_new_data(self, data: bytes):
self.data += data
self.__total_stream_size += len(data)
@classmethod
def _fetch_packet(cls, internal_data:DataStreamCtx, is_input:bool=False):
if internal_data.current_pkt is None or internal_data.current_pkt.is_tcp is False:
raise NotReadyToRun()
if internal_data.current_pkt.is_input != is_input:
raise NotReadyToRun()
datahandler: TCPInputStream = internal_data.data_handler_context.get(cls, None)
if datahandler is None:
datahandler = cls(internal_data.current_pkt.data, internal_data.current_pkt.is_ipv6)
internal_data.data_handler_context[cls] = datahandler
else:
if datahandler.total_stream_size+len(internal_data.current_pkt.data) > internal_data.stream_max_size:
match internal_data.full_stream_action:
case FullStreamAction.FLUSH:
datahandler = cls(internal_data.current_pkt.data, internal_data.current_pkt.is_ipv6)
internal_data.data_handler_context[cls] = datahandler
case FullStreamAction.REJECT:
raise StreamFullReject()
case FullStreamAction.DROP:
raise StreamFullDrop()
case FullStreamAction.ACCEPT:
raise NotReadyToRun()
else:
datahandler._push_new_data(internal_data.current_pkt.data)
return datahandler
class TCPInputStream(InternalTCPStream):
"""
This datamodel will assemble the TCP input stream from the client sent data.
The function that use this data model will be handled when:
- The packet is TCP
- At least 1 packet has been sent
- A new client packet has been received
"""
@classmethod
def _fetch_packet(cls, internal_data:DataStreamCtx):
return super()._fetch_packet(internal_data, is_input=True)
TCPClientStream = TCPInputStream
class TCPOutputStream:
"""
This datamodel will assemble the TCP output stream from the server sent data.
The function that use this data model will be handled when:
- The packet is TCP
- At least 1 packet has been sent
- A new server packet has been sent
"""
@classmethod
def _fetch_packet(cls, internal_data:DataStreamCtx):
return super()._fetch_packet(internal_data, is_input=False)
TCPServerStream = TCPOutputStream

View File

@@ -0,0 +1,303 @@
import socket
import os
from firegex.nfproxy.internals import get_filter_names
import traceback
from multiprocessing import Process
from firegex.nfproxy import ACCEPT, DROP, REJECT, UNSTABLE_MANGLE
from rich.markup import escape
from rich import print
import asyncio
from watchfiles import awatch, Change
fake_ip_header = b"FAKE:IP:TCP:HEADERS:"
fake_ip_header_len = len(fake_ip_header)
MANGLE_WARNING = True
class LogLevels:
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
DEBUG = "DEBUG"
def load_level_str(level:str):
if level is None:
return ""
match level:
case LogLevels.INFO:
return "[chartreuse4 bold]\\[INFO][/]"
case LogLevels.WARNING:
return "[yellow bold]\\[WARNING][/]"
case LogLevels.ERROR:
return "[red bold]\\[ERROR][/]"
case LogLevels.DEBUG:
return "[blue bold]\\[DEBUG][/]"
case _:
return f"\\[[red bold]{escape(level)}[/]]"
def log_print(module:str, *args, level:str = LogLevels.INFO, **kwargs):
return print(f"{load_level_str(level)}[deep_pink4 bold]\\[nfproxy][/][medium_orchid3 bold]\\[{escape(module)}][/]", *args, **kwargs)
async def _watch_filter_file(filter_file: str, reload_action):
abs_path = os.path.abspath(filter_file)
directory = os.path.dirname(abs_path)
# Immediately call the reload action on startup.
if reload_action is not None:
reload_action()
log_print("observer", f"Listening for changes on {escape(abs_path)}")
try:
# Monitor the directory; set recursive=False since we only care about the specific file.
async for changes in awatch(directory, recursive=False):
# Process events and filter for our file.
for change in changes:
event, path = change
if os.path.abspath(path) == abs_path:
# Optionally, you can check the event type:
if event in {Change.modified, Change.deleted}:
if reload_action is not None:
reload_action()
except asyncio.CancelledError:
log_print("observer", "Watcher cancelled, stopping.")
async def _forward_and_filter(filter_ctx: dict,
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
is_input: bool,
is_ipv6: bool,
is_tcp: bool,
has_to_filter: bool = True):
"""Asynchronously forward data from reader to writer applying filters."""
try:
has_to_drop = False
while True:
try:
data = await reader.read(4096)
except Exception:
break
if not data:
break
if has_to_drop:
continue
if has_to_filter:
filter_ctx["__firegex_packet_info"] = {
"data": data,
"l4_size": len(data),
"raw_packet": fake_ip_header + data,
"is_input": is_input,
"is_ipv6": is_ipv6,
"is_tcp": is_tcp
}
try:
exec("firegex.nfproxy.internals.handle_packet(globals())", filter_ctx, filter_ctx)
except Exception as e:
log_print("packet-handling",
f"Error while executing filter: {escape(str(e))}, forwarding normally from now",
level=LogLevels.ERROR)
traceback.print_exc()
# Stop filtering and forward the packet as is.
has_to_filter = False
writer.write(data)
await writer.drain()
continue
finally:
filter_ctx.pop("__firegex_packet_info", None)
result = filter_ctx.pop("__firegex_pyfilter_result", None)
if result is None or not isinstance(result, dict):
log_print("filter-parsing", "No result found", level=LogLevels.ERROR)
has_to_filter = False
writer.write(data)
await writer.drain()
continue
action = result.get("action")
if action is None or not isinstance(action, int):
log_print("filter-parsing", "No action found", level=LogLevels.ERROR)
has_to_filter = False
writer.write(data)
await writer.drain()
continue
if action == ACCEPT.value:
writer.write(data)
await writer.drain()
continue
filter_name = result.get("matched_by")
if filter_name is None or not isinstance(filter_name, str):
log_print("filter-parsing", "No matched_by found", level=LogLevels.ERROR)
has_to_filter = False
writer.write(data)
await writer.drain()
continue
if action == DROP.value:
log_print("drop-action", "Dropping connection caused by {escape(filter_name)} pyfilter")
has_to_drop = True
continue
if action == REJECT.value:
log_print("reject-action", f"Rejecting connection caused by {escape(filter_name)} pyfilter")
writer.close()
await writer.wait_closed()
return
elif action == UNSTABLE_MANGLE.value:
mangled_packet = result.get("mangled_packet")
if mangled_packet is None or not isinstance(mangled_packet, bytes):
log_print("filter-parsing", "No mangled_packet found", level=LogLevels.ERROR)
has_to_filter = False
writer.write(data)
await writer.drain()
continue
log_print("mangle", f"Mangling packet caused by {escape(filter_name)} pyfilter")
if MANGLE_WARNING:
log_print("mangle",
"In the real execution mangling is not so stable as the simulation does, l4_data can be different by data",
level=LogLevels.WARNING)
writer.write(mangled_packet[fake_ip_header_len:])
await writer.drain()
continue
else:
log_print("filter-parsing", f"Invalid action {action} found", level=LogLevels.ERROR)
has_to_filter = False
writer.write(data)
await writer.drain()
continue
else:
writer.write(data)
await writer.drain()
except Exception as exc:
log_print("forward_and_filter", f"Exception occurred: {escape(str(exc))}", level=LogLevels.ERROR)
finally:
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
async def _handle_connection(
reader: asyncio.StreamReader, writer: asyncio.StreamWriter, filter_code: str,
target_ip: str, target_port: int, ipv6: bool):
"""Handle a new incoming connection and create a remote connection."""
addr = writer.get_extra_info('peername')
log_print("listener", f"Accepted connection from {escape(addr[0])}:{addr[1]}")
try:
remote_reader, remote_writer = await asyncio.open_connection(
target_ip, target_port,
family=socket.AF_INET6 if ipv6 else socket.AF_INET)
except Exception as e:
log_print("listener",
f"Could not connect to remote {escape(target_ip)}:{target_port}: {escape(str(e))}",
level=LogLevels.ERROR)
writer.close()
await writer.wait_closed()
return
try:
filter_ctx = {}
exec(filter_code, filter_ctx, filter_ctx)
except Exception as e:
log_print("listener",
f"Error while compiling filter context: {escape(str(e))}, forwarding normally",
level=LogLevels.ERROR)
traceback.print_exc()
filter_ctx = {}
# Create asynchronous tasks for bidirectional forwarding.
task1 = asyncio.create_task(_forward_and_filter(filter_ctx, reader, remote_writer, True, ipv6, True, True))
task2 = asyncio.create_task(_forward_and_filter(filter_ctx, remote_reader, writer, False, ipv6, True, True))
try:
await asyncio.gather(task1, task2)
except (KeyboardInterrupt, asyncio.CancelledError):
task1.cancel()
task2.cancel()
await asyncio.gather(task1, task2)
finally:
remote_writer.close()
await remote_writer.wait_closed()
async def _execute_proxy(
filter_code: str,
target_ip: str, target_port: int,
local_ip: str = "127.0.0.1", local_port: int = 7474,
ipv6: bool = False
):
"""Start the asyncio-based TCP proxy server."""
addr_family = socket.AF_INET6 if ipv6 else socket.AF_INET
server = await asyncio.start_server(
lambda r, w: _handle_connection(r, w, filter_code, target_ip, target_port, ipv6),
local_ip, local_port, family=addr_family)
log_print("listener", f"TCP proxy listening on {escape(local_ip)}:{local_port} and forwarding to -> {escape(target_ip)}:{target_port}")
async with server:
await server.serve_forever()
def _proxy_asyncio_runner(filter_code: str, target_ip: str, target_port: int, local_ip: str, local_port: int, ipv6: bool):
try:
return asyncio.run(_execute_proxy(filter_code, target_ip, target_port, local_ip, local_port, ipv6))
except KeyboardInterrupt:
log_print("listener", "Proxy server stopped", level=LogLevels.WARNING)
def _build_filter(filepath:str, proto:str):
if os.path.isfile(filepath) is False:
raise Exception(f"Filter file {filepath} not found")
with open(filepath, "r") as f:
filter_code = f.read()
filters = get_filter_names(filter_code, proto)
filter_code += (
"\n\n__firegex_pyfilter_enabled = [" + ", ".join([repr(f) for f in filters]) + "]\n"
"__firegex_proto = " + repr(proto) + "\n"
"import firegex.nfproxy.internals\n"
"firegex.nfproxy.internals.compile(globals())\n"
)
filter_glob = {}
exec(filter_code, filter_glob, filter_glob) # test compilation of filters
return filter_code
def run_proxy_simulation(filter_file:str, proto:str, target_ip:str, target_port:int, local_ip:str = None, local_port:int = 7474, ipv6:bool = False):
if local_ip is None:
if ipv6:
local_ip = "::1"
else:
local_ip = "127.0.0.1"
if os.path.isfile(filter_file) is False:
raise Exception(f"\\[nfproxy]\\[init] Filter file {filter_file} not found")
else:
filter_file = os.path.abspath(filter_file)
proxy_process:Process|None = None
def reload_proxy_proc():
nonlocal proxy_process
if proxy_process is not None:
log_print("RELOADING", "Proxy reload triggered", level=LogLevels.WARNING)
proxy_process.kill()
proxy_process.join()
proxy_process = None
compiled_filter = None
try:
compiled_filter = _build_filter(filter_file, proto)
except Exception:
log_print("reloader", f"Failed to build filter {escape(filter_file)}!", level=LogLevels.ERROR)
traceback.print_exc()
if compiled_filter is not None:
proxy_process = Process(target=_proxy_asyncio_runner, args=(compiled_filter, target_ip, target_port, local_ip, local_port, ipv6))
proxy_process.start()
try:
asyncio.run(_watch_filter_file(filter_file, reload_proxy_proc))
except KeyboardInterrupt:
pass
finally:
if proxy_process is not None:
proxy_process.kill()
proxy_process.join()

View File

@@ -0,0 +1,6 @@
typer==0.15.2
pydantic>=2
typing-extensions>=4.7.1
watchfiles
fgex
pyllhttp

View File

@@ -5,17 +5,19 @@
"name": "firegex-frontend", "name": "firegex-frontend",
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@mantine/core": "^7.16.2", "@mantine/code-highlight": "^7.17.0",
"@mantine/form": "^7.16.2", "@mantine/core": "^7.16.3",
"@mantine/hooks": "^7.16.2", "@mantine/form": "^7.16.3",
"@mantine/modals": "^7.16.2", "@mantine/hooks": "^7.16.3",
"@mantine/notifications": "^7.16.2", "@mantine/modals": "^7.16.3",
"@mantine/notifications": "^7.16.3",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^20.17.16", "@types/node": "^20.17.17",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"install": "^0.13.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
@@ -141,17 +143,19 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@mantine/core": ["@mantine/core@7.16.3", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-cxhIpfd2i0Zmk9TKdejYAoIvWouMGhzK3OOX+VRViZ5HEjnTQCGl2h3db56ThqB6NfVPCno6BPbt5lwekTtmuQ=="], "@mantine/code-highlight": ["@mantine/code-highlight@7.17.0", "", { "dependencies": { "clsx": "^2.1.1", "highlight.js": "^11.10.0" }, "peerDependencies": { "@mantine/core": "7.17.0", "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-i6MvxW+PtdRNYHCm8Qa/aiMkLr47EYS0+12rf5XhDVdYZy+0+XiRkwBsxnvzQfKqv0QtH2dchBJDEBMmPB/nVw=="],
"@mantine/form": ["@mantine/form@7.16.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-GqomUG2Ri5adxYsTU1S5IhKRPcqTG5JkPvMERns8PQAcUz/lvzsnk3wY1v4K5CEbCAdpimle4bSsZTM9g697vg=="], "@mantine/core": ["@mantine/core@7.17.0", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-AU5UFewUNzBCUXIq5Jk6q402TEri7atZW61qHW6P0GufJ2W/JxGHRvgmHOVHTVIcuWQRCt9SBSqZoZ/vHs9LhA=="],
"@mantine/hooks": ["@mantine/hooks@7.16.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-B94FBWk5Sc81tAjV+B3dGh/gKzfqzpzVC/KHyBRWOOyJRqeeRbI/FAaJo4zwppyQo1POSl5ArdyjtDRrRIj2SQ=="], "@mantine/form": ["@mantine/form@7.17.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-LONdeb+wL8h9fvyQ339ZFLxqrvYff+b+H+kginZhnr45OBTZDLXNVAt/YoKVFEkynF9WDJjdBVrXKcOZvPgmrA=="],
"@mantine/modals": ["@mantine/modals@7.16.3", "", { "peerDependencies": { "@mantine/core": "7.16.3", "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-BJuDzRugK6xLbuFTTo8NLJumVvVmSYsNVcEtmlXOWTE3NkDGktBXGKo8V1B0XfJ9/d/rZw7HCE0p4i76MtA+bQ=="], "@mantine/hooks": ["@mantine/hooks@7.17.0", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-vo3K49mLy1nJ8LQNb5KDbJgnX0xwt3Y8JOF3ythjB5LEFMptdLSSgulu64zj+QHtzvffFCsMb05DbTLLpVP/JQ=="],
"@mantine/notifications": ["@mantine/notifications@7.16.3", "", { "dependencies": { "@mantine/store": "7.16.3", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "7.16.3", "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-wtEME9kSYfXWYmAmQUZ8c+rwNmhdWRBaW1mlPdQsPkzMqkv4q6yy0IpgwcnuHStSG9EHaQBXazmVxMZJdEAWBQ=="], "@mantine/modals": ["@mantine/modals@7.17.0", "", { "peerDependencies": { "@mantine/core": "7.17.0", "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-4sfiFxIxMxfm2RH4jXMN+cr8tFS5AexXG4TY7TRN/ySdkiWtFVvDe5l2/KRWWeWwDUb7wQhht8Ompj5KtexlEA=="],
"@mantine/store": ["@mantine/store@7.16.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-6M2M5+0BrRtnVv+PUmr04tY1RjPqyapaHplo90uK1NMhP/1EIqrwTL9KoEtCNCJ5pog1AQtu0bj0QPbqUvxwLg=="], "@mantine/notifications": ["@mantine/notifications@7.17.0", "", { "dependencies": { "@mantine/store": "7.17.0", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "7.17.0", "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-xejr1WW02NrrrE4HPDoownILJubcjLLwCDeTk907ZeeHKBEPut7RukEq6gLzOZBhNhKdPM+vCM7GcbXdaLZq/Q=="],
"@mantine/store": ["@mantine/store@7.17.0", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-nhWRYRLqvAjrD/ApKCXxuHyTWg2b5dC06Z5gmO8udj4pBgndNf9nmCl+Of90H6bgOa56moJA7UQyXoF1SfxqVg=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
@@ -205,7 +209,7 @@
"@types/jest": ["@types/jest@27.5.2", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA=="], "@types/jest": ["@types/jest@27.5.2", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA=="],
"@types/node": ["@types/node@20.17.17", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg=="], "@types/node": ["@types/node@20.17.19", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A=="],
"@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="],
@@ -295,12 +299,16 @@
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"install": ["install@0.13.0", "", {}, "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
@@ -365,7 +373,7 @@
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
"react-icons": ["react-icons@5.4.0", "", { "peerDependencies": { "react": "*" } }, "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ=="], "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
@@ -379,9 +387,9 @@
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-router": ["react-router@7.1.5", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA=="], "react-router": ["react-router@7.2.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ=="],
"react-router-dom": ["react-router-dom@7.1.5", "", { "dependencies": { "react-router": "7.1.5" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ=="], "react-router-dom": ["react-router-dom@7.2.0", "", { "dependencies": { "react-router": "7.2.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],

View File

@@ -5,21 +5,23 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@mantine/core": "^7.16.3", "@mantine/code-highlight": "^7.17.0",
"@mantine/form": "^7.16.3", "@mantine/core": "^7.17.0",
"@mantine/hooks": "^7.16.3", "@mantine/form": "^7.17.0",
"@mantine/modals": "^7.16.3", "@mantine/hooks": "^7.17.0",
"@mantine/notifications": "^7.16.3", "@mantine/modals": "^7.17.0",
"@mantine/notifications": "^7.17.0",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^20.17.17", "@types/node": "^20.17.19",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"install": "^0.13.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.4.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.1.5", "react-router-dom": "^7.2.0",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",

View File

@@ -5,16 +5,14 @@ import { ImCross } from 'react-icons/im';
import { Outlet, Route, Routes } from 'react-router-dom'; import { Outlet, Route, Routes } from 'react-router-dom';
import MainLayout from './components/MainLayout'; import MainLayout from './components/MainLayout';
import { PasswordSend, ServerStatusResponse } from './js/models'; import { PasswordSend, ServerStatusResponse } from './js/models';
import { DEV_IP_BACKEND, errorNotify, getstatus, HomeRedirector, IS_DEV, login, setpassword } from './js/utils'; import { errorNotify, getstatus, HomeRedirector, IS_DEV, login, setpassword, socketio } from './js/utils';
import NFRegex from './pages/NFRegex'; import NFRegex from './pages/NFRegex';
import io from 'socket.io-client';
import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails'; import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails';
import PortHijack from './pages/PortHijack'; import PortHijack from './pages/PortHijack';
import { Firewall } from './pages/Firewall'; import { Firewall } from './pages/Firewall';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import NFProxy from './pages/NFProxy';
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
const socket = IS_DEV?io("ws://"+DEV_IP_BACKEND, {transports: ["websocket"], path:"/sock/socket.io" }):io({transports: ["websocket"], path:"/sock/socket.io"});
function App() { function App() {
@@ -23,33 +21,20 @@ function App() {
const [reqError, setReqError] = useState<undefined|string>() const [reqError, setReqError] = useState<undefined|string>()
const [error, setError] = useState<string|null>() const [error, setError] = useState<string|null>()
const [loadinBtn, setLoadingBtn] = useState(false); const [loadinBtn, setLoadingBtn] = useState(false);
const queryClient = useQueryClient()
const getStatus = () =>{ const getStatus = () =>{
getstatus().then( res =>{ getstatus().then( res =>{
setSystemStatus(res) setSystemStatus(res)
setReqError(undefined) setReqError(undefined)
setLoading(false) }).catch(err=>{
}).catch(err=>{ setReqError(err.toString())
setReqError(err.toString()) setTimeout(getStatus, 500)
setLoading(false) }).finally( ()=>setLoading(false) )
setTimeout(getStatus, 500)
})
} }
useEffect(()=>{ useEffect(()=>{
getStatus() getStatus()
socket.on("update", (data) => {
queryClient.invalidateQueries({ queryKey: data })
})
socket.on("connect_error", (err) => {
errorNotify("Socket.Io connection failed! ",`Error message: [${err.message}]`)
getStatus()
});
return () => {
socket.off("update")
socket.off("connect_error")
}
},[]) },[])
const form = useForm({ const form = useForm({
@@ -143,16 +128,7 @@ function App() {
</Notification><Space h="md" /></>:null} </Notification><Space h="md" /></>:null}
</Box> </Box>
}else if (systemStatus.status === "run" && systemStatus.loggined){ }else if (systemStatus.status === "run" && systemStatus.loggined){
return <Routes> return <PageRouting getStatus={getStatus} />
<Route element={<MainLayout><Outlet /></MainLayout>}>
<Route path="nfregex" element={<NFRegex><Outlet /></NFRegex>} >
<Route path=":srv" element={<ServiceDetailsNFRegex />} />
</Route>
<Route path="firewall" element={<Firewall />} />
<Route path="porthijack" element={<PortHijack />} />
<Route path="*" element={<HomeRedirector />} />
</Route>
</Routes>
}else{ }else{
return <Box className='center-flex-row' style={{padding:"100px"}}> return <Box className='center-flex-row' style={{padding:"100px"}}>
<Title order={1} style={{textAlign:"center"}}>Error launching Firegex! 🔥</Title> <Title order={1} style={{textAlign:"center"}}>Error launching Firegex! 🔥</Title>
@@ -162,4 +138,41 @@ function App() {
} }
} }
const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
const queryClient = useQueryClient()
useEffect(()=>{
getStatus()
socketio.on("update", (data) => {
queryClient.invalidateQueries({ queryKey: data })
})
socketio.on("connect_error", (err) => {
errorNotify("Socket.Io connection failed! ",`Error message: [${err.message}]`)
getStatus()
});
return () => {
socketio.off("update")
socketio.off("connect_error")
}
},[])
return <Routes>
<Route element={<MainLayout><Outlet /></MainLayout>}>
<Route path="nfregex" element={<NFRegex><Outlet /></NFRegex>} >
<Route path=":srv" element={<ServiceDetailsNFRegex />} />
</Route>
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
</Route>
<Route path="firewall" element={<Firewall />} />
<Route path="porthijack" element={<PortHijack />} />
<Route path="*" element={<HomeRedirector />} />
</Route>
</Routes>
}
export default App; export default App;

View File

@@ -2,8 +2,9 @@ import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select }
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useState } from 'react'; import { useState } from 'react';
import { RegexAddForm } from '../js/models'; import { RegexAddForm } from '../js/models';
import { b64decode, b64encode, getapiobject, okNotify } from '../js/utils'; import { b64decode, b64encode, okNotify } from '../js/utils';
import { ImCross } from "react-icons/im" import { ImCross } from "react-icons/im"
import { nfregex } from './NFRegex/utils';
type RegexAddInfo = { type RegexAddInfo = {
regex:string, regex:string,
@@ -47,7 +48,7 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>
active: !values.deactive active: !values.deactive
} }
setSubmitLoading(false) setSubmitLoading(false)
getapiobject().regexesadd(request).then( res => { nfregex.regexesadd(request).then( res => {
if (!res){ if (!res){
setSubmitLoading(false) setSubmitLoading(false)
close(); close();

View File

@@ -0,0 +1,38 @@
import { ActionIcon, ActionIconProps, Box, Modal, ScrollArea, Title, Tooltip } from "@mantine/core";
import { useState } from "react";
import { FaBookBookmark } from "react-icons/fa6";
import { NFRegexDocs } from "./NFRegex/NFRegexDocs";
import { NFProxyDocs } from "./NFProxy/NFProxyDocs";
import { PortHijackDocs } from "./PortHijack/PortHijackDocs";
import { EnumToPrimitiveUnion } from "../js/utils";
export enum DocType{
NFREGEX = "nfregex",
NFPROXY = "nfproxy",
PORTHIJACK = "porthijack",
}
export const DocsButton = ({ doc, ...props }: { doc: EnumToPrimitiveUnion<DocType> } & ActionIconProps) => {
const [open, setOpen] = useState(false);
return <Box>
<Tooltip label="Read the documentation" color="pink">
<ActionIcon color="pink" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled" {...props}><FaBookBookmark size="20px" /></ActionIcon>
</Tooltip>
<Modal opened={open} onClose={() => setOpen(false)} fullScreen title={
<Title order={2}>Firegex Docs 📕</Title>
} scrollAreaComponent={ScrollArea.Autosize}>
{
doc == DocType.NFREGEX ?
<NFRegexDocs />:
doc == DocType.NFPROXY ?
<NFProxyDocs />:
doc == DocType.PORTHIJACK ?
<PortHijackDocs />:
<Title order={3}>Docs not found</Title>
}
</Modal>
</Box>
}

View File

@@ -90,4 +90,4 @@ export const firewall = {
ruleset: async (data:RuleAddForm) => { ruleset: async (data:RuleAddForm) => {
return await postapi("firewall/rules", data) as ServerResponseListed; return await postapi("firewall/rules", data) as ServerResponseListed;
} }
} }

View File

@@ -31,8 +31,6 @@ function HeaderPage(props: any) {
const [changePasswordModal, setChangePasswordModal] = useState(false); const [changePasswordModal, setChangePasswordModal] = useState(false);
const [resetFiregexModal, setResetFiregexModal] = useState(false); const [resetFiregexModal, setResetFiregexModal] = useState(false);
const [tooltipHomeOpened, setTooltipHomeOpened] = useState(false);
const [tooltipLogoutOpened,setTooltipLogoutOpened] = useState(false);
return <AppShell.Header className="firegex__header__header" {...props}> return <AppShell.Header className="firegex__header__header" {...props}>
<Burger <Burger
hiddenFrom='md' hiddenFrom='md'
@@ -64,19 +62,16 @@ function HeaderPage(props: any) {
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item> <Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
</MenuDropDownWithButton> </MenuDropDownWithButton>
<Space w="md" /> <Space w="md" />
<Tooltip label="Home" position='bottom' color="teal" opened={tooltipHomeOpened}> <Tooltip label="Home" position='bottom' color="teal">
<ActionIcon color="teal" style={{marginRight:"10px"}} <ActionIcon color="teal" style={{marginRight:"10px"}}
size="xl" radius="md" variant="filled" size="xl" radius="md" variant="filled"
onClick={go_to_home} onClick={go_to_home}>
onFocus={() => setTooltipHomeOpened(false)} onBlur={() => setTooltipHomeOpened(false)}
onMouseEnter={() => setTooltipHomeOpened(true)} onMouseLeave={() => setTooltipHomeOpened(false)}>
<AiFillHome size="25px" /> <AiFillHome size="25px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="Logout" position='bottom' color="blue" opened={tooltipLogoutOpened}> <Tooltip label="Logout" position='bottom' color="blue">
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled" <ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
onFocus={() => setTooltipLogoutOpened(false)} onBlur={() => setTooltipLogoutOpened(false)} <ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
onMouseEnter={() => setTooltipLogoutOpened(true)} onMouseLeave={() => setTooltipLogoutOpened(false)}><ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
</Tooltip> </Tooltip>
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} /> <ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} /> <ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />

View File

@@ -0,0 +1,17 @@
import { Code, Modal, ScrollArea } from "@mantine/core"
export const ModalLog = (
{ title, opened, close, data }:
{
title: string,
opened: boolean,
close: () => void,
data: string,
}
) => {
return <Modal size="90%" title={title} opened={opened} onClose={close} centered>
<ScrollArea h={500} style={{ maxWidth: "100%",whiteSpace: "break-spaces"}} scrollbars="y">
<Code block mih={500} style={{ maxWidth: "100%",whiteSpace: "break-spaces"}}>{data}</Code>
</ScrollArea>
</Modal>
}

View File

@@ -0,0 +1,139 @@
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { nfproxy, Service } from './utils';
import PortAndInterface from '../PortAndInterface';
import { IoMdInformationCircleOutline } from "react-icons/io";
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
const initialValues = {
name: "",
port:edit?.port??8080,
ip_int:edit?.ip_int??"",
proto:edit?.proto??"tcp",
fail_open: edit?.fail_open??false,
autostart: true
}
const form = useForm({
initialValues: initialValues,
validate:{
name: (value) => edit? null : value !== "" ? null : "Service name is required",
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
}
})
useEffect(() => {
if (opened){
form.setInitialValues(initialValues)
form.reset()
}
}, [opened])
const close = () =>{
onClose()
form.reset()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
setSubmitLoading(true)
if (edit){
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}else{
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
if (res.status === "ok" && res.service_id){
setSubmitLoading(false)
close();
if (autostart) nfproxy.servicestart(res.service_id)
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
}else{
setSubmitLoading(false)
setError("Invalid request! [ "+res.status+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
}
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
{!edit?<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>:null}
<Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" />
<Box className='center-flex'>
<Box>
{!edit?<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>:null}
<Space h="sm" />
<Switch
label={<Box className='center-flex'>
Enable fail-open nfqueue
<Space w="xs" />
<Tooltip label={<>
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
</>}>
<IoMdInformationCircleOutline size={15} />
</Tooltip>
</Box>}
{...form.getInputProps('fail_open', { type: 'checkbox' })}
/>
</Box>
<Box className="flex-spacer"></Box>
{edit?null:<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'HTTP', value: 'http' },
]}
{...form.getInputProps('proto')}
/>}
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddEditService;

View File

@@ -0,0 +1,44 @@
import { IoIosWarning } from "react-icons/io"
import { socketio, WARNING_NFPROXY_TIME_LIMIT } from "../../js/utils"
import { Tooltip } from "@mantine/core"
import { useEffect, useState } from "react"
import { round } from "@mantine/core/lib/components/ColorPicker/converters/parsers"
export const ExceptionWarning = ({ service_id }: { service_id: string }) => {
const [lastExceptionTimestamp, setLastExceptionTimestamp] = useState<number>(0)
useEffect(() => {
socketio.emit("nfproxy-exception-join", { service: service_id });
socketio.on(`nfproxy-exception-${service_id}`, (data) => {
setLastExceptionTimestamp(data)
});
return () => {
socketio.emit("nfproxy-exception-leave", { service: service_id });
}
}, [])
const [_time, setTime] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
const deltaTime = new Date().getTime()-lastExceptionTimestamp
const minutes = Math.floor(deltaTime/(1000*60))
const seconds = Math.floor(deltaTime/1000)%60
const deltaStringTime = `${minutes.toString().length>1?minutes:"0"+minutes}:${seconds.toString().length>1?seconds:"0"+seconds}`
return <>
{(new Date().getTime()-lastExceptionTimestamp <= WARNING_NFPROXY_TIME_LIMIT)?
<Tooltip label={`There was an exception less than ${deltaStringTime} minutes ago: check the logs`} color="yellow">
<IoIosWarning size={30} style={{ color: "yellow" }} />
</Tooltip>
:null}
</>
}

View File

@@ -0,0 +1,410 @@
import { CodeHighlight } from "@mantine/code-highlight";
import { Container, Title, Text, List, Code, Space, Badge, Box } from "@mantine/core";
import { CgEditBlackPoint } from "react-icons/cg";
import { EXAMPLE_PYFILTER } from "./utils";
const IMPORT_CODE_EXAMPLE = `from firegex.nfproxy import pyfilter, ACCEPT, REJECT`
const FOO_FILTER_CODE = `from firegex.nfproxy import pyfilter, ACCEPT, REJECT
# This is NOT a filter
def useless_function() -> int:
print("This is a useless function")
return 42
@pyfilter
def none_filter(): # This is a filter that does nothing
useless_function()
return ACCEPT
`
const TYPING_ARGS_EXAMPLE = `from firegex.nfproxy import pyfilter, ACCEPT, REJECT
from firegex.nfproxy.models import HttpRequest
@pyfilter
def filter_with_args(http_request: HttpRequest) -> int:
if http_request.body:
if b"ILLEGAL" in http_request.body:
return REJECT
`
const IMPORT_FULL_ACTION_STREAM = `from firegex.nfproxy import FullStreamAction`
export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h
Usage: fgex nfproxy [OPTIONS] FILTER_FILE ADDRESS PORT
Run an nfproxy simulation
╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ * filter_file TEXT The path to the filter file [default: None] [required] │
│ * address TEXT The address of the target to proxy [default: None] [required] │
│ * port INTEGER The port of the target to proxy [default: None] [required] │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --proto [tcp|http] The protocol to proxy [default: tcp] │
│ --from-address TEXT The address of the local server [default: None] │
│ --from-port INTEGER The port of the local server [default: 7474] │
│ -6 Use IPv6 for the connection │
│ --help -h Show this message and exit. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯`
const HttpBadge = () => {
return <Badge size="md" ml="xs" radius="md" variant="gradient" gradient={{ from: 'red', to: 'grape', deg: 107 }} style={{ fontSize: "13px", fontWeight: "bolder"}}>HTTP</Badge>
}
const TCPBadge = () => {
return <Badge size="md" ml="xs" radius="md" variant="gradient" gradient={{ from: 'indigo', to: 'teal', deg: 164 }} style={{ fontSize: "13px", fontWeight: "bolder"}}>TCP</Badge>
}
export const NFProxyDocs = () => {
return (
<Container size="xl">
<Title order={1}>🌐 Netfilter Proxy Documentation</Title>
<Title order={2} mt="xl" mb="sm">📖 Overview</Title>
<Text size="lg">
Netfilter Proxy is a simulated proxy that leverages <a href="https://netfilter.org/projects/libnetfilter_queue/">nfqueue</a> to intercept network packets.
It follows a similar workflow to NFRegex but introduces Python-based filtering capabilities,
providing users with the flexibility to upload custom filters.
</Text>
<Title order={2} mt="lg" mb="sm"> How to use Netfilter Proxy</Title>
<Text size="lg">
To use Netfilter Proxy, simply create and upload a Python filter. The filter is passed to the C++ binary,
which then processes packets using the provided logic. This allows you to tailor the filtering behavior
to your needs.
</Text>
<Title order={2} mt="lg" mb="sm">💡 How to write pyfilters?</Title>
<Text size="lg">
First of all install the firegex lib and update it running <Code>pip install -U fgex</Code>.
After that you can use <Code>firegex</Code> module.
<CodeHighlight code={IMPORT_CODE_EXAMPLE} language="python" my="sm"/>
With this code we imported the <Code>pyfilter</Code> decorator and the <Code>ACCEPT</Code> and <Code>REJECT</Code> statements.<br />
Let's create a first (useless) filter to see the syntax:
<CodeHighlight code={FOO_FILTER_CODE} language="python" my="sm"/>
You see that the filter must be decorated with the <Code>pyfilter</Code> decorator and must return a statement about how to manage that packet.
<br/><Space h="sm" />
You can save every data about the current flow in the global variables, the code you write will be executed only once for flow. The globals variables are isolated between flows.
For each packet the filter functions will be called with the required paramethers and using the same globals as before.
<br/><Space h="sm" />
<strong>Saving data in globals of other modules is not recommended, because that memory is shared by the flows managed by the same thread and lead to unexpected behaviors.</strong>
<br/><Space h="sm" />
<strong>Global variables that starts with '__firegex' are reserved for internal use, don't use them.</strong>
<br/><Space h="sm" />
You can manage when the function is called and also getting some data specifying some paramethers, using type decorators.
Default values of the paramethers will be ignored, also kvargs values will be ignored.
<br/><Space h="sm" />
<strong>Functions with no type decorator are considered invalid pyfilters!</strong>
<br/><Space h="sm" />
<CodeHighlight code={TYPING_ARGS_EXAMPLE} language="python" my="sm"/>
In this code we are filtering all the http requests that contains the word "ILLEGAL" in the body. All the other packets will be accepted (default behavior).
The function will be called only if at least internally teh HTTP request header has been parsed, and also when the body will be parsed.
<br/><Space h="sm" />
If we have multiple paramether, the function will be called only if with the packet arrived is possible to build all the paramethers.
</Text>
<Title order={2} mt="lg" mb="sm">🔧 How can I test the filter?</Title>
<Text size="lg">
You can test your filter by using <Code>fgex</Code> command installed by firegex lib: This will run a local proxy to a remote destination with the filter you specified.
<br/><Space h="sm" />
This can be done by running for instance: <Code>fgex nfproxy test_http.py 127.0.0.1 8080 --proto http</Code>
<CodeHighlight code={HELP_NFPROXY_SIM} language="" my="sm"/>
You don't need to restart the proxy every time you change the filter, the filter will be reloaded automatically.
</Text>
<Title order={2} mt="lg" mb="sm">📦 Packet Statements</Title>
<Text size="lg" my="xs">
Here there are all the statments you can return from a filter:
<List>
<List.Item><strong>ACCEPT: </strong> The packet will be accepted and forwarded to the destination. (default if None is returned)</List.Item>
<List.Item><strong>REJECT: </strong> The connection will be closed and all the packets will be dropped.</List.Item>
<List.Item><strong>DROP: </strong> This packet and all the following will be dropped. (This not simulate a connection closure)</List.Item>
<List.Item><strong>UNSTABLE_MANGLE: </strong> The packet will be modified and forwarded. You can edit the packet only with RawPacket data handler. (This is an unstable statement, use it carefully)</List.Item>
</List>
</Text>
<Title order={2} mt="lg" mb="sm">⚙️ Data Structures</Title>
<Text size="lg" my="xs">
Here there are all the data structure you can use for your filters:
</Text>
<Box display="flex" style={{alignItems: "center"}}>
<Title order={3} my="xs"><CgEditBlackPoint style={{marginBottom: -3}}/> RawPacket</Title><Space w="sm" /><TCPBadge /><HttpBadge />
</Box>
<Text size="lg">This data is the raw packet processed by nfqueue. It contains:</Text>
<Space h="sm" />
<Text size="lg" ml="xs">
<List>
<List.Item>
<strong>data: </strong> The raw packet data assembled by libtins (read only).
</List.Item>
<List.Item>
<strong>is_input: </strong> It's true if the packet is incoming, false if it's outgoing. (read only)
</List.Item>
<List.Item>
<strong>is_ipv6: </strong> It's true if the packet is IPv6, false if it's IPv4. (read only)
</List.Item>
<List.Item>
<strong>is_tcp: </strong> It's true if the packet is TCP, false if it's UDP. (read only)
</List.Item>
<List.Item>
<strong>l4_size: </strong> The size of l4 payload (read only)
</List.Item>
<List.Item>
<strong>raw_packet_header_len: </strong> The size of the raw packet header (read only)
</List.Item>
<List.Item>
<strong>raw_packet: </strong> The raw packet data with ip and TCP header. You can edit all the packet content and it will be modified if you send
the UNSTABLE_MANGLE statement. <strong>Be careful, beacause the associated layer 4 data can be different from 'data' filed that instead arrives from libtins.</strong>
When you edit this field, l4_size and l4_data will be updated automatically.
</List.Item>
<List.Item>
<strong>l4_data: </strong> The l4 payload data, directly taken by the raw packet. You can edit all the packet content and it will be modified if you send
the UNSTABLE_MANGLE statement. <strong>Be careful, beacause the associated layer 4 data can be different from 'data' filed that instead arrives from libtins.</strong> When you edit this field, l4_size and raw_packet will be updated automatically.
</List.Item>
</List>
</Text>
<Box display="flex" style={{alignItems: "center"}}>
<Title order={3} my="xs"><CgEditBlackPoint style={{marginBottom: -3}}/> TCPInputStream (alias: TCPClientStream)</Title><Space w="sm" /><TCPBadge /><HttpBadge />
</Box>
<Text size="lg">This data is the TCP input stream: this handler is called only on is_input=True packets. The filters that handles this data will be called only in this case.</Text>
<Space h="sm" />
<Text size="lg" ml="xs">
<List>
<List.Item>
<strong>data: </strong> The entire stream in input direction. (read only)
</List.Item>
<List.Item>
<strong>total_stream_size: </strong> The size of the entire stream in input direction. (read only)
</List.Item>
<List.Item>
<strong>is_ipv6: </strong> It's true if the stream is IPv6, false if it's IPv4. (read only)
</List.Item>
</List>
</Text>
<Box display="flex" style={{alignItems: "center"}}>
<Title order={3} my="xs"><CgEditBlackPoint style={{marginBottom: -3}}/> TCPOutputStream (alias TCPServerStream)</Title><Space w="sm" /><TCPBadge /><HttpBadge />
</Box>
<Text size="lg">This data is the TCP output stream: this handler is called only on is_input=False packets. The filters that handles this data will be called only in this case.</Text>
<Space h="sm" />
<Text size="lg" ml="xs">
<List>
<List.Item>
<strong>data: </strong> The entire stream in output direction. (read only)
</List.Item>
<List.Item>
<strong>total_stream_size: </strong> The size of the entire stream in output direction. (read only)
</List.Item>
<List.Item>
<strong>is_ipv6: </strong> It's true if the stream is IPv6, false if it's IPv4. (read only)
</List.Item>
</List>
</Text>
<Box display="flex" style={{alignItems: "center"}}>
<Title order={3} my="xs"><CgEditBlackPoint style={{marginBottom: -3}}/> HttpRequest</Title><Space w="sm" /><HttpBadge />
</Box>
<Text size="lg">This data is the Http request processed by nfqueue. This handler can be called twice per request: once when the http headers are complete, and once when the body is complete.</Text>
<Text size="lg">If the http data arrives in 1 single TCP packet, this handler will be called once</Text>
<Space h="sm" />
<Text size="lg" ml="xs">
<List>
<List.Item>
<strong>url: </strong> The url of the request (read only)
</List.Item>
<List.Item>
<strong>headers: </strong> The headers of the request (read only). The keys and values are exactly the same as the original request (case sensitive).
</List.Item>
<List.Item>
<strong>get_header(key:str, default = None): </strong> A function that returns the value of a header: it matches the key without case sensitivity. If the header is not found, it returns the default value.
</List.Item>
<List.Item>
<strong>user_agent: </strong> The user agent of the request (read only)
</List.Item>
<List.Item>
<strong>content_encoding: </strong> The content encoding of the request (read only)
</List.Item>
<List.Item>
<strong>content_length: </strong> The content length of the request (read only)
</List.Item>
<List.Item>
<strong>body: </strong> The body of the request (read only). It's None if the body has not arrived yet.
</List.Item>
<List.Item>
<strong>http_version: </strong> The http version of the request (read only)
</List.Item>
<List.Item>
<strong>keep_alive: </strong> It's true if the connection was marked for keep alive, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>should_upgrade: </strong> It's true if the connection should be upgraded, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>method: </strong> The method of the request (read only)
</List.Item>
<List.Item>
<strong>has_begun: </strong> It's true if the request has begun, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>headers_complete: </strong> It's true if the headers are complete, false if they are not. (read only)
</List.Item>
<List.Item>
<strong>message_complete: </strong> It's true if the message is complete, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>total_size: </strong> The size of the entire http request (read only)
</List.Item>
<List.Item>
<strong>stream: </strong> It's the buffer that contains the stream of the websocket traffic in input. This is used only if should_upgrade is True. (read only)
</List.Item>
</List>
</Text>
<Box display="flex" style={{alignItems: "center"}}>
<Title order={3} my="xs"><CgEditBlackPoint style={{marginBottom: -3}}/> HttpRequestHeader</Title><Space w="sm" /><HttpBadge />
</Box>
<Text size="lg">Same as HttpRequest, but this handler is called only when the headers are complete and body is not buffered. Body will always be None</Text>
<Box display="flex" style={{alignItems: "center"}}>
<Title order={3} my="xs"><CgEditBlackPoint style={{marginBottom: -3}}/> HttpResponse</Title><Space w="sm" /><HttpBadge />
</Box>
<Text size="lg">This data is the Http response processed by nfqueue. This handler can be called twice per response: once when the http headers are complete, and once when the body is complete.</Text>
<Text size="lg">If the http data arrives in 1 single TCP packet, this handler will be called once</Text>
<Space h="sm" />
<Text size="lg" ml="xs">
<List>
<List.Item>
<strong>url: </strong> The url of the response (read only)
</List.Item>
<List.Item>
<strong>headers: </strong> The headers of the response (read only). The keys and values are exactly the same as the original response (case sensitive).
</List.Item>
<List.Item>
<strong>get_header(key:str, default = None): </strong> A function that returns the value of a header: it matches the key without case sensitivity. If the header is not found, it returns the default value.
</List.Item>
<List.Item>
<strong>user_agent: </strong> The user agent of the response (read only)
</List.Item>
<List.Item>
<strong>content_encoding: </strong> The content encoding of the response (read only)
</List.Item>
<List.Item>
<strong>content_length: </strong> The content length of the response (read only)
</List.Item>
<List.Item>
<strong>body: </strong> The body of the response (read only). It's None if the body has not arrived yet.
</List.Item>
<List.Item>
<strong>http_version: </strong> The http version of the response (read only)
</List.Item>
<List.Item>
<strong>keep_alive: </strong> It's true if the connection was marked for keep alive, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>should_upgrade: </strong> It's true if the connection should be upgraded, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>status_code: </strong> The status code of the response (read only) (int)
</List.Item>
<List.Item>
<strong>has_begun: </strong> It's true if the response has begun, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>headers_complete: </strong> It's true if the headers are complete, false if they are not. (read only)
</List.Item>
<List.Item>
<strong>message_complete: </strong> It's true if the message is complete, false if it's not. (read only)
</List.Item>
<List.Item>
<strong>total_size: </strong> The size of the entire http response (read only)
</List.Item>
<List.Item>
<strong>stream: </strong> It's the buffer that contains the stream of the websocket traffic in output. This is used only if should_upgrade is True. (read only)
</List.Item>
</List>
</Text>
<Box display="flex" style={{alignItems: "center"}}>
<Title order={3} my="xs"><CgEditBlackPoint style={{marginBottom: -3}}/> HttpResponseHeader</Title><Space w="sm" /><HttpBadge />
</Box>
<Text size="lg">Same as HttpResponse, but this handler is called only when the headers are complete and body is not buffered. Body will always be None</Text>
<Title order={2} mt="lg" mb="sm">⚠️ Stream Limiter</Title>
<Text size="lg" my="xs">
What happen if in a specific TCP stream you have a lot of data? The stream limiter will be activated and some action will be taken.
You can configure the action performed by setting some option in the globals:
<br /><Space h="sm" />
First import the FullStreamAction enum:
<CodeHighlight code={IMPORT_FULL_ACTION_STREAM} language="python" my="sm" />
Then you can set in the globals these options:
<List>
<List.Item>
<strong>FGEX_STREAM_MAX_SIZE: </strong> Sets the maximum size of the stream. If the stream exceeds this size, the FGEX_FULL_STREAM_ACTION will be performed. (this limit is applyed at the single stream related to the single data handler).
For example if TCPInputStream has reached the limit but HttpResponse has not, the action will be performed only on the TCPInputStream. The default is 1MB.
</List.Item>
<List.Item>
<strong>FGEX_FULL_STREAM_ACTION: </strong> Sets the action performed when the stream exceeds the FGEX_STREAM_MAX_SIZE. The default is FullStreamAction.FLUSH.
</List.Item>
</List>
Heres will be explained every type of action you can set:
<List>
<List.Item>
<strong>FLUSH: </strong> Flush the stream and continue to acquire new packets (default)
</List.Item>
<List.Item>
<strong>DROP: </strong> Drop the next stream packets - like a DROP action by filter
</List.Item>
<List.Item>
<strong>REJECT: </strong> Reject the stream and close the connection - like a REJECT action by filter
</List.Item>
<List.Item>
<strong>ACCEPT: </strong> Stops to call pyfilters and accept the traffic
</List.Item>
</List>
</Text>
<Title order={2} mt="lg" mb="sm">🚀 How It Works</Title>
<Text mb="sm" size="lg">
The proxy is built on a multi-threaded architecture and integrates Python for dynamic filtering:
</Text>
<List>
<List.Item>
<Text size="lg">
<strong>Packet Interception: </strong>
The <a href="https://netfilter.org/projects/libnetfilter_queue/">nfqueue</a> kernel module intercepts network packets(a <a href="https://netfilter.org/">netfilter</a> module) 🔍<br />
The rules for attach the nfqueue on the network traffic is done by the nftables lib with json APIs by the python manager.
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>Packet Reading: </strong>
A dedicated thread reads packets from <a href="https://netfilter.org/projects/libnetfilter_queue/">nfqueue</a>. 🧵
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>Multi-threaded Analysis: </strong>
The C++ binary launches multiple threads, each starting its own Python interpreter.
Thanks to Python 3.12s support for <a href="https://peps.python.org/pep-0684/">a per-interpeter GIL</a>, real multithreading is achieved.
Traffic is distributed among threads based on IP addresses and port hashing, ensuring that
packets belonging to the same flow are processed by the same thread. ⚡️
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>Python Filter Integration: </strong>
Users can upload custom Python filters which are then executed by the interpreter,
allowing for dynamic and flexible packet handling. 🐍
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>HTTP Parsing: </strong>
<a href="https://github.com/domysh/pyllhttp">A Python wrapper for llhttp</a> (forked and adapted for working with multi-interpeters) is used to parse HTTP connections, making it easier to handle
and analyze HTTP traffic. 📡
</Text>
</List.Item>
</List>
<Space h="xl" />
<Title order={2} mt="lg" mb="sm">📚 Additional Resources</Title>
<Text size="lg">
Here's a pyfilter code commented example:
<CodeHighlight code={EXAMPLE_PYFILTER} language="python" my="sm"/>
</Text>
</Container>
);
};

View File

@@ -0,0 +1,68 @@
import { Button, Group, Space, TextInput, Notification, Modal } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { okNotify } from '../../../js/utils';
import { ImCross } from "react-icons/im"
import { nfproxy, Service } from '../utils';
function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:Service }) {
const form = useForm({
initialValues: { name:service.name },
validate:{ name: (value) => value !== ""? null : "Service name is required" }
})
const close = () =>{
onClose()
form.reset()
setError(null)
}
useEffect(()=> form.setFieldValue("name", service.name),[opened])
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name }:{ name:string }) => {
setSubmitLoading(true)
nfproxy.servicerename(service.service_id, name).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Service ${service.name} has been renamed in ${ name }`, `Successfully renamed service on port ${service.port}`)
}else{
setSubmitLoading(false)
setError("Error: [ "+res+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
return <Modal size="xl" title={`Rename '${service.name}' service on port ${service.port}`} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
<TextInput
label="Service Name"
placeholder="Awesome Service Name!"
{...form.getInputProps('name')}
/>
<Group mt="md" justify="flex-end" mb="sm">
<Button loading={submitLoading} type="submit">Rename</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default RenameForm;

View File

@@ -0,0 +1,164 @@
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa';
import { nfproxy, Service, serviceQueryKey } from '../utils';
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm';
import { MenuDropDownWithButton } from '../../MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { TbPlugConnected } from "react-icons/tb";
import { FaFilter } from "react-icons/fa";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../AddEditService';
import { FaPencilAlt } from "react-icons/fa";
import { ExceptionWarning } from '../ExceptionWarning';
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
let status_color = "gray";
switch(service.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const queryClient = useQueryClient()
const [buttonLoading, setButtonLoading] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const isMedium = isMediumScreen()
const stopService = async () => {
setButtonLoading(true)
await nfproxy.servicestop(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
})
setButtonLoading(false);
}
const startService = async () => {
setButtonLoading(true)
await nfproxy.servicestart(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
})
setButtonLoading(false)
}
const deleteService = () => {
nfproxy.servicedelete(service.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
return <>
<Box className='firegex__nfregex__rowbox'>
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name}
</Title>
</Box>
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
:{service.port}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Box className={isMedium?"center-flex":"center-flex-row"}>
<Box className="center-flex-row">
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
<Space h="xs" />
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
<Space w="xs" />
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
</Box>
</Box>
{isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex">
<ExceptionWarning service_id={service.service_id} />
<Space w="sm"/>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={service.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
{isMedium?<Space w="xl" />:<Space w="md" />}
{onClick?<Box className='firegex__service_forward_btn'>
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
</Box>:null}
</Box>
</Box>
</Box>
</Box>
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
onClose={()=>setDeleteModal(false) }
action={deleteService}
opened={deleteModal}
/>
<RenameForm
onClose={()=>setRenameModal(false)}
opened={renameModal}
service={service}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={service}
/>
</>
}

View File

@@ -0,0 +1,54 @@
import { Button, FileButton, Group, Modal, Notification, Space } from "@mantine/core";
import { nfproxy, Service } from "./utils";
import { useEffect, useState } from "react";
import { ImCross } from "react-icons/im";
import { okNotify } from "../../js/utils";
export const UploadFilterModal = ({ opened, onClose, service }: { opened: boolean, onClose: () => void, service?: Service }) => {
const close = () =>{
onClose()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
if (opened && file){
file.bytes().then( code => {
console.log(code.toString())
setSubmitLoading(true)
nfproxy.setpyfilterscode(service?.service_id??"",code.toString()).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Service ${name} code updated`, `Successfully updated code for service ${name}`)
}
}).catch( err => {
setSubmitLoading(false)
setError("Error: "+err)
})
})
}
}, [opened, file])
return <Modal opened={opened && service != null} onClose={onClose} title="Upload filter Code" size="xl" closeOnClickOutside={false} centered>
<Space h="md" />
<Group justify="center">
<FileButton onChange={setFile} accept=".py" multiple={false}>
{(props) => <Button {...props}>Upload filter python code</Button>}
</FileButton>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification>
</>:null}
<Space h="md" />
</Modal>
}

View File

@@ -0,0 +1,175 @@
import { PyFilter, ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { useQuery } from "@tanstack/react-query"
export type Service = {
service_id:string,
name:string,
status:string,
port:number,
proto: string,
ip_int: string,
n_filters:number,
edited_packets:number,
blocked_packets:number,
fail_open:boolean,
}
export type ServiceAddForm = {
name:string,
port:number,
proto:string,
ip_int:string,
fail_open: boolean,
}
export type ServiceSettings = {
port?:number,
ip_int?:string,
fail_open?: boolean,
}
export type ServiceAddResponse = {
status: string,
service_id?: string,
}
export const serviceQueryKey = ["nfproxy","services"]
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
queryFn:() => nfproxy.servicepyfilters(service_id)
})
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
queryFn:() => nfproxy.getpyfilterscode(service_id)
})
export const nfproxy = {
services: async () => {
return await getapi("nfproxy/services") as Service[];
},
serviceinfo: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}`) as Service;
},
pyfilterenable: async (filter_name:string) => {
const { status } = await postapi(`nfproxy/pyfilters/${filter_name}/enable`) as ServerResponse;
return status === "ok"?undefined:status
},
pyfilterdisable: async (filter_name:string) => {
const { status } = await postapi(`nfproxy/pyfilters/${filter_name}/disable`) as ServerResponse;
return status === "ok"?undefined:status
},
servicestart: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
return status === "ok"?undefined:status
},
servicerename: async (service_id:string, name: string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
servicestop: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
return status === "ok"?undefined:status
},
servicesadd: async (data:ServiceAddForm) => {
return await postapi("nfproxy/services",data) as ServiceAddResponse;
},
servicedelete: async (service_id:string) => {
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
return status === "ok"?undefined:status
},
servicepyfilters: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
},
settings: async (service_id:string, data:ServiceSettings) => {
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
return status === "ok"?undefined:status
},
getpyfilterscode: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/pyfilters/code`) as string;
},
setpyfilterscode: async (service_id:string, code:string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/pyfilters/code`,{ code }) as ServerResponse;
return status === "ok"?undefined:status
}
}
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
# From here we can import the DataTypes that we want to use:
# The data type must be specified in the filter functions
# And will also interally be used to decide when call some filters and how aggregate data
from firegex.nfproxy.models import RawPacket
# global context in this execution is dedicated to a single TCP stream
# - This code will be executed once at the TCP stream start
# - The filter will be called for each packet in the stream
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
# - At the end of the stream the global context will be destroyed
from firegex.nfproxy import pyfilter
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
# - The filter must return one of the following values:
# - ACCEPT: The packet will be accepted
# - REJECT: The packet will be rejected (will be activated a mechanism to send a FIN packet and drop all data in the stream)
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
# - DROP: All the packets in this stream will be easly dropped
# If you want, you can use print to debug your filters, but this could slow down the filter
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
@pyfilter
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
def strange_filter(packet:RawPacket):
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
# Also is not garanteed that l4_data is the same of the packet data:
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
# Unorder packets in TCP are accepted by default, and python is not called in this case
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
if b"TEST_MANGLING" in packet.l4_data:
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
return UNSTABLE_MANGLE
# Drops the traffic
if b"BAD DATA 1" in packet.data:
return DROP
# Rejects the traffic
if b"BAD DATA 2" in packet.data:
return REJECT
# Accepts the traffic (default if None is returned)
return ACCEPT
# Example with a higher level of abstraction
@pyfilter
def http_filter(http:HTTPRequest):
if http.method == "GET" and "test" in http.url:
return REJECT
# ADVANCED OPTIONS
# You can specify some additional options on the streaming managment
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
#
# If the stream is too big, you can specify what actions to take:
# This can be done defining some variables in the global context
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
# Only types required by at least 1 filter will be stored.
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
from firege.nfproxy import FullStreamAction
# Example of a global context
FGEX_STREAM_MAX_SIZE = 4096
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
`

View File

@@ -0,0 +1,69 @@
import { Container, Title, Text, List } from "@mantine/core";
export const NFRegexDocs = () => {
return (
<Container size="xl">
<Title order={1}>📡 Netfilter Regex Documentation</Title>
<Title order={2} mt="xl" mb="sm">📖 Overview</Title>
<Text size="lg">
Netfilter Regex is a powerful feature that enables filtering of network packets using regular expressions. This capability is especially useful when you need to inspect packet content and match specific strings or patterns.
</Text>
<Title order={2} mt="lg" mb="sm"> How to Use Netfilter Regex</Title>
<Text size="lg">
To get started, create a service and attach a regular expression to it. Once the service is configured, apply it to a network interface to dynamically filter packets based on the defined regex.
</Text>
<Title order={2} mt="lg" mb="sm">🚀 How It Works</Title>
<Text mb="sm" size="lg">
The packet filtering process is implemented in C++ and involves several key steps:
</Text>
<List>
<List.Item>
<Text size="lg">
<strong>Packet Interception: </strong>
The <a href="https://netfilter.org/projects/libnetfilter_queue/">nfqueue</a> kernel module intercepts network packets (a <a href="https://netfilter.org/">netfilter</a> module) 🔍<br />
The rules for attach the nfqueue on the network traffic is done by the nftables lib with json APIs by the python manager.
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>Packet Reading: </strong>
A dedicated thread reads packets from <a href="https://netfilter.org/projects/libnetfilter_queue/">nfqueue</a>. 🧵
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>Packet Parsing: </strong>
Intercepted packets are parsed by <a href="https://libtins.github.io/">libtins</a>, a C++ library that extracts the payload from each packet. 📄
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>Multi-threaded Analysis: </strong>
Multiple threads analyze packets concurrently.
While the <a href="https://netfilter.org/projects/libnetfilter_queue/">nfqueue</a> module balances network
load based solely on IP addressesresulting in a single thread handling traffic in NAT environments
like CTF networks, firegex manage this threads user-level in a different way.
The traffic is routed in the threads based on IP addresses combined with port hashing,
ensuring a more balanced workload and that flows will be analyzed by the same thread.
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>TCP Handling: </strong>
For TCP connections, <a href="https://libtins.github.io/">libtins</a> employs a TCP follower to order packets received from the kernel. 📈
</Text>
</List.Item>
<List.Item>
<Text size="lg">
<strong>Regex Matching: </strong>
The extracted payload is processed using <a href="https://github.com/VectorCamp/vectorscan">vectorscan</a> a fork of <a href="https://github.com/intel/hyperscan">hyperscan</a> that runs also on arm64.
For UDP packets, matching occurs on a per-packet basis while saving only the match context rather than the full payload. 🎯
</Text>
</List.Item>
</List>
</Container>
);
};

View File

@@ -25,7 +25,6 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [buttonLoading, setButtonLoading] = useState(false) const [buttonLoading, setButtonLoading] = useState(false)
const [tooltipStopOpened, setTooltipStopOpened] = useState(false);
const [deleteModal, setDeleteModal] = useState(false) const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false) const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false) const [editModal, setEditModal] = useState(false)
@@ -115,13 +114,11 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item> <Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton> </MenuDropDownWithButton>
<Space w="md"/> <Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}> <Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading} <ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled" onClick={stopService} size="xl" radius="md" variant="filled"
disabled={service.status === "stop"} disabled={service.status === "stop"}
aria-describedby="tooltip-stop-id" aria-describedby="tooltip-stop-id">
onFocus={() => setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)}
onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}>
<FaStop size="20px" /> <FaStop size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>

View File

@@ -36,7 +36,6 @@ export type ServiceAddResponse = {
} }
export const serviceQueryKey = ["nfregex","services"] export const serviceQueryKey = ["nfregex","services"]
export const statsQueryKey = ["nfregex","stats"]
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services}) export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({ export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({

View File

@@ -1,12 +1,12 @@
import { Collapse, Divider, Group, MantineColor, ScrollArea, Text, ThemeIcon, Title, UnstyledButton, Box, AppShell } from "@mantine/core"; import { Divider, Group, MantineColor, ScrollArea, Text, ThemeIcon, Title, UnstyledButton, Box, AppShell } from "@mantine/core";
import { useState } from "react"; import { useState } from "react";
import { IoMdGitNetwork } from "react-icons/io"; import { TbPlugConnected } from "react-icons/tb";
import { MdOutlineExpandLess, MdOutlineExpandMore, MdTransform } from "react-icons/md";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { GrDirections } from "react-icons/gr"; import { GrDirections } from "react-icons/gr";
import { PiWallLight } from "react-icons/pi"; import { PiWallLight } from "react-icons/pi";
import { useNavbarStore } from "../../js/store"; import { useNavbarStore } from "../../js/store";
import { getMainPath } from "../../js/utils"; import { getMainPath } from "../../js/utils";
import { BsRegex } from "react-icons/bs";
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }: function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
{ navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) { { navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) {
@@ -36,9 +36,15 @@ export default function NavBar() {
</Box> </Box>
<Divider my="xs" /> <Divider my="xs" />
<Box style={{flexGrow: 1}} component={ScrollArea} px="xs" mt="xs"> <Box style={{flexGrow: 1}} component={ScrollArea} px="xs" mt="xs">
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="lime" icon={<IoMdGitNetwork />} /> <NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="grape" icon={<BsRegex size={19} />} />
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight />} /> <NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections />} /> <NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
<Box px="xs" mt="lg">
<Title order={5}>Experimental Features 🧪</Title>
</Box>
<Text></Text>
<Divider my="xs" />
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
</Box> </Box>
</AppShell.Navbar> </AppShell.Navbar>

View File

@@ -0,0 +1,37 @@
import { CodeHighlight } from "@mantine/code-highlight"
import { Code, Container, Space, Text, Title } from "@mantine/core"
import { HELP_NFPROXY_SIM } from "../NFProxy/NFProxyDocs"
export const PortHijackDocs = () => {
return <Container size="xl">
<Title order={1}> Hijack port to proxy</Title>
<Title order={2} mt="xl" mb="sm">📖 Overview</Title>
<Text size="lg">
'Hijack port to proxy' uses <a href="https://netfilter.org/">nftables</a> to redirect the traffic from an external IP to a localhost server.
You are responsable to run and keep alive this server, that is your proxy. The original service will be accessible using loopback (127.0.0.1).
In this way you can run your custom proxy without touching the service configuration.
</Text>
<Title order={2} mt="lg" mb="sm"> How to use Hijack port to proxy</Title>
<Text size="lg">
To use this feature, simply create your proxy, run it, than create a new service and set the proxy port and the external ip and port.
The traffic will be redirected to your proxy, that will still be able to contact the original service using loopback.
The responses of your proxy will be redirected to the original client, and teh proxy will see as the requests were made by the original client.
<br /><Space h="sm" />
You can use for instance the proxy simulator of nfproxy feature of firegex, and run it using nfproxy features. This will advantage you if for instance you need to mangle the traffic.
changing packets it's possible but not sure to do with nfproxy, but the simulator can change the packets normally (on PacketRaw data is always == l4_data in the simulator, check the nfproxy docs for more info)
<br /><Space h="sm" />
You will need to install firegex library with <Code>pip install -U fgex</Code> and than use the simulator command
<CodeHighlight code={HELP_NFPROXY_SIM} language="" my="sm"/>
for instance: <Code>fgex nfproxy test_http.py 127.0.0.1 8080 --proto http --from-port 13377</Code>
</Text>
<Title order={2} mt="lg" mb="sm">🚀 How It Works</Title>
<Text mb="sm" size="lg">
This modules works in a simple way: this only thing done is to change the destination and source ip using <a href="https://netfilter.org/">nftables</a> rules so that the kernel will see that the request was done to the proxy port,
but externaly the packets exists as connections to the original service. This mangle is done only for external packet arriving from the external ip indicated, localhost traffic won't be touched.
</Text>
<Space h="xl" />
</Container>
}

View File

@@ -1,5 +1,5 @@
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core'; import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
import React, { useState } from 'react'; import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa'; import { FaPlay, FaStop } from 'react-icons/fa';
import { porthijack, Service } from '../utils'; import { porthijack, Service } from '../utils';
import YesNoModal from '../../YesNoModal'; import YesNoModal from '../../YesNoModal';
@@ -17,11 +17,9 @@ export default function ServiceRow({ service }:{ service:Service }) {
let status_color = service.active ? "teal": "red" let status_color = service.active ? "teal": "red"
const [buttonLoading, setButtonLoading] = useState(false) const [buttonLoading, setButtonLoading] = useState(false)
const [tooltipStopOpened, setTooltipStopOpened] = useState(false);
const [deleteModal, setDeleteModal] = useState(false) const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false) const [renameModal, setRenameModal] = useState(false)
const [changeDestModal, setChangeDestModal] = useState(false) const [changeDestModal, setChangeDestModal] = useState(false)
const portInputRef = React.createRef<HTMLInputElement>()
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const form = useForm({ const form = useForm({
@@ -113,13 +111,11 @@ export default function ServiceRow({ service }:{ service:Service }) {
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item> <Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton> </MenuDropDownWithButton>
<Space w="md"/> <Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}> <Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading} <ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled" onClick={stopService} size="xl" radius="md" variant="filled"
disabled={!service.active} disabled={!service.active}
aria-describedby="tooltip-stop-id" aria-describedby="tooltip-stop-id">
onFocus={() => setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)}
onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}>
<FaStop size="20px" /> <FaStop size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>

View File

@@ -0,0 +1,44 @@
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
import { useState } from 'react';
import { PyFilter } from '../../js/models';
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
import { FaPause, FaPlay } from 'react-icons/fa';
import { FaFilter } from "react-icons/fa";
import { nfproxy } from '../NFProxy/utils';
import { FaPencilAlt } from 'react-icons/fa';
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
const isMedium = isMediumScreen()
const changeRegexStatus = () => {
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.name).then(res => {
if(!res){
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
}else{
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
}
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
}
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
{filterInfo.name}
<Box className='flex-spacer' />
<Space w="xs" />
{isMedium?<>
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
<Space w="xs" />
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
<Space w="lg" />
</>:null}
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip>
</Box>
</Box>
}

View File

@@ -1,13 +1,14 @@
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { RegexFilter } from '../../js/models'; import { RegexFilter } from '../../js/models';
import { b64decode, errorNotify, getapiobject, isMediumScreen, okNotify } from '../../js/utils'; import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
import { BsTrashFill } from "react-icons/bs" import { BsTrashFill } from "react-icons/bs"
import YesNoModal from '../YesNoModal'; import YesNoModal from '../YesNoModal';
import { FaPause, FaPlay } from 'react-icons/fa'; import { FaPause, FaPlay } from 'react-icons/fa';
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import { FaFilter } from "react-icons/fa"; import { FaFilter } from "react-icons/fa";
import { VscRegex } from "react-icons/vsc";
import { nfregex } from '../NFRegex/utils';
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
@@ -18,13 +19,10 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
let regex_expr = b64decode(regexInfo.regex); let regex_expr = b64decode(regexInfo.regex);
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
const [deleteTooltipOpened, setDeleteTooltipOpened] = useState(false);
const [statusTooltipOpened, setStatusTooltipOpened] = useState(false);
const clipboard = useClipboard({ timeout: 500 }); const clipboard = useClipboard({ timeout: 500 });
const isMedium = isMediumScreen();
const deleteRegex = () => { const deleteRegex = () => {
getapiobject().regexdelete(regexInfo.id).then(res => { nfregex.regexdelete(regexInfo.id).then(res => {
if(!res){ if(!res){
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`) okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
}else{ }else{
@@ -34,9 +32,9 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
} }
const changeRegexStatus = () => { const changeRegexStatus = () => {
(regexInfo.active?getapiobject().regexdisable:getapiobject().regexenable)(regexInfo.id).then(res => { (regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
if(!res){ if(!res){
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been ${regexInfo.active?"deactivated":"activated"}!`) okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
}else{ }else{
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`) errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
} }
@@ -53,18 +51,14 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
}}>{regex_expr}</Text> }}>{regex_expr}</Text>
</Box> </Box>
<Space w="xs" /> <Space w="xs" />
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"} opened={statusTooltipOpened}> <Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled" <ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
onFocus={() => setStatusTooltipOpened(false)} onBlur={() => setStatusTooltipOpened(false)}
onMouseEnter={() => setStatusTooltipOpened(true)} onMouseLeave={() => setStatusTooltipOpened(false)}
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon> >{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Delete regex" zIndex={0} color="red" opened={deleteTooltipOpened} > <Tooltip label="Delete regex" zIndex={0} color="red" >
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled" <ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
onFocus={() => setDeleteTooltipOpened(false)} onBlur={() => setDeleteTooltipOpened(false)} <BsTrashFill size={22} /></ActionIcon>
onMouseEnter={() => setDeleteTooltipOpened(true)} onMouseLeave={() => setDeleteTooltipOpened(false)}
><BsTrashFill size={22} /></ActionIcon>
</Tooltip> </Tooltip>
</Box> </Box>
<Box display="flex" mt="sm" ml="xs"> <Box display="flex" mt="sm" ml="xs">

View File

@@ -96,6 +96,20 @@ body {
opacity: 0.8; opacity: 0.8;
} }
.firegex__regexview__pyfilter_text{
padding: 6px;
padding-left: 15px;
padding-right: 15px;
background-color: var(--fourth_color);
border: 1px solid #444;
overflow-x: hidden;
border-radius: 8px;
}
.firegex__regexview__pyfilter_text:hover{
overflow-x: auto;
}
.firegex__porthijack__servicerow__row{ .firegex__porthijack__servicerow__row{
width: 95%; width: 95%;
padding: 15px 0px; padding: 15px 0px;

View File

@@ -9,6 +9,7 @@ import {
import { queryClient } from './js/utils'; import { queryClient } from './js/utils';
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css'; import '@mantine/notifications/styles.css';
import '@mantine/code-highlight/styles.css';
import './index.css'; import './index.css';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(

View File

@@ -48,4 +48,11 @@ export type RegexAddForm = {
is_case_sensitive:boolean, is_case_sensitive:boolean,
mode:string, // C->S S->C BOTH, mode:string, // C->S S->C BOTH,
active: boolean active: boolean
}
export type PyFilter = {
name:string,
blocked_packets:number,
edited_packets:number,
active:boolean
} }

View File

@@ -2,11 +2,11 @@ import { showNotification } from "@mantine/notifications";
import { ImCross } from "react-icons/im"; import { ImCross } from "react-icons/im";
import { TiTick } from "react-icons/ti" import { TiTick } from "react-icons/ti"
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { nfregex } from "../components/NFRegex/utils";
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models"; import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
import { Buffer } from "buffer" import { Buffer } from "buffer"
import { QueryClient, useQuery } from "@tanstack/react-query"; import { QueryClient, useQuery } from "@tanstack/react-query";
import { useMediaQuery } from "@mantine/hooks"; import { useMediaQuery } from "@mantine/hooks";
import { io } from "socket.io-client";
export const IS_DEV = import.meta.env.DEV export const IS_DEV = import.meta.env.DEV
@@ -18,6 +18,31 @@ export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$" export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
export const DEV_IP_BACKEND = "127.0.0.1:4444" export const DEV_IP_BACKEND = "127.0.0.1:4444"
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
return value as E;
}
export const socketio = import.meta.env.DEV?
io("ws://"+DEV_IP_BACKEND, {
path:"/sock/socket.io",
transports: ['websocket'],
auth: {
token: localStorage.getItem("access_token")
}
}):
io({
path:"/sock/socket.io",
transports: ['websocket'],
auth: {
token: localStorage.getItem("access_token")
}
})
export const queryClient = new QueryClient({ defaultOptions: { queries: { export const queryClient = new QueryClient({ defaultOptions: { queries: {
staleTime: Infinity staleTime: Infinity
} }}) } }})
@@ -71,9 +96,14 @@ export async function genericapi(method:string,path:string,data:any = undefined,
const errorDefault = res.statusText const errorDefault = res.statusText
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault)) return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
} }
res.json().then( res => resolve(res) ).catch( err => reject(err)) res.text().then(t => {
}) try{
.catch(err => { resolve(JSON.parse(t))
}catch(e){
resolve(t)
}
}).catch( err => reject(err))
}).catch(err => {
reject(err) reject(err)
}) })
}); });
@@ -101,14 +131,6 @@ export function getMainPath(){
return "" return ""
} }
export function getapiobject(){
switch(getMainPath()){
case "nfregex":
return nfregex
}
throw new Error('No api for this tool!');
}
export function HomeRedirector(){ export function HomeRedirector(){
const section = sessionStorage.getItem("home_section") const section = sessionStorage.getItem("home_section")
const path = section?`/${section}`:`/nfregex` const path = section?`/${section}`:`/nfregex`

View File

@@ -1,4 +1,4 @@
import { ActionIcon, Badge, Box, Divider, FloatingIndicator, LoadingOverlay, Space, Switch, Table, Tabs, TextInput, Title, Tooltip, useMantineTheme } from "@mantine/core" import { ActionIcon, Badge, Box, Divider, FloatingIndicator, LoadingOverlay, Space, Switch, Table, Tabs, TextInput, ThemeIcon, Title, Tooltip, useMantineTheme } from "@mantine/core"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { BsPlusLg, BsTrashFill } from "react-icons/bs" import { BsPlusLg, BsTrashFill } from "react-icons/bs"
import { rem } from '@mantine/core'; import { rem } from '@mantine/core';
@@ -20,16 +20,12 @@ import { LuArrowBigRightDash } from "react-icons/lu"
import { ImCheckmark, ImCross } from "react-icons/im"; import { ImCheckmark, ImCross } from "react-icons/im";
import { IoSettingsSharp } from "react-icons/io5"; import { IoSettingsSharp } from "react-icons/io5";
import { SettingsModal } from "./SettingsModal"; import { SettingsModal } from "./SettingsModal";
import { FaDirections } from "react-icons/fa";
import { PiWallLight } from "react-icons/pi";
export const Firewall = () => { export const Firewall = () => {
const [tooltipAddOpened, setTooltipAddOpened] = useState(false);
const [tooltipRefreshOpened, setTooltipRefreshOpened] = useState(false);
const [tooltipApplyOpened, setTooltipApplyOpened] = useState(false);
const [tooltipSettingsOpened, setTooltipSettingsOpened] = useState(false);
const [currentPolicy, setCurrentPolicy] = useState<ActionType>(ActionType.ACCEPT) const [currentPolicy, setCurrentPolicy] = useState<ActionType>(ActionType.ACCEPT)
const [tooltipAddRulOpened, setTooltipAddRulOpened] = useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const rules = firewallRulesQuery() const rules = firewallRulesQuery()
const [state, handlers] = useListState<Rule & {rule_id:string}>([]); const [state, handlers] = useListState<Rule & {rule_id:string}>([]);
@@ -346,7 +342,7 @@ export const Firewall = () => {
<Space h="sm" /> <Space h="sm" />
<LoadingOverlay visible={rules.isLoading} /> <LoadingOverlay visible={rules.isLoading} />
<Box className={isMedium?'center-flex':'center-flex-row'}> <Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={3}>Firewall Rules</Title> <Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='red' ><PiWallLight size={20} /></ThemeIcon><Space w="xs" />Firewall Rules</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />} {isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex'> <Box className='center-flex'>
Enabled: <Space w="sm" /> <Switch checked={fwEnabled} onChange={switchState} /> Enabled: <Space w="sm" /> <Switch checked={fwEnabled} onChange={switchState} />
@@ -361,33 +357,24 @@ export const Firewall = () => {
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />} {isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex'> <Box className='center-flex'>
<Space w="xs" /> <Space w="xs" />
<Badge size="sm" color="green" variant="filled">Rules: {rules.isLoading?0:rules.data?.rules.length}</Badge> <Badge size="md" radius="sm" color="green" variant="filled"><FaDirections style={{ marginBottom: -1, marginRight: 4}}/>Rules: {rules.isLoading?0:rules.data?.rules.length}</Badge>
<Space w="xs" /> <Space w="md" />
<Tooltip label="Add a new rule" position='bottom' color="blue" opened={tooltipAddOpened}> <Tooltip label="Add a new rule" position='bottom' color="blue">
<ActionIcon color="blue" onClick={emptyRuleAdd} size="lg" radius="md" variant="filled" <ActionIcon color="blue" onClick={emptyRuleAdd} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}><BsPlusLg size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo" opened={tooltipRefreshOpened}> <Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["firewall"])} size="lg" radius="md" variant="filled" <ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["firewall"])} size="lg" radius="md" variant="filled"
loading={rules.isFetching} loading={rules.isFetching}><TbReload size={18} /></ActionIcon>
onFocus={() => setTooltipRefreshOpened(false)} onBlur={() => setTooltipRefreshOpened(false)}
onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}><TbReload size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Settings" position='bottom' color="cyan" opened={tooltipSettingsOpened}> <Tooltip label="Settings" position='bottom' color="cyan">
<ActionIcon color="cyan" onClick={()=>setSettingsModal(true)} size="lg" radius="md" variant="filled" <ActionIcon color="cyan" onClick={()=>setSettingsModal(true)} size="lg" radius="md" variant="filled"><IoSettingsSharp size={18} /></ActionIcon>
onFocus={() => setTooltipSettingsOpened(false)} onBlur={() => setTooltipSettingsOpened(false)}
onMouseEnter={() => setTooltipSettingsOpened(true)} onMouseLeave={() => setTooltipSettingsOpened(false)}><IoSettingsSharp size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Apply" position='bottom' color="grape" opened={tooltipApplyOpened}> <Tooltip label="Apply" position='bottom' color="grape">
<ActionIcon color="grape" onClick={applyChanges} size="lg" radius="md" variant="filled" <ActionIcon color="grape" onClick={applyChanges} size="lg" radius="md" variant="filled" disabled={!valuesChanged}>
onFocus={() => setTooltipApplyOpened(false)} onBlur={() => setTooltipApplyOpened(false)} <TiTick size={22} /></ActionIcon>
onMouseEnter={() => setTooltipApplyOpened(true)} onMouseLeave={() => setTooltipApplyOpened(false)}
disabled={!valuesChanged}
><TiTick size={22} /></ActionIcon>
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
@@ -420,16 +407,20 @@ export const Firewall = () => {
)} )}
</Droppable> </Droppable>
</DragDropContext>:<> </DragDropContext>:<>
<Space h="xl"/> <Title className='center-flex' style={{textAlign:"center"}} order={3}>No rule found! Add one clicking the "+" buttons</Title> <Box className='center-flex-row'>
<Space h="xl" /> <Space h="xl" /> <Space h="xl" />
<Box className='center-flex'> <Title className='center-flex' style={{textAlign:"center"}} order={3}>Firewall Rules allows you to use nftables but through a web interface</Title>
<Tooltip label="Add a new rule" color="blue" opened={tooltipAddRulOpened}> <Space h="xs" />
<ActionIcon color="blue" onClick={emptyRuleAdd} size="xl" radius="md" variant="filled" <Title className='center-flex' style={{textAlign:"center"}} order={5}>Add new rules, sort it and enable the firewall: be carefull, wrong rules could also drops out firegex access</Title>
onFocus={() => setTooltipAddRulOpened(false)} onBlur={() => setTooltipAddRulOpened(false)} <Space h="lg" />
onMouseEnter={() => setTooltipAddRulOpened(true)} onMouseLeave={() => setTooltipAddRulOpened(false)}><BsPlusLg size="20px" /></ActionIcon> <Box className='center-flex' style={{gap: 20}}>
</Tooltip> <Tooltip label="Add a new rule" color="blue">
</Box> <ActionIcon color="blue" onClick={emptyRuleAdd} size="xl" radius="md" variant="filled">
</>} <BsPlusLg size="20px" /></ActionIcon>
</Tooltip>
</Box>
</Box>
</>}
<YesNoModal <YesNoModal
title='Are you sure to apply the changes to the firewall?' title='Are you sure to apply the changes to the firewall?'

View File

@@ -0,0 +1,237 @@
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router-dom';
import { Badge, Divider, Menu } from '@mantine/core';
import { useEffect, useState } from 'react';
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
import { MdDoubleArrow } from "react-icons/md"
import YesNoModal from '../../components/YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
import { MenuDropDownWithButton } from '../../components/MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaArrowLeft } from "react-icons/fa";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../../components/NFProxy/AddEditService';
import PyFilterView from '../../components/PyFilterView';
import { TbPlugConnected } from 'react-icons/tb';
import { CodeHighlight } from '@mantine/code-highlight';
import { FaPython } from "react-icons/fa";
import { FiFileText } from "react-icons/fi";
import { ModalLog } from '../../components/ModalLog';
import { useListState } from '@mantine/hooks';
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
import { DocsButton } from '../../components/DocsButton';
export default function ServiceDetailsNFProxy() {
const {srv} = useParams()
const services = nfproxyServiceQuery()
const serviceInfo = services.data?.find(s => s.service_id == srv)
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient()
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
const navigate = useNavigate()
const isMedium = isMediumScreen()
const [openLogModal, setOpenLogModal] = useState(false)
const [logData, logDataSetters] = useListState<string>([]);
useEffect(()=>{
if (srv){
if (openLogModal){
logDataSetters.setState([])
socketio.emit("nfproxy-outstream-join", { service: srv });
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
logDataSetters.append(data)
});
}else{
socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([])
}
return () => {
socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([])
}
}
}, [openLogModal, srv])
if (services.isLoading) return <LoadingOverlay visible={true} />
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
let status_color = "gray";
switch(serviceInfo.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const startService = async () => {
setButtonLoading(true)
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false)
}
const deleteService = () => {
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
const stopService = async () => {
setButtonLoading(true)
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false);
}
return <>
<LoadingOverlay visible={filtersList.isLoading} />
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box>
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
</Box>
</Title>
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex'>
<ExceptionWarning service_id={srv} />
<Space w="sm" />
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
{serviceInfo.status}
</Badge>
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
:{serviceInfo.port}
</Badge>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Show logs" zIndex={0} color="cyan">
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
<FiFileText size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
{isMedium?null:<Space h="md" />}
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Box className='center-flex'>
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
</Box>
{isMedium?<Space w="xs" />:<Space h="xs" />}
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
</Box>
{isMedium?null:<Space h="xl" />}
<Box className='center-flex'>
<Tooltip label="Go back" zIndex={0} color="cyan">
<ActionIcon color="cyan"
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-back-id">
<FaArrowLeft size="25px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={serviceInfo.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="xl" />
{filterCode.data?<>
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
</>: null}
{(!filtersList.data || filtersList.data.length == 0)?<>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
}
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
onClose={()=>setDeleteModal(false) }
action={deleteService}
opened={deleteModal}
/>
<RenameForm
onClose={()=>setRenameModal(false)}
opened={renameModal}
service={serviceInfo}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={serviceInfo}
/>
<ModalLog
opened={openLogModal}
close={()=>setOpenLogModal(false)}
title={`Logs for service ${serviceInfo.name}`}
data={logData.join("")}
/>
</>
}

View File

@@ -0,0 +1,172 @@
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs";
import { useNavigate, useParams } from 'react-router-dom';
import ServiceRow from '../../components/NFProxy/ServiceRow';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddEditService from '../../components/NFProxy/AddEditService';
import { useQueryClient } from '@tanstack/react-query';
import { TbPlugConnected, TbReload } from 'react-icons/tb';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
import { MdUploadFile } from "react-icons/md";
import { notifications } from '@mantine/notifications';
import { useFileDialog } from '@mantine/hooks';
import { CodeHighlight } from '@mantine/code-highlight';
import { DocsButton } from '../../components/DocsButton';
export default function NFProxy({ children }: { children: any }) {
const navigator = useNavigate()
const [open, setOpen] = useState(false);
const {srv} = useParams()
const queryClient = useQueryClient()
const isMedium = isMediumScreen()
const services = nfproxyServiceQuery()
const fileDialog = useFileDialog({
accept: ".py",
multiple: false,
resetOnOpen: true,
onChange: (files) => {
if (files?.length??0 > 0)
setFile(files![0])
}
});
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
if (!srv) return
const service = services.data?.find(s => s.service_id === srv)
if (!service) return
if (file){
console.log("Uploading code")
const notify_id = notifications.show(
{
title: "Uploading code",
message: `Uploading code for service ${service.name}`,
color: "blue",
icon: <MdUploadFile size={20} />,
autoClose: false,
loading: true,
}
)
file.text()
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
.then( res => {
if (!res){
notifications.update({
id: notify_id,
title: "Code uploaded",
message: `Successfully uploaded code for service ${service.name}`,
color: "green",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}else{
notifications.update({
id: notify_id,
title: "Code upload failed",
message: `Error: ${res}`,
color: "red",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}
}).catch( err => {
notifications.update({
id: notify_id,
title: "Code upload failed",
message: `Error: ${err}`,
color: "red",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}).finally(()=>{setFile(null)})
}
}, [file])
useEffect(()=> {
if(services.isError)
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
},[services.isError])
const closeModal = () => {setOpen(false);}
return <>
<Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex' >
{isMedium?"General stats:":null}
<Space w="xs" />
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
<Space w="xs" />
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
<Space w="xs" />
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
<Space w="xs" />
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex' >
{ srv?
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
<MdUploadFile size={18} />
</ActionIcon>
</Tooltip>
: <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
<BsPlusLg size={18} />
</ActionIcon>
</Tooltip>
}
<Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
<TbReload size={18} />
</ActionIcon>
</Tooltip>
<Space w="xs" />
<DocsButton doc="nfproxy" />
</Box>
</Box>
<Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}>
{srv?null:<>
<LoadingOverlay visible={services.isLoading} />
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfproxy/"+srv.service_id)
}} />):<>
<Box className='center-flex-row'>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="nfproxy" size="xl" />
</Box>
</Box>
</>}
</>}
</Box>
{srv?children:null}
{!srv?
<AddEditService opened={open} onClose={closeModal} />:null
}
</>
}

View File

@@ -27,15 +27,12 @@ export default function ServiceDetailsNFRegex() {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const services = nfregexServiceQuery() const services = nfregexServiceQuery()
const serviceInfo = services.data?.find(s => s.service_id == srv) const serviceInfo = services.data?.find(s => s.service_id == srv)
const [tooltipAddRegexOpened, setTooltipAddRegexOpened] = useState(false)
const regexesList = nfregexServiceRegexesQuery(srv??"") const regexesList = nfregexServiceRegexesQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false) const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false) const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false) const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false) const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [tooltipStopOpened, setTooltipStopOpened] = useState(false);
const [tooltipBackOpened, setTooltipBackOpened] = useState(false);
const navigate = useNavigate() const navigate = useNavigate()
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
@@ -133,23 +130,19 @@ export default function ServiceDetailsNFRegex() {
</Box> </Box>
{isMedium?null:<Space h="xl" />} {isMedium?null:<Space h="xl" />}
<Box className='center-flex'> <Box className='center-flex'>
<Tooltip label="Go back" zIndex={0} color="cyan" opened={tooltipBackOpened}> <Tooltip label="Go back" zIndex={0} color="cyan">
<ActionIcon color="cyan" <ActionIcon color="cyan"
onClick={() => navigate("/")} size="xl" radius="md" variant="filled" onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-back-id" aria-describedby="tooltip-back-id">
onFocus={() => setTooltipBackOpened(false)} onBlur={() => setTooltipBackOpened(false)}
onMouseEnter={() => setTooltipBackOpened(true)} onMouseLeave={() => setTooltipBackOpened(false)}>
<FaArrowLeft size="25px" /> <FaArrowLeft size="25px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Space w="md"/> <Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}> <Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading} <ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled" onClick={stopService} size="xl" radius="md" variant="filled"
disabled={serviceInfo.status === "stop"} disabled={serviceInfo.status === "stop"}
aria-describedby="tooltip-stop-id" aria-describedby="tooltip-stop-id">
onFocus={() => setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)}
onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}>
<FaStop size="20px" /> <FaStop size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@@ -168,11 +161,9 @@ export default function ServiceDetailsNFRegex() {
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title> <Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
<Space h="xl" /> <Space h="xl" /> <Space h="xl" /> <Space h="xl" />
<Box className='center-flex'> <Box className='center-flex'>
<Tooltip label="Add a new regex" zIndex={0} color="blue" opened={tooltipAddRegexOpened}> <Tooltip label="Add a new regex" zIndex={0} color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled" <ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-AddRegex-id" aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
onFocus={() => setTooltipAddRegexOpened(false)} onBlur={() => setTooltipAddRegexOpened(false)}
onMouseEnter={() => setTooltipAddRegexOpened(true)} onMouseLeave={() => setTooltipAddRegexOpened(false)}><BsPlusLg size="20px" /></ActionIcon>
</Tooltip> </Tooltip>
</Box> </Box>
</>: </>:

View File

@@ -1,6 +1,6 @@
import { ActionIcon, Badge, Box, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs"; import { BsPlusLg, BsRegex } from "react-icons/bs";
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import ServiceRow from '../../components/NFRegex/ServiceRow'; import ServiceRow from '../../components/NFRegex/ServiceRow';
import { nfregexServiceQuery } from '../../components/NFRegex/utils'; import { nfregexServiceQuery } from '../../components/NFRegex/utils';
@@ -9,7 +9,10 @@ import AddEditService from '../../components/NFRegex/AddEditService';
import AddNewRegex from '../../components/AddNewRegex'; import AddNewRegex from '../../components/AddNewRegex';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb'; import { TbReload } from 'react-icons/tb';
import { FaFilter } from 'react-icons/fa';
import { FaServer } from "react-icons/fa6";
import { VscRegex } from "react-icons/vsc";
import { DocsButton } from '../../components/DocsButton';
function NFRegex({ children }: { children: any }) { function NFRegex({ children }: { children: any }) {
@@ -17,9 +20,6 @@ function NFRegex({ children }: { children: any }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const {srv} = useParams() const {srv} = useParams()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [tooltipRefreshOpened, setTooltipRefreshOpened] = useState(false);
const [tooltipAddServOpened, setTooltipAddServOpened] = useState(false);
const [tooltipAddOpened, setTooltipAddOpened] = useState(false);
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const services = nfregexServiceQuery() const services = nfregexServiceQuery()
@@ -33,37 +33,35 @@ function NFRegex({ children }: { children: any }) {
return <> return <>
<Space h="sm" /> <Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}> <Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={4}>Netfilter Regex</Title> <Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />} {isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex' > <Box className='center-flex' >
<Badge size="sm" color="green" variant="filled">Services: {services.isLoading?0:services.data?.length}</Badge> {isMedium?"General stats:":null}
<Space w="xs" /> <Space w="xs" />
<Badge size="sm" color="yellow" variant="filled">Filtered Connections: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge> <Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" /> <Space w="xs" />
<Badge size="sm" color="violet" variant="filled">Regexes: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge> <Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
<Space w="xs" />
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
<Space w="xs" /> <Space w="xs" />
</Box> </Box>
{isMedium?null:<Space h="md" />} {isMedium?null:<Space h="md" />}
<Box className='center-flex' > <Box className='center-flex' >
{ srv? { srv?
<Tooltip label="Add a new regex" position='bottom' color="blue" opened={tooltipAddOpened}> <Tooltip label="Add a new regex" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled" <ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}><BsPlusLg size={18} /></ActionIcon>
</Tooltip> </Tooltip>
: <Tooltip label="Add a new service" position='bottom' color="blue" opened={tooltipAddOpened}> : <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled" <ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}><BsPlusLg size={18} /></ActionIcon>
</Tooltip> </Tooltip>
} }
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo" opened={tooltipRefreshOpened}> <Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled" <ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
loading={services.isFetching} loading={services.isFetching}><TbReload size={18} /></ActionIcon>
onFocus={() => setTooltipRefreshOpened(false)} onBlur={() => setTooltipRefreshOpened(false)}
onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}><TbReload size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" />
<DocsButton doc="nfregex" />
</Box> </Box>
</Box> </Box>
<Space h="md" /> <Space h="md" />
@@ -72,13 +70,21 @@ function NFRegex({ children }: { children: any }) {
<LoadingOverlay visible={services.isLoading} /> <LoadingOverlay visible={services.isLoading} />
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{ {(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfregex/"+srv.service_id) navigator("/nfregex/"+srv.service_id)
}} />):<><Space h="xl"/> <Title className='center-flex' style={{textAlign:"center"}} order={3}>No services found! Add one clicking the "+" buttons</Title> }} />):<>
<Box className='center-flex'> <Box className='center-flex-row'>
<Tooltip label="Add a new service" color="blue" opened={tooltipAddServOpened}> <Space h="xl" />
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled" <Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
onFocus={() => setTooltipAddServOpened(false)} onBlur={() => setTooltipAddServOpened(false)} <Space h="xs" />
onMouseEnter={() => setTooltipAddServOpened(true)} onMouseLeave={() => setTooltipAddServOpened(false)}><BsPlusLg size="20px" /></ActionIcon> <Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
</Tooltip> <Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="nfregex" size="xl" />
</Box>
</Box> </Box>
</>} </>}
</>} </>}

View File

@@ -1,4 +1,4 @@
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs"; import { BsPlusLg } from "react-icons/bs";
import ServiceRow from '../../components/PortHijack/ServiceRow'; import ServiceRow from '../../components/PortHijack/ServiceRow';
@@ -7,15 +7,15 @@ import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddNewService from '../../components/PortHijack/AddNewService'; import AddNewService from '../../components/PortHijack/AddNewService';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb'; import { TbReload } from 'react-icons/tb';
import { FaServer } from 'react-icons/fa';
import { GrDirections } from 'react-icons/gr';
import { DocsButton } from '../../components/DocsButton';
function PortHijack() { function PortHijack() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [tooltipAddServOpened, setTooltipAddServOpened] = useState(false);
const [tooltipAddOpened, setTooltipAddOpened] = useState(false);
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [tooltipRefreshOpened, setTooltipRefreshOpened] = useState(false);
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const services = porthijackServiceQuery() const services = porthijackServiceQuery()
@@ -30,36 +30,43 @@ function PortHijack() {
return <> return <>
<Space h="sm" /> <Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}> <Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={4}>Hijack port to proxy</Title> <Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />} {isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex'> <Box className='center-flex'>
<Badge size="sm" color="yellow" variant="filled">Services: {services.isLoading?0:services.data?.length}</Badge> <Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Add a new service" position='bottom' color="blue" opened={tooltipAddOpened}> <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled" <ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}><BsPlusLg size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo" opened={tooltipRefreshOpened}> <Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled" <ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
loading={services.isFetching} loading={services.isFetching}><TbReload size={18} /></ActionIcon>
onFocus={() => setTooltipRefreshOpened(false)} onBlur={() => setTooltipRefreshOpened(false)}
onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}><TbReload size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" />
<DocsButton doc="porthijack" />
</Box> </Box>
</Box> </Box>
<Space h="md" /> <Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}> <Box className="center-flex-row" style={{gap: 20}}>
<LoadingOverlay visible={services.isLoading} /> <LoadingOverlay visible={services.isLoading} />
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<> {(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
<Space h="xl"/> <Title className='center-flex' style={{textAlign:"center"}} order={3}>No services found! Add one clicking the "+" buttons</Title> <Box className='center-flex-row'>
<Box className='center-flex'> <Space h="xl" />
<Tooltip label="Add a new service" color="blue" opened={tooltipAddServOpened}> <Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled" <Space h="xs" />
onFocus={() => setTooltipAddServOpened(false)} onBlur={() => setTooltipAddServOpened(false)} <Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
onMouseEnter={() => setTooltipAddServOpened(true)} onMouseLeave={() => setTooltipAddServOpened(false)}><BsPlusLg size="20px" /></ActionIcon> <Space h="xs" />
</Tooltip> <Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="porthijack" size="xl" />
</Box>
</Box> </Box>
</>} </>}
<AddNewService opened={open} onClose={closeModal} /> <AddNewService opened={open} onClose={closeModal} />

View File

@@ -1,14 +0,0 @@
typer==0.12.3
requests>=2.32.3
python-dateutil==2.9.0.post0
pydantic >= 2
typing-extensions >= 4.7.1
textual==0.89.1
toml==0.10.2
psutil==6.0.0
dirhash==0.5.0
requests-toolbelt==1.0.0
python-socketio[client]==5.11.4
orjson
# TODO choose dependencies

View File

@@ -35,7 +35,7 @@ else:
def exit_test(code): def exit_test(code):
if service_id: if service_id:
server.stop() server.kill()
if(firegex.nf_delete_service(service_id)): if(firegex.nf_delete_service(service_id)):
puts("Sucessfully deleted service ✔", color=colors.green) puts("Sucessfully deleted service ✔", color=colors.green)
else: else:

View File

@@ -43,6 +43,11 @@ def exit_test(code):
exit_test(1) exit_test(1)
exit(code) exit(code)
srvs = firegex.nf_get_services()
for ele in srvs:
if ele['name'] == args.service_name:
firegex.nf_delete_service(ele['service_id'])
service_id = firegex.nf_add_service(args.service_name, args.port, args.proto , "::1" if args.ipv6 else "127.0.0.1" ) service_id = firegex.nf_add_service(args.service_name, args.port, args.proto , "::1" if args.ipv6 else "127.0.0.1" )
if service_id: if service_id:
puts(f"Sucessfully created service {service_id}", color=colors.green) puts(f"Sucessfully created service {service_id}", color=colors.green)
@@ -64,7 +69,7 @@ try:
else: else:
puts("Test Failed: Data was corrupted ", color=colors.red) puts("Test Failed: Data was corrupted ", color=colors.red)
exit_test(1) exit_test(1)
except Exception as e: except Exception:
puts("Test Failed: Couldn't send data to the server ", color=colors.red) puts("Test Failed: Couldn't send data to the server ", color=colors.red)
exit_test(1) exit_test(1)
#Add new regex #Add new regex
@@ -220,10 +225,24 @@ else:
exit_test(1) exit_test(1)
#Check if service was renamed correctly #Check if service was renamed correctly
for services in firegex.nf_get_services(): service = firegex.nf_get_service(service_id)
if services["name"] == f"{args.service_name}2": if service["name"] == f"{args.service_name}2":
puts("Checked that service was renamed correctly ✔", color=colors.green) puts("Checked that service was renamed correctly ✔", color=colors.green)
exit_test(0) else:
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
exit_test(1)
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red) #Change settings
exit_test(1) opposite_proto = "udp" if args.proto == "tcp" else "tcp"
if(firegex.nf_settings_service(service_id, 1338, opposite_proto, "::dead:beef" if args.ipv6 else "123.123.123.123", True)):
srv_updated = firegex.nf_get_service(service_id)
if srv_updated["port"] == 1338 and srv_updated["proto"] == opposite_proto and ("::dead:beef" if args.ipv6 else "123.123.123.123") in srv_updated["ip_int"] and srv_updated["fail_open"]:
puts("Sucessfully changed service settings ✔", color=colors.green)
else:
puts("Test Failed: Service settings weren't updated correctly ✗", color=colors.red)
exit_test(1)
else:
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
exit_test(1)
exit_test(0)

View File

@@ -42,6 +42,11 @@ def exit_test(code):
exit_test(1) exit_test(1)
exit(code) exit(code)
srvs = firegex.ph_get_services()
for ele in srvs:
if ele['name'] == args.service_name:
firegex.ph_delete_service(ele['service_id'])
#Create and start serivce #Create and start serivce
service_id = firegex.ph_add_service(args.service_name, args.port, args.port+1, args.proto , "::1" if args.ipv6 else "127.0.0.1", "::1" if args.ipv6 else "127.0.0.1") service_id = firegex.ph_add_service(args.service_name, args.port, args.port+1, args.proto , "::1" if args.ipv6 else "127.0.0.1", "::1" if args.ipv6 else "127.0.0.1")
if service_id: if service_id:

View File

@@ -27,5 +27,10 @@ python3 ph_test.py -p $PASSWORD -m udp || ERROR=1
echo "Running Port Hijack UDP ipv6" echo "Running Port Hijack UDP ipv6"
python3 ph_test.py -p $PASSWORD -m udp -6 || ERROR=1 python3 ph_test.py -p $PASSWORD -m udp -6 || ERROR=1
if [[ "$ERROR" == "0" ]] then
python3 benchmark.py -p $PASSWORD -r 5 -d 1 -s 10 || ERROR=1
fi
exit $ERROR exit $ERROR

View File

@@ -101,6 +101,10 @@ class FiregexAPI:
def nf_rename_service(self,service_id: str, newname: str): def nf_rename_service(self,service_id: str, newname: str):
req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/rename" , json={"name":newname}) req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/rename" , json={"name":newname})
return verify(req) return verify(req)
def nf_settings_service(self,service_id: str, port: int, proto: str, ip_int: str, fail_open: bool):
req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/settings" , json={"port":port, "proto":proto, "ip_int":ip_int, "fail_open":fail_open})
return verify(req)
def nf_get_service_regexes(self,service_id: str): def nf_get_service_regexes(self,service_id: str):
req = self.s.get(f"{self.address}api/nfregex/services/{service_id}/regexes") req = self.s.get(f"{self.address}api/nfregex/services/{service_id}/regexes")
@@ -127,9 +131,9 @@ class FiregexAPI:
json={"service_id": service_id, "regex": regex, "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive}) json={"service_id": service_id, "regex": regex, "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive})
return verify(req) return verify(req)
def nf_add_service(self, name: str, port: int, proto: str, ip_int: str): def nf_add_service(self, name: str, port: int, proto: str, ip_int: str, fail_open: bool = False):
req = self.s.post(f"{self.address}api/nfregex/services" , req = self.s.post(f"{self.address}api/nfregex/services" ,
json={"name":name,"port":port, "proto": proto, "ip_int": ip_int}) json={"name":name,"port":port, "proto": proto, "ip_int": ip_int, "fail_open": fail_open})
return req.json()["service_id"] if verify(req) else False return req.json()["service_id"] if verify(req) else False
def nf_get_metrics(self): def nf_get_metrics(self):