From 8652f40235191155d4be49b7dab760d02ff03072 Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Thu, 20 Feb 2025 19:51:28 +0100 Subject: [PATCH] nfproxy module writing: written part of the firegex lib, frontend refactored and improved, c++ improves --- .gitignore | 8 +- Dockerfile | 3 +- backend/binsrc/classes/nfqueue.cpp | 15 +- backend/binsrc/nfproxy.cpp | 80 +++++- backend/binsrc/proxytun/proxytun.cpp | 165 ------------- backend/binsrc/proxytun/settings.cpp | 22 -- backend/binsrc/proxytun/stream_ctx.cpp | 39 --- backend/binsrc/pyproxy/pyproxy.cpp | 232 ++++++++++++++++++ backend/binsrc/pyproxy/settings.cpp | 71 ++++++ backend/binsrc/pyproxy/stream_ctx.cpp | 202 +++++++++++++++ backend/binsrc/regex/regex_rules.cpp | 11 +- backend/modules/nfproxy/firegex.py | 28 ++- backend/modules/nfregex/firegex.py | 3 +- backend/routers/nfproxy.py | 66 ++++- {proxy-client => fgex-lib}/MANIFEST.in | 0 {proxy-client => fgex-lib}/README.md | 0 .../fgex/__main__.py => fgex-lib/fgex | 0 {proxy-client => fgex-lib}/fgex-pip/README.md | 0 .../fgex-pip/fgex/__init__.py | 0 .../fgex-pip/fgex}/__main__.py | 1 - {proxy-client => fgex-lib}/fgex-pip/setup.py | 0 .../firegex/__init__.py | 0 .../fgex => fgex-lib/firegex/__main__.py | 1 - fgex-lib/firegex/cli.py | 5 + fgex-lib/firegex/nfproxy/__init__.py | 38 +++ fgex-lib/firegex/nfproxy/internals.py | 161 ++++++++++++ fgex-lib/firegex/nfproxy/params.py | 71 ++++++ fgex-lib/requirements.txt | 10 + {proxy-client => fgex-lib}/setup.py | 0 frontend/bun.lock | 40 +-- frontend/package.json | 18 +- frontend/src/App.tsx | 5 + frontend/src/components/AddNewRegex.tsx | 5 +- .../src/components/NFProxy/AddEditService.tsx | 139 +++++++++++ .../NFProxy/ServiceRow/RenameForm.tsx | 68 +++++ .../components/NFProxy/ServiceRow/index.tsx | 164 +++++++++++++ frontend/src/components/NFProxy/utils.ts | 99 ++++++++ frontend/src/components/NFRegex/utils.ts | 1 - frontend/src/components/NavBar/index.tsx | 13 +- .../PortHijack/ServiceRow/index.tsx | 2 +- .../src/components/PyFilterView/index.tsx | 50 ++++ frontend/src/components/RegexView/index.tsx | 11 +- frontend/src/index.tsx | 1 + frontend/src/js/models.ts | 8 + frontend/src/js/utils.tsx | 9 +- frontend/src/pages/Firewall/index.tsx | 11 +- frontend/src/pages/NFProxy/ServiceDetails.tsx | 200 +++++++++++++++ frontend/src/pages/NFProxy/index.tsx | 91 +++++++ frontend/src/pages/NFRegex/index.tsx | 18 +- frontend/src/pages/PortHijack/index.tsx | 8 +- proxy-client/requirements.txt | 14 -- 51 files changed, 1864 insertions(+), 343 deletions(-) delete mode 100644 backend/binsrc/proxytun/proxytun.cpp delete mode 100644 backend/binsrc/proxytun/settings.cpp delete mode 100644 backend/binsrc/proxytun/stream_ctx.cpp create mode 100644 backend/binsrc/pyproxy/pyproxy.cpp create mode 100644 backend/binsrc/pyproxy/settings.cpp create mode 100644 backend/binsrc/pyproxy/stream_ctx.cpp rename {proxy-client => fgex-lib}/MANIFEST.in (100%) rename {proxy-client => fgex-lib}/README.md (100%) rename proxy-client/fgex-pip/fgex/__main__.py => fgex-lib/fgex (100%) mode change 100644 => 100755 rename {proxy-client => fgex-lib}/fgex-pip/README.md (100%) rename {proxy-client => fgex-lib}/fgex-pip/fgex/__init__.py (100%) rename {proxy-client/firegex => fgex-lib/fgex-pip/fgex}/__main__.py (71%) rename {proxy-client => fgex-lib}/fgex-pip/setup.py (100%) rename {proxy-client => fgex-lib}/firegex/__init__.py (100%) rename proxy-client/fgex => fgex-lib/firegex/__main__.py (71%) mode change 100755 => 100644 create mode 100644 fgex-lib/firegex/cli.py create mode 100644 fgex-lib/firegex/nfproxy/__init__.py create mode 100644 fgex-lib/firegex/nfproxy/internals.py create mode 100644 fgex-lib/firegex/nfproxy/params.py create mode 100644 fgex-lib/requirements.txt rename {proxy-client => fgex-lib}/setup.py (100%) create mode 100644 frontend/src/components/NFProxy/AddEditService.tsx create mode 100644 frontend/src/components/NFProxy/ServiceRow/RenameForm.tsx create mode 100644 frontend/src/components/NFProxy/ServiceRow/index.tsx create mode 100644 frontend/src/components/NFProxy/utils.ts create mode 100644 frontend/src/components/PyFilterView/index.tsx create mode 100644 frontend/src/pages/NFProxy/ServiceDetails.tsx create mode 100644 frontend/src/pages/NFProxy/index.tsx delete mode 100644 proxy-client/requirements.txt diff --git a/.gitignore b/.gitignore index cf7f774..d74d9af 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,10 @@ # testing /frontend/coverage -/proxy-client/firegex.egg-info -/proxy-client/dist -/proxy-client/fgex-pip/fgex.egg-info -/proxy-client/fgex-pip/dist +/fgex-lib/firegex.egg-info +/fgex-lib/dist +/fgex-lib/fgex-pip/fgex.egg-info +/fgex-lib/fgex-pip/dist /backend/db/ /backend/db/** /frontend/build/ diff --git a/Dockerfile b/Dockerfile index a09e4e9..4599907 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,6 @@ RUN bun i COPY ./frontend/ . RUN bun run build - #Building main conteiner FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \ @@ -24,6 +23,8 @@ WORKDIR /execute ADD ./backend/requirements.txt /execute/requirements.txt RUN uv pip install --no-cache --system -r /execute/requirements.txt +COPY ./proxy-client /execute/proxy-client +RUN uv pip install --no-cache --system ./proxy-client 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) diff --git a/backend/binsrc/classes/nfqueue.cpp b/backend/binsrc/classes/nfqueue.cpp index 513db4a..582e683 100644 --- a/backend/binsrc/classes/nfqueue.cpp +++ b/backend/binsrc/classes/nfqueue.cpp @@ -131,6 +131,15 @@ class PktRequest { } } + void mangle_custom_pkt(const uint8_t* pkt, size_t pkt_size){ + if (action == FilterAction::NOACTION){ + action = FilterAction::MANGLE; + perfrom_action(pkt, pkt_size); + }else{ + throw invalid_argument("Cannot mangle a packet that has already been accepted or dropped"); + } + } + FilterAction get_action(){ return action; } @@ -141,7 +150,7 @@ class PktRequest { } private: - void perfrom_action(){ + void perfrom_action(const uint8_t* custom_data = nullptr, size_t custom_data_size = 0){ char buf[MNL_SOCKET_BUFFER_SIZE]; struct nlmsghdr *nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(res_id)); switch (action) @@ -153,7 +162,9 @@ class PktRequest { nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_DROP ); break; case FilterAction::MANGLE:{ - if (is_ipv6){ + if (custom_data != nullptr){ + nfq_nlmsg_verdict_put_pkt(nlh_verdict, custom_data, custom_data_size); + }else if (is_ipv6){ nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv6->serialize().data(), ipv6->size()); }else{ nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv4->serialize().data(), ipv4->size()); diff --git a/backend/binsrc/nfproxy.cpp b/backend/binsrc/nfproxy.cpp index 520292b..96c12d1 100644 --- a/backend/binsrc/nfproxy.cpp +++ b/backend/binsrc/nfproxy.cpp @@ -1,18 +1,87 @@ #define PY_SSIZE_T_CLEAN #include -#include "proxytun/settings.cpp" -#include "proxytun/proxytun.cpp" +#include "pyproxy/settings.cpp" +#include "pyproxy/pyproxy.cpp" #include "classes/netfilter.cpp" #include #include #include #include +#include using namespace std; using namespace Firegex::PyProxy; using Firegex::NfQueue::MultiThreadQueue; +/* + +How python code is handles: + +User code example: +```python + +from firegex.nfproxy import DROP, ACCEPT, pyfilter + +@pyfilter +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 +global __firegex_pyfilter_enabled, __firegex_proto +__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() # This function can save other global variables, to use by the packet handler and is used generally to check and optimize the code +```` + +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() +```` + +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 +*/ + ssize_t read_check(int __fd, void *__buf, size_t __nbytes){ ssize_t bytes = read(__fd, __buf, __nbytes); if (bytes == 0){ @@ -30,7 +99,10 @@ void config_updater (){ while (true){ uint32_t code_size; read_check(STDIN_FILENO, &code_size, 4); - vector code(code_size); + //Python will send number always in little endian + code_size = le32toh(code_size); + string code; + code.resize(code_size); read_check(STDIN_FILENO, code.data(), code_size); cerr << "[info] [updater] Updating configuration" << endl; try{ @@ -44,10 +116,12 @@ void config_updater (){ } } + int main(int argc, char *argv[]){ Py_Initialize(); 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; diff --git a/backend/binsrc/proxytun/proxytun.cpp b/backend/binsrc/proxytun/proxytun.cpp deleted file mode 100644 index 910a86b..0000000 --- a/backend/binsrc/proxytun/proxytun.cpp +++ /dev/null @@ -1,165 +0,0 @@ -#ifndef PROXY_TUNNEL_CLASS_CPP -#define PROXY_TUNNEL_CLASS_CPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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 { - public: - stream_ctx sctx; - StreamFollower follower; - - struct { - bool matching_has_been_called = false; - bool already_closed = false; - bool result; - NfQueue::PktRequest* 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* pkt){ - shared_ptr 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* 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 \ No newline at end of file diff --git a/backend/binsrc/proxytun/settings.cpp b/backend/binsrc/proxytun/settings.cpp deleted file mode 100644 index f4adae4..0000000 --- a/backend/binsrc/proxytun/settings.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef PROXY_TUNNEL_SETTINGS_CPP -#define PROXY_TUNNEL_SETTINGS_CPP - -#include -#include - -using namespace std; - -class PyCodeConfig{ - public: - const vector code; - public: - PyCodeConfig(vector pycode): code(pycode){} - PyCodeConfig(): code(vector()){} - - ~PyCodeConfig(){} -}; - -shared_ptr config; - -#endif // PROXY_TUNNEL_SETTINGS_CPP - diff --git a/backend/binsrc/proxytun/stream_ctx.cpp b/backend/binsrc/proxytun/stream_ctx.cpp deleted file mode 100644 index 3057ac8..0000000 --- a/backend/binsrc/proxytun/stream_ctx.cpp +++ /dev/null @@ -1,39 +0,0 @@ - -#ifndef STREAM_CTX_CPP -#define STREAM_CTX_CPP - -#include -#include -#include - -using namespace std; - -typedef Tins::TCPIP::StreamIdentifier stream_id; - -struct pyfilter_ctx { - void * pyglob; // TODO python glob??? - string pycode; -}; - -typedef map 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 \ No newline at end of file diff --git a/backend/binsrc/pyproxy/pyproxy.cpp b/backend/binsrc/pyproxy/pyproxy.cpp new file mode 100644 index 0000000..1f2c51c --- /dev/null +++ b/backend/binsrc/pyproxy/pyproxy.cpp @@ -0,0 +1,232 @@ +#ifndef PROXY_TUNNEL_CLASS_CPP +#define PROXY_TUNNEL_CLASS_CPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../classes/netfilter.cpp" +#include "stream_ctx.cpp" +#include "settings.cpp" +#include + +using Tins::TCPIP::Stream; +using Tins::TCPIP::StreamFollower; +using namespace std; + +namespace Firegex { +namespace PyProxy { + +class PyProxyQueue: public NfQueue::ThreadNfQueue { + private: + u_int16_t latest_config_ver = 0; + public: + stream_ctx sctx; + StreamFollower follower; + PyGILState_STATE gstate; + 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, + }; + PyThreadState *tstate = NULL; + PyStatus pystatus; + + struct { + bool matching_has_been_called = false; + bool already_closed = false; + bool rejected = true; + NfQueue::PktRequest* pkt; + } match_ctx; + + void before_loop() override { + // Create thred structure for python + gstate = PyGILState_Ensure(); + // Create a new interpreter for the thread + pystatus = Py_NewInterpreterFromConfig(&tstate, &py_thread_config); + if (PyStatus_Exception(pystatus)) { + Py_ExitStatusException(pystatus); + cerr << "[fatal] [main] Failed to create new interpreter" << endl; + exit(EXIT_FAILURE); + } + // 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){ + osyncstream(cout) << "BLOCKED " << func_name << endl; + } + + inline void print_mangle_reason(const string& func_name){ + osyncstream(cout) << "MANGLED " << func_name << endl; + } + + inline void print_exception_reason(){ + osyncstream(cout) << "EXCEPTION" << endl; + } + + //If the stream has already been matched, drop all data, and try to close the connection + static void keep_fin_packet(PyProxyQueue* proxy_info){ + proxy_info->match_ctx.matching_has_been_called = true; + proxy_info->match_ctx.already_closed = true; + } + + void filter_action(NfQueue::PktRequest* pkt, Stream& stream){ + auto stream_search = sctx.streams_ctx.find(pkt->sid); + pyfilter_ctx* stream_match; + if (stream_search == sctx.streams_ctx.end()){ + shared_ptr conf = config; + //If config is not set, ignore the stream + if (conf->glob == nullptr || conf->local == nullptr){ + stream.client_data_callback(nullptr); + stream.server_data_callback(nullptr); + return pkt->accept(); + } + stream_match = new pyfilter_ctx(conf->glob, conf->local); + sctx.streams_ctx.insert_or_assign(pkt->sid, stream_match); + }else{ + stream_match = stream_search->second; + } + auto result = stream_match->handle_packet(pkt); + switch(result.action){ + case PyFilterResponse::ACCEPT: + pkt->accept(); + case PyFilterResponse::DROP: + print_blocked_reason(*result.filter_match_by); + sctx.clean_stream_by_id(pkt->sid); + stream.client_data_callback(nullptr); + stream.server_data_callback(nullptr); + break; + case PyFilterResponse::REJECT: + 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)); + pkt->ctx->match_ctx.rejected = true; //Handler will take care of the rest + break; + case PyFilterResponse::MANGLE: + print_mangle_reason(*result.filter_match_by); + pkt->mangle_custom_pkt((uint8_t*)result.mangled_packet->c_str(), result.mangled_packet->size()); + break; + case PyFilterResponse::EXCEPTION: + case PyFilterResponse::INVALID: + print_exception_reason(); + sctx.clean_stream_by_id(pkt->sid); + //Free the packet data + stream.client_data_callback(nullptr); + stream.server_data_callback(nullptr); + pkt->accept(); + break; + } + } + + + static void on_data_recv(Stream& stream, PyProxyQueue* proxy_info, string data) { + proxy_info->match_ctx.matching_has_been_called = true; + proxy_info->match_ctx.already_closed = false; + proxy_info->filter_action(proxy_info->match_ctx.pkt, stream); + } + + //Input data filtering + static void on_client_data(Stream& stream, PyProxyQueue* proxy_info) { + on_data_recv(stream, proxy_info, string(stream.client_payload().begin(), stream.client_payload().end())); + } + + //Server data filtering + static void on_server_data(Stream& stream, PyProxyQueue* proxy_info) { + on_data_recv(stream, proxy_info, 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* proxy_info) { + stream_id stream_id = stream_id::make_identifier(stream); + proxy_info->sctx.clean_stream_by_id(stream_id); + } + + static void on_new_stream(Stream& stream, PyProxyQueue* proxy_info) { + stream.auto_cleanup_payloads(true); + if (stream.is_partial_stream()) { + stream.enable_recovery_mode(10 * 1024); + } + stream.client_data_callback(bind(on_client_data, placeholders::_1, proxy_info)); + stream.server_data_callback(bind(on_server_data, placeholders::_1, proxy_info)); + stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, proxy_info)); + } + + + void handle_next_packet(NfQueue::PktRequest* 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.rejected || 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.rejected || !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(); + }else{ + //Fallback to the default action + if (pkt->get_action() == NfQueue::FilterAction::NOACTION){ + return pkt->accept(); + } + } + }else{ + return pkt->accept(); + } + } + + ~PyProxyQueue() { + // Closing first the interpreter + Py_EndInterpreter(tstate); + // Releasing the GIL and the thread data structure + PyGILState_Release(gstate); + sctx.clean(); + } + +}; + +}} +#endif // PROXY_TUNNEL_CLASS_CPP \ No newline at end of file diff --git a/backend/binsrc/pyproxy/settings.cpp b/backend/binsrc/pyproxy/settings.cpp new file mode 100644 index 0000000..80f9a08 --- /dev/null +++ b/backend/binsrc/pyproxy/settings.cpp @@ -0,0 +1,71 @@ +#ifndef PROXY_TUNNEL_SETTINGS_CPP +#define PROXY_TUNNEL_SETTINGS_CPP + +#include + +#include +#include +#include + +using namespace std; + +namespace Firegex { +namespace PyProxy { + + +class PyCodeConfig{ + public: + PyObject* glob = nullptr; + PyObject* local = nullptr; + + private: + void _clean(){ + Py_XDECREF(glob); + Py_XDECREF(local); + } + public: + + PyCodeConfig(const string& pycode){ + + PyObject* compiled_code = Py_CompileStringExFlags(pycode.c_str(), "", Py_file_input, NULL, 2); + if (compiled_code == nullptr){ + std::cerr << "[fatal] [main] Failed to compile the code" << endl; + _clean(); + throw invalid_argument("Failed to compile the code"); + } + glob = PyDict_New(); + local = PyDict_New(); + PyObject* result = PyEval_EvalCode(compiled_code, glob, local); + Py_XDECREF(compiled_code); + if (!result){ + PyErr_Print(); + _clean(); + 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_DECREF(result); + } + PyCodeConfig(){} + + ~PyCodeConfig(){ + _clean(); + } +}; + +shared_ptr config; +PyObject* py_handle_packet_code = nullptr; + +void init_handle_packet_code(){ + py_handle_packet_code = Py_CompileStringExFlags( + "firegex.nfproxy.internals.handle_packet()\n", "", + Py_file_input, NULL, 2); + + if (py_handle_packet_code == nullptr){ + std::cerr << "[fatal] [main] Failed to compile the utility python code (strange behaviour, probably a bug)" << endl; + throw invalid_argument("Failed to compile the code"); + } +} + +}} +#endif // PROXY_TUNNEL_SETTINGS_CPP + diff --git a/backend/binsrc/pyproxy/stream_ctx.cpp b/backend/binsrc/pyproxy/stream_ctx.cpp new file mode 100644 index 0000000..633ca50 --- /dev/null +++ b/backend/binsrc/pyproxy/stream_ctx.cpp @@ -0,0 +1,202 @@ + +#ifndef STREAM_CTX_CPP +#define STREAM_CTX_CPP + +#include +#include +#include +#include +#include "../classes/netfilter.cpp" +#include "settings.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 +}; + +struct py_filter_response { + PyFilterResponse action; + string* filter_match_by = nullptr; + string* mangled_packet = nullptr; + ~py_filter_response(){ + delete mangled_packet; + delete filter_match_by; + } +}; + +typedef Tins::TCPIP::StreamIdentifier stream_id; + +struct pyfilter_ctx { + + PyObject * glob = nullptr; + PyObject * local = nullptr; + + pyfilter_ctx(PyObject * original_glob, PyObject * original_local){ + PyObject *copy = PyImport_ImportModule("copy"); + if (copy == nullptr){ + PyErr_Print(); + throw invalid_argument("Failed to import copy module"); + } + PyObject *deepcopy = PyObject_GetAttrString(copy, "deepcopy"); + glob = PyObject_CallFunctionObjArgs(deepcopy, original_glob, NULL); + if (glob == nullptr){ + PyErr_Print(); + throw invalid_argument("Failed to deepcopy the global dict"); + } + local = PyObject_CallFunctionObjArgs(deepcopy, original_local, NULL); + if (local == nullptr){ + PyErr_Print(); + throw invalid_argument("Failed to deepcopy the local dict"); + } + Py_DECREF(copy); + } + + ~pyfilter_ctx(){ + Py_XDECREF(glob); + Py_XDECREF(local); + } + + 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){ + PyErr_Print(); + throw invalid_argument("Failed to delete item from dict"); + } + } + + inline void set_item_to_local(const char* key, PyObject* value){ + set_item_to_dict(local, key, value); + } + + inline void set_item_to_dict(PyObject* dict, const char* key, PyObject* value){ + if (PyDict_SetItemString(dict, key, value) != 0){ + PyErr_Print(); + throw invalid_argument("Failed to set item to dict"); + } + } + + py_filter_response handle_packet( + NfQueue::PktRequest* pkt + ){ + PyObject * packet_info = PyDict_New(); + + set_item_to_dict(packet_info, "data", PyBytes_FromStringAndSize(pkt->data, 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_code, glob, local); + del_item_from_glob("__firegex_packet_info"); + Py_DECREF(packet_info); + + if (!result){ + PyErr_Print(); + return py_filter_response{PyFilterResponse::EXCEPTION, nullptr}; + } + Py_DECREF(result); + + result = get_item_from_glob("__firegex_pyfilter_result"); + if (result == nullptr){ + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + + if (!PyDict_Check(result)){ + PyErr_Print(); + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + PyObject* action = PyDict_GetItemString(result, "action"); + if (action == nullptr){ + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + if (!PyLong_Check(action)){ + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + PyFilterResponse action_enum = (PyFilterResponse)PyLong_AsLong(action); + + if (action_enum == PyFilterResponse::ACCEPT || action_enum == PyFilterResponse::EXCEPTION || action_enum == PyFilterResponse::INVALID){ + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{action_enum, nullptr, nullptr}; + }else{ + PyObject *func_name_py = PyDict_GetItemString(result, "matched_by"); + if (func_name_py == nullptr){ + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + if (!PyUnicode_Check(func_name_py)){ + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + 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, nullptr}; + } + if (action_enum != PyFilterResponse::MANGLE){ + PyObject* mangled_packet = PyDict_GetItemString(result, "mangled_packet"); + if (mangled_packet == nullptr){ + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + if (!PyBytes_Check(mangled_packet)){ + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + 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}; + } + } + del_item_from_glob("__firegex_pyfilter_result"); + return py_filter_response{PyFilterResponse::INVALID, nullptr, nullptr}; + } + +}; + +typedef map 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; + delete stream_match; + } + } + void clean(){ + for (auto ele: streams_ctx){ + delete ele.second; + } + } +}; + + +}} +#endif // STREAM_CTX_CPP \ No newline at end of file diff --git a/backend/binsrc/regex/regex_rules.cpp b/backend/binsrc/regex/regex_rules.cpp index 71ef786..83fd6dc 100644 --- a/backend/binsrc/regex/regex_rules.cpp +++ b/backend/binsrc/regex/regex_rules.cpp @@ -76,12 +76,11 @@ class RegexRules{ }else{ hs_free_database(db); } - } private: - static inline u_int16_t glob_seq = 0; - u_int16_t version; + static inline uint16_t glob_seq = 0; + uint16_t version; vector> decoded_input_rules; vector> decoded_output_rules; bool is_stream = true; @@ -96,9 +95,7 @@ class RegexRules{ input_ruleset.hs_db = nullptr; } } - - - + void fill_ruleset(vector> & decoded, regex_ruleset & ruleset){ size_t n_of_regex = decoded.size(); if (n_of_regex == 0){ @@ -150,7 +147,6 @@ class RegexRules{ public: RegexRules(vector raw_rules, bool 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){ try{ decoded_regex rule = decode_regex(ele); @@ -170,6 +166,7 @@ class RegexRules{ free_dbs(); throw current_exception(); } + this->version = ++glob_seq; // 0 version is the null version } u_int16_t ver(){ diff --git a/backend/modules/nfproxy/firegex.py b/backend/modules/nfproxy/firegex.py index 70fb5ca..095eb17 100644 --- a/backend/modules/nfproxy/firegex.py +++ b/backend/modules/nfproxy/firegex.py @@ -22,7 +22,7 @@ class FiregexInterceptor: self.update_task: asyncio.Task self.ack_arrived = False self.ack_status = None - self.ack_fail_what = "" + self.ack_fail_what = "Unknown" self.ack_lock = asyncio.Lock() async def _call_stats_updater_callback(self, filter: PyFilter): @@ -79,12 +79,14 @@ class FiregexInterceptor: if filter_id in self.filter_map: self.filter_map[filter_id].blocked_packets+=1 await self.filter_map[filter_id].update() - if line.startswith("EDITED "): + if line.startswith("MANGLED "): filter_id = line.split()[1] async with self.filter_map_lock: if filter_id in self.filter_map: self.filter_map[filter_id].edited_packets+=1 await self.filter_map[filter_id].update() + if line.startswith("EXCEPTION"): + print("TODO EXCEPTION HANDLING") # TODO if line.startswith("ACK "): self.ack_arrived = True self.ack_status = line.split()[1].upper() == "OK" @@ -103,10 +105,9 @@ class FiregexInterceptor: if self.process and self.process.returncode is None: self.process.kill() - async def _update_config(self, filters_codes): + async def _update_config(self, code): async with self.update_config_lock: - # TODO write compiled code correctly - # self.process.stdin.write((" ".join(filters_codes)+"\n").encode()) + self.process.stdin.write(len(code).to_bytes(4, byteorder='big')+code.encode()) await self.process.stdin.drain() try: async with asyncio.timeout(3): @@ -114,11 +115,22 @@ class FiregexInterceptor: except TimeoutError: pass 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}") async def reload(self, filters:list[PyFilter]): async with self.filter_map_lock: - self.filter_map = self.compile_filters(filters) - # TODO COMPILE CODE - #await self._update_config(filters_codes) TODO pass the compiled code + if os.path.exists(f"db/nfproxy_filters/{self.srv.id}.py"): + with open(f"db/nfproxy_filters/{self.srv.id}.py") as f: + filter_file = f.read() + else: + filter_file = "" + await self._update_config( + "global __firegex_pyfilter_enabled\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\n" + + filter_file + "\n\n" + + "firegex.nfproxy.internals.compile()" + ) diff --git a/backend/modules/nfregex/firegex.py b/backend/modules/nfregex/firegex.py index 3d14bda..5e6b2b0 100644 --- a/backend/modules/nfregex/firegex.py +++ b/backend/modules/nfregex/firegex.py @@ -79,7 +79,7 @@ class FiregexInterceptor: self.update_task: asyncio.Task self.ack_arrived = False self.ack_status = None - self.ack_fail_what = "" + self.ack_fail_what = "Unknown" self.ack_lock = asyncio.Lock() @classmethod @@ -160,6 +160,7 @@ class FiregexInterceptor: except TimeoutError: pass 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}") diff --git a/backend/routers/nfproxy.py b/backend/routers/nfproxy.py index 703fff7..efcc664 100644 --- a/backend/routers/nfproxy.py +++ b/backend/routers/nfproxy.py @@ -7,6 +7,9 @@ from modules.nfproxy.firewall import STATUS, FirewallManager from utils.sqlite import SQLite from utils import ip_parse, refactor_name, socketio_emit, PortType from utils.models import ResetRequest, StatusMessageModel +import os +from firegex.nfproxy.internals import get_filter_names +from fastapi.responses import PlainTextResponse class ServiceModel(BaseModel): service_id: str @@ -47,6 +50,9 @@ class ServiceAddResponse(BaseModel): status:str service_id: str|None = None +class SetPyFilterForm(BaseModel): + code: str + app = APIRouter() db = SQLite('db/nft-pyfilters.db', { @@ -70,7 +76,7 @@ db = SQLite('db/nft-pyfilters.db', { }, 'QUERY':[ "CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (port, ip_int, 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);" ] }) @@ -174,6 +180,8 @@ async def service_delete(service_id: str): """Request the deletion of a specific service""" db.query('DELETE FROM services 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 refresh_frontend() return {'status': 'ok'} @@ -253,17 +261,6 @@ async def get_pyfilter_by_id(filter_id: int): raise HTTPException(status_code=400, detail="This filter does not exists!") return res[0] -@app.delete('/pyfilters/{filter_id}', response_model=StatusMessageModel) -async def pyfilter_delete(filter_id: int): - """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""" @@ -304,6 +301,49 @@ async def add_new_service(form: ServiceAddForm): await refresh_frontend() return {'status': 'ok', 'service_id': srv_id} +@app.put('/services/{service_id}/pyfilters/code', response_model=StatusMessageModel) +async def set_pyfilters(service_id: str, form: SetPyFilterForm): + """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] + srv_proto = service["proto"] + try: + found_filters = get_filter_names(form.code, srv_proto) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + # Remove filters that are not in the new code + existing_filters = db.query("SELECT filter_id FROM pyfilter WHERE service_id = ?;", service_id) + for filter in existing_filters: + if filter["name"] not in found_filters: + db.query("DELETE FROM pyfilter WHERE filter_id = ?;", filter["filter_id"]) + + # 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() + 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 "" + #TODO check all the APIs and add -# 1. API to change the python filter file +# 1. API to change the python filter file (DONE) # 2. a socketio mechanism to lock the previous feature \ No newline at end of file diff --git a/proxy-client/MANIFEST.in b/fgex-lib/MANIFEST.in similarity index 100% rename from proxy-client/MANIFEST.in rename to fgex-lib/MANIFEST.in diff --git a/proxy-client/README.md b/fgex-lib/README.md similarity index 100% rename from proxy-client/README.md rename to fgex-lib/README.md diff --git a/proxy-client/fgex-pip/fgex/__main__.py b/fgex-lib/fgex old mode 100644 new mode 100755 similarity index 100% rename from proxy-client/fgex-pip/fgex/__main__.py rename to fgex-lib/fgex diff --git a/proxy-client/fgex-pip/README.md b/fgex-lib/fgex-pip/README.md similarity index 100% rename from proxy-client/fgex-pip/README.md rename to fgex-lib/fgex-pip/README.md diff --git a/proxy-client/fgex-pip/fgex/__init__.py b/fgex-lib/fgex-pip/fgex/__init__.py similarity index 100% rename from proxy-client/fgex-pip/fgex/__init__.py rename to fgex-lib/fgex-pip/fgex/__init__.py diff --git a/proxy-client/firegex/__main__.py b/fgex-lib/fgex-pip/fgex/__main__.py similarity index 71% rename from proxy-client/firegex/__main__.py rename to fgex-lib/fgex-pip/fgex/__main__.py index adcf48a..810291c 100644 --- a/proxy-client/firegex/__main__.py +++ b/fgex-lib/fgex-pip/fgex/__main__.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -# TODO implement cli start function from firegex.cli import run if __name__ == "__main__": diff --git a/proxy-client/fgex-pip/setup.py b/fgex-lib/fgex-pip/setup.py similarity index 100% rename from proxy-client/fgex-pip/setup.py rename to fgex-lib/fgex-pip/setup.py diff --git a/proxy-client/firegex/__init__.py b/fgex-lib/firegex/__init__.py similarity index 100% rename from proxy-client/firegex/__init__.py rename to fgex-lib/firegex/__init__.py diff --git a/proxy-client/fgex b/fgex-lib/firegex/__main__.py old mode 100755 new mode 100644 similarity index 71% rename from proxy-client/fgex rename to fgex-lib/firegex/__main__.py index adcf48a..810291c --- a/proxy-client/fgex +++ b/fgex-lib/firegex/__main__.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -# TODO implement cli start function from firegex.cli import run if __name__ == "__main__": diff --git a/fgex-lib/firegex/cli.py b/fgex-lib/firegex/cli.py new file mode 100644 index 0000000..b95f5a4 --- /dev/null +++ b/fgex-lib/firegex/cli.py @@ -0,0 +1,5 @@ + + +def run(): + pass # TODO implement me + diff --git a/fgex-lib/firegex/nfproxy/__init__.py b/fgex-lib/firegex/nfproxy/__init__.py new file mode 100644 index 0000000..40d6559 --- /dev/null +++ b/fgex-lib/firegex/nfproxy/__init__.py @@ -0,0 +1,38 @@ +import functools + +ACCEPT = 0 +DROP = 1 +REJECT = 2 +MANGLE = 3 +EXCEPTION = 4 +INVALID = 5 + +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) + + + + + + + + + + diff --git a/fgex-lib/firegex/nfproxy/internals.py b/fgex-lib/firegex/nfproxy/internals.py new file mode 100644 index 0000000..fb9fa98 --- /dev/null +++ b/fgex-lib/firegex/nfproxy/internals.py @@ -0,0 +1,161 @@ +from inspect import signature +from firegex.nfproxy.params import RawPacket, NotReadyToRun +from firegex.nfproxy import ACCEPT, DROP, REJECT, MANGLE, EXCEPTION, INVALID + +RESULTS = [ + ACCEPT, + DROP, + REJECT, + MANGLE, + EXCEPTION, + INVALID +] +FULL_STREAM_ACTIONS = [ + "flush" + "accept", + "reject", + "drop" +] + +type_annotations_associations = { + "tcp": { + RawPacket: RawPacket.fetch_from_global + }, + "http": { + RawPacket: RawPacket.fetch_from_global + } +} + +def _generate_filter_structure(filters: list[str], proto:str, glob:dict, local:dict): + 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.append((v.annotation, valid_annotation_type[v.annotation])) + else: + raise Exception(f"Invalid type annotation {v.annotation} for function {func.__name__}") + res.append((func, params_function)) + + 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]) + elif filter in local.keys(): + add_func_to_list(local[filter]) + else: + raise Exception(f"Filter {filter} not found") + + return res + +def get_filters_info(code:str, proto:str): + glob = {} + local = {} + exec(code, glob, local) + exec("import firegex.nfproxy", glob, local) + filters = eval("firegex.nfproxy.get_pyfilters()", glob, local) + return _generate_filter_structure(filters, proto, glob, local) + +def get_filter_names(code:str, proto:str): + return [ele[0].__name__ for ele in get_filters_info(code, proto)] + +def compile(): + glob = globals() + local = locals() + filters = glob["__firegex_pyfilter_enabled"] + proto = glob["__firegex_proto"] + glob["__firegex_func_list"] = _generate_filter_structure(filters, proto, glob, local) + glob["__firegex_stream"] = [] + glob["__firegex_stream_size"] = 0 + + if "FGEX_STREAM_MAX_SIZE" in local and int(local["FGEX_STREAM_MAX_SIZE"]) > 0: + glob["__firegex_stream_max_size"] = int(local["FGEX_STREAM_MAX_SIZE"]) + elif "FGEX_STREAM_MAX_SIZE" in glob and int(glob["FGEX_STREAM_MAX_SIZE"]) > 0: + glob["__firegex_stream_max_size"] = int(glob["FGEX_STREAM_MAX_SIZE"]) + else: + glob["__firegex_stream_max_size"] = 1*8e20 # 1MB default value + + if "FGEX_FULL_STREAM_ACTION" in local and local["FGEX_FULL_STREAM_ACTION"] in FULL_STREAM_ACTIONS: + glob["__firegex_full_stream_action"] = local["FGEX_FULL_STREAM_ACTION"] + else: + glob["__firegex_full_stream_action"] = "flush" + + glob["__firegex_pyfilter_result"] = None + +def handle_packet(): + glob = globals() + func_list = glob["__firegex_func_list"] + final_result = ACCEPT + cache_call = {} + cache_call[RawPacket] = RawPacket.fetch_from_global() + data_size = len(cache_call[RawPacket].data) + if glob["__firegex_stream_size"]+data_size > glob["__firegex_stream_max_size"]: + match glob["__firegex_full_stream_action"]: + case "flush": + glob["__firegex_stream"] = [] + glob["__firegex_stream_size"] = 0 + case "accept": + glob["__firegex_pyfilter_result"] = { + "action": ACCEPT, + "matched_by": None, + "mangled_packet": None + } + return + case "reject": + glob["__firegex_pyfilter_result"] = { + "action": REJECT, + "matched_by": "@MAX_STREAM_SIZE_REACHED", + "mangled_packet": None + } + return + case "drop": + glob["__firegex_pyfilter_result"] = { + "action": DROP, + "matched_by": "@MAX_STREAM_SIZE_REACHED", + "mangled_packet": None + } + return + glob["__firegex_stream"].append(cache_call[RawPacket]) + glob["__firegex_stream_size"] += data_size + func_name = None + mangled_packet = None + for filter in func_list: + final_params = [] + for ele in filter[1]: + if ele[0] not in cache_call.keys(): + try: + cache_call[ele[0]] = ele[1]() + except NotReadyToRun: + cache_call[ele[0]] = None + if cache_call[ele[0]] is None: + continue # Parsing raised NotReadyToRun, skip filter + final_params.append(cache_call[ele[0]]) + res = filter[0](*final_params) + if res is None: + continue #ACCEPTED + if res == MANGLE: + if RawPacket not in cache_call.keys(): + continue #Packet not modified + pkt:RawPacket = cache_call[RawPacket] + mangled_packet = pkt.raw_packet + break + elif res != ACCEPT: + final_result = res + func_name = filter[0].__name__ + break + glob["__firegex_pyfilter_result"] = { + "action": final_result, + "matched_by": func_name, + "mangled_packet": mangled_packet + } + diff --git a/fgex-lib/firegex/nfproxy/params.py b/fgex-lib/firegex/nfproxy/params.py new file mode 100644 index 0000000..e2969b5 --- /dev/null +++ b/fgex-lib/firegex/nfproxy/params.py @@ -0,0 +1,71 @@ + +class NotReadyToRun(Exception): # raise this exception if the stream state is not ready to parse this object, the call will be skipped + pass + +class RawPacket: + def __init__(self, + data: bytes, + raw_packet: bytes, + is_input: bool, + is_ipv6: bool, + is_tcp: bool, + ): + 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) + + @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 proto_header(self) -> bytes: + return self.__raw_packet[:self.proto_header_len] + + @property + def proto_header_len(self) -> int: + return len(self.__raw_packet) - len(self.__data) + + @data.setter + def data(self, v:bytes): + if not isinstance(v, bytes): + raise Exception("Invalid data type, data MUST be of type bytes") + self.__raw_packet = self.proto_header + v + self.__data = 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) < self.proto_header_len: + raise Exception("Invalid packet length") + header_len = self.proto_header_len + self.__data = v[header_len:] + self.__raw_packet = v + + @staticmethod + def fetch_from_global(): + glob = globals() + if "__firegex_packet_info" not in glob.keys(): + raise Exception("Packet info not found") + return RawPacket(**glob["__firegex_packet_info"]) + + diff --git a/fgex-lib/requirements.txt b/fgex-lib/requirements.txt new file mode 100644 index 0000000..f7771d8 --- /dev/null +++ b/fgex-lib/requirements.txt @@ -0,0 +1,10 @@ +typer==0.15.1 +requests>=2.32.3 +pydantic>=2 +typing-extensions>=4.7.1 +fasteners==0.19 +textual==2.1.0 +python-socketio[client]==5.12.1 +fgex +orjson + diff --git a/proxy-client/setup.py b/fgex-lib/setup.py similarity index 100% rename from proxy-client/setup.py rename to fgex-lib/setup.py diff --git a/frontend/bun.lock b/frontend/bun.lock index 526f399..b28c77a 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,17 +5,19 @@ "name": "firegex-frontend", "dependencies": { "@hello-pangea/dnd": "^16.6.0", - "@mantine/core": "^7.16.2", - "@mantine/form": "^7.16.2", - "@mantine/hooks": "^7.16.2", - "@mantine/modals": "^7.16.2", - "@mantine/notifications": "^7.16.2", + "@mantine/code-highlight": "^7.17.0", + "@mantine/core": "^7.16.3", + "@mantine/form": "^7.16.3", + "@mantine/hooks": "^7.16.3", + "@mantine/modals": "^7.16.3", + "@mantine/notifications": "^7.16.3", "@tanstack/react-query": "^4.36.1", "@types/jest": "^27.5.2", - "@types/node": "^20.17.16", + "@types/node": "^20.17.17", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "buffer": "^6.0.3", + "install": "^0.13.0", "react": "^19.0.0", "react-dom": "^19.0.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=="], - "@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=="], @@ -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/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=="], @@ -295,12 +299,16 @@ "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=="], "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=="], + "install": ["install@0.13.0", "", {}, "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "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-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=="], @@ -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-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=="], diff --git a/frontend/package.json b/frontend/package.json index 071420d..0d7cdd3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,21 +5,23 @@ "private": true, "dependencies": { "@hello-pangea/dnd": "^16.6.0", - "@mantine/core": "^7.16.3", - "@mantine/form": "^7.16.3", - "@mantine/hooks": "^7.16.3", - "@mantine/modals": "^7.16.3", - "@mantine/notifications": "^7.16.3", + "@mantine/code-highlight": "^7.17.0", + "@mantine/core": "^7.17.0", + "@mantine/form": "^7.17.0", + "@mantine/hooks": "^7.17.0", + "@mantine/modals": "^7.17.0", + "@mantine/notifications": "^7.17.0", "@tanstack/react-query": "^4.36.1", "@types/jest": "^27.5.2", - "@types/node": "^20.17.17", + "@types/node": "^20.17.19", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "buffer": "^6.0.3", + "install": "^0.13.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-icons": "^5.4.0", - "react-router-dom": "^7.1.5", + "react-icons": "^5.5.0", + "react-router-dom": "^7.2.0", "socket.io-client": "^4.8.1", "typescript": "^5.7.3", "web-vitals": "^2.1.4", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7fc33a5..c13dd32 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,8 @@ import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails'; import PortHijack from './pages/PortHijack'; import { Firewall } from './pages/Firewall'; 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"}); @@ -148,6 +150,9 @@ function App() { } > } /> + } > + } /> + } /> } /> } /> diff --git a/frontend/src/components/AddNewRegex.tsx b/frontend/src/components/AddNewRegex.tsx index 86d08c7..5437ec5 100644 --- a/frontend/src/components/AddNewRegex.tsx +++ b/frontend/src/components/AddNewRegex.tsx @@ -2,8 +2,9 @@ import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } import { useForm } from '@mantine/form'; import { useState } from 'react'; 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 { nfregex } from './NFRegex/utils'; type RegexAddInfo = { regex:string, @@ -47,7 +48,7 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> active: !values.deactive } setSubmitLoading(false) - getapiobject().regexesadd(request).then( res => { + nfregex.regexesadd(request).then( res => { if (!res){ setSubmitLoading(false) close(); diff --git a/frontend/src/components/NFProxy/AddEditService.tsx b/frontend/src/components/NFProxy/AddEditService.tsx new file mode 100644 index 0000000..d9287e2 --- /dev/null +++ b/frontend/src/components/NFProxy/AddEditService.tsx @@ -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","udp"].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(null) + + const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ + setSubmitLoading(true) + if (edit){ + nfproxy.settings(edit.service_id, { port, proto, 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 +
+ {!edit?:null} + + + + + + + {!edit?:null} + + + Enable fail-open nfqueue + + + Firegex use internally nfqueue to handle packets
enabling this option will allow packets to pass through the firewall
in case the filtering is too slow or too many traffic is coming
+ }> + +
+
} + {...form.getInputProps('fail_open', { type: 'checkbox' })} + /> +
+ + + + + + + + + {error?<> + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} + + +
+ +} + +export default AddEditService; diff --git a/frontend/src/components/NFProxy/ServiceRow/RenameForm.tsx b/frontend/src/components/NFProxy/ServiceRow/RenameForm.tsx new file mode 100644 index 0000000..fec56b6 --- /dev/null +++ b/frontend/src/components/NFProxy/ServiceRow/RenameForm.tsx @@ -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(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 +
+ + + + + + {error?<> + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} + + +
+ +} + +export default RenameForm; diff --git a/frontend/src/components/NFProxy/ServiceRow/index.tsx b/frontend/src/components/NFProxy/ServiceRow/index.tsx new file mode 100644 index 0000000..5943bb7 --- /dev/null +++ b/frontend/src/components/NFProxy/ServiceRow/index.tsx @@ -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"; + +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 [tooltipStopOpened, setTooltipStopOpened] = 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 <> + + + + + + + {service.name} + + + + {service.status} + + :{service.port} + + + {isMedium?null:} + + + + + {service.ip_int} on {service.proto} + + + {service.blocked_packets} + + {service.edited_packets} + + {service.n_filters} + + + {isMedium?:} + + + Edit service + } onClick={()=>setEditModal(true)}>Service Settings + } onClick={()=>setRenameModal(true)}>Change service name + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)} + onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}> + + + + + + + + + + {isMedium?:} + {onClick? + + :null} + + + + + setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={service} + /> + setEditModal(false)} + edit={service} + /> + +} diff --git a/frontend/src/components/NFProxy/utils.ts b/frontend/src/components/NFProxy/utils.ts new file mode 100644 index 0000000..492f560 --- /dev/null +++ b/frontend/src/components/NFProxy/utils.ts @@ -0,0 +1,99 @@ +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, + proto?:string, + 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 (regex_id:number) => { + const { status } = await postapi(`nfproxy/pyfilters/${regex_id}/enable`) as ServerResponse; + return status === "ok"?undefined:status + }, + pyfilterdisable: async (regex_id:number) => { + const { status } = await postapi(`nfproxy/pyfilters/${regex_id}/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 + } +} diff --git a/frontend/src/components/NFRegex/utils.ts b/frontend/src/components/NFRegex/utils.ts index faa67c4..0fd8b3a 100644 --- a/frontend/src/components/NFRegex/utils.ts +++ b/frontend/src/components/NFRegex/utils.ts @@ -36,7 +36,6 @@ export type ServiceAddResponse = { } export const serviceQueryKey = ["nfregex","services"] -export const statsQueryKey = ["nfregex","stats"] export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services}) export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({ diff --git a/frontend/src/components/NavBar/index.tsx b/frontend/src/components/NavBar/index.tsx index 456e12b..68a4f5f 100644 --- a/frontend/src/components/NavBar/index.tsx +++ b/frontend/src/components/NavBar/index.tsx @@ -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 { IoMdGitNetwork } from "react-icons/io"; -import { MdOutlineExpandLess, MdOutlineExpandMore, MdTransform } from "react-icons/md"; +import { TbPlugConnected } from "react-icons/tb"; import { useNavigate } from "react-router-dom"; import { GrDirections } from "react-icons/gr"; import { PiWallLight } from "react-icons/pi"; import { useNavbarStore } from "../../js/store"; import { getMainPath } from "../../js/utils"; +import { BsRegex } from "react-icons/bs"; function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }: { navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) { @@ -36,9 +36,10 @@ export default function NavBar() { - } /> - } /> - } /> + } /> + } /> + } /> + } /> diff --git a/frontend/src/components/PortHijack/ServiceRow/index.tsx b/frontend/src/components/PortHijack/ServiceRow/index.tsx index 2f4a136..0b47b6b 100644 --- a/frontend/src/components/PortHijack/ServiceRow/index.tsx +++ b/frontend/src/components/PortHijack/ServiceRow/index.tsx @@ -1,4 +1,4 @@ -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 { FaPlay, FaStop } from 'react-icons/fa'; import { porthijack, Service } from '../utils'; diff --git a/frontend/src/components/PyFilterView/index.tsx b/frontend/src/components/PyFilterView/index.tsx new file mode 100644 index 0000000..9a16108 --- /dev/null +++ b/frontend/src/components/PyFilterView/index.tsx @@ -0,0 +1,50 @@ +import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; +import { useState } from 'react'; +import { PyFilter } from '../../js/models'; +import { errorNotify, 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 [deleteTooltipOpened, setDeleteTooltipOpened] = useState(false); + const [statusTooltipOpened, setStatusTooltipOpened] = useState(false); + + const changeRegexStatus = () => { + (filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.filter_id).then(res => { + if(!res){ + okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter with id '${filterInfo.filter_id}' 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 + + + + {filterInfo.name} + + + + setStatusTooltipOpened(false)} onBlur={() => setStatusTooltipOpened(false)} + onMouseEnter={() => setStatusTooltipOpened(true)} onMouseLeave={() => setStatusTooltipOpened(false)} + >{filterInfo.active?:} + + + + {filterInfo.blocked_packets} + + {filterInfo.edited_packets} + + {filterInfo.active?"ACTIVE":"DISABLED"} + + + + + +} diff --git a/frontend/src/components/RegexView/index.tsx b/frontend/src/components/RegexView/index.tsx index e5b0821..7f72c68 100644 --- a/frontend/src/components/RegexView/index.tsx +++ b/frontend/src/components/RegexView/index.tsx @@ -1,13 +1,14 @@ import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; import { useState } from 'react'; 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 YesNoModal from '../YesNoModal'; import { FaPause, FaPlay } from 'react-icons/fa'; import { useClipboard } from '@mantine/hooks'; import { FaFilter } from "react-icons/fa"; -import { VscRegex } from "react-icons/vsc"; + +import { nfregex } from '../NFRegex/utils'; function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { @@ -24,7 +25,7 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { const isMedium = isMediumScreen(); const deleteRegex = () => { - getapiobject().regexdelete(regexInfo.id).then(res => { + nfregex.regexdelete(regexInfo.id).then(res => { if(!res){ okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`) }else{ @@ -34,9 +35,9 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { } const changeRegexStatus = () => { - (regexInfo.active?getapiobject().regexdisable:getapiobject().regexenable)(regexInfo.id).then(res => { + (regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(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{ errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`) } diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index b636476..b3212c8 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -9,6 +9,7 @@ import { import { queryClient } from './js/utils'; import '@mantine/core/styles.css'; import '@mantine/notifications/styles.css'; +import '@mantine/code-highlight/styles.css'; import './index.css'; const root = ReactDOM.createRoot( diff --git a/frontend/src/js/models.ts b/frontend/src/js/models.ts index 3f9804a..6f992f4 100644 --- a/frontend/src/js/models.ts +++ b/frontend/src/js/models.ts @@ -48,4 +48,12 @@ export type RegexAddForm = { is_case_sensitive:boolean, mode:string, // C->S S->C BOTH, active: boolean +} + +export type PyFilter = { + filter_id:number, + name:string, + blocked_packets:number, + edited_packets:number, + active:boolean } \ No newline at end of file diff --git a/frontend/src/js/utils.tsx b/frontend/src/js/utils.tsx index 0197205..55b1493 100644 --- a/frontend/src/js/utils.tsx +++ b/frontend/src/js/utils.tsx @@ -7,6 +7,7 @@ import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerRespons import { Buffer } from "buffer" import { QueryClient, useQuery } from "@tanstack/react-query"; import { useMediaQuery } from "@mantine/hooks"; +import { nfproxy } from "../components/NFProxy/utils"; export const IS_DEV = import.meta.env.DEV @@ -101,14 +102,6 @@ export function getMainPath(){ return "" } -export function getapiobject(){ - switch(getMainPath()){ - case "nfregex": - return nfregex - } - throw new Error('No api for this tool!'); -} - export function HomeRedirector(){ const section = sessionStorage.getItem("home_section") const path = section?`/${section}`:`/nfregex` diff --git a/frontend/src/pages/Firewall/index.tsx b/frontend/src/pages/Firewall/index.tsx index 07594ca..1517e97 100644 --- a/frontend/src/pages/Firewall/index.tsx +++ b/frontend/src/pages/Firewall/index.tsx @@ -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 { BsPlusLg, BsTrashFill } from "react-icons/bs" import { rem } from '@mantine/core'; @@ -20,7 +20,8 @@ import { LuArrowBigRightDash } from "react-icons/lu" import { ImCheckmark, ImCross } from "react-icons/im"; import { IoSettingsSharp } from "react-icons/io5"; import { SettingsModal } from "./SettingsModal"; - +import { FaDirections } from "react-icons/fa"; +import { PiWallLight } from "react-icons/pi"; export const Firewall = () => { @@ -346,7 +347,7 @@ export const Firewall = () => { - Firewall Rules + <ThemeIcon radius="md" size="md" variant='filled' color='red' ><PiWallLight size={20} /></ThemeIcon><Space w="xs" />Firewall Rules {isMedium?:} Enabled: @@ -361,8 +362,8 @@ export const Firewall = () => { {isMedium?:} - Rules: {rules.isLoading?0:rules.data?.rules.length} - + Rules: {rules.isLoading?0:rules.data?.rules.length} + setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)} diff --git a/frontend/src/pages/NFProxy/ServiceDetails.tsx b/frontend/src/pages/NFProxy/ServiceDetails.tsx new file mode 100644 index 0000000..95117f8 --- /dev/null +++ b/frontend/src/pages/NFProxy/ServiceDetails.tsx @@ -0,0 +1,200 @@ +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 { useState } from 'react'; +import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa'; +import { 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 } 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"; + +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 [tooltipStopOpened, setTooltipStopOpened] = useState(false); + const [tooltipBackOpened, setTooltipBackOpened] = useState(false); + const filterCode = nfproxyServiceFilterCodeQuery(srv??"") + const navigate = useNavigate() + const isMedium = isMediumScreen() + + if (services.isLoading) return + if (!srv || !serviceInfo || filtersList.isError) return + + 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 <> + + + + + <Box className="center-flex"> + <MdDoubleArrow /><Space w="sm" />{serviceInfo.name} + </Box> + + + {isMedium?null:} + + + {serviceInfo.status} + + + :{serviceInfo.port} + + + + Edit service + } onClick={()=>setEditModal(true)}>Service Settings + } onClick={()=>setRenameModal(true)}>Change service name + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + {isMedium?null:} + + + + {serviceInfo.edited_packets} + + {serviceInfo.blocked_packets} + + {serviceInfo.n_filters} + + {isMedium?:} + {serviceInfo.ip_int} on {serviceInfo.proto} + + {isMedium?null:} + + + navigate("/")} size="xl" radius="md" variant="filled" + aria-describedby="tooltip-back-id" + onFocus={() => setTooltipBackOpened(false)} onBlur={() => setTooltipBackOpened(false)} + onMouseEnter={() => setTooltipBackOpened(true)} onMouseLeave={() => setTooltipBackOpened(false)}> + + + + + + setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)} + onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}> + + + + + + + + + + + + + {filterCode.data?<> + <FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code + + : null} + + {(!filtersList.data || filtersList.data.length == 0)?<> + No filters found! Edit the proxy file + + Install the firegex client:<Space w="xs" /><Code mb={-4} >pip install fgex</Code> + + Then run the command:<Space w="xs" /><Code mb={-4} >fgex nfproxy</Code> + : + + {filtersList.data?.map( (filterInfo) => )} + + } + setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={serviceInfo} + /> + setEditModal(false)} + edit={serviceInfo} + /> + +} diff --git a/frontend/src/pages/NFProxy/index.tsx b/frontend/src/pages/NFProxy/index.tsx new file mode 100644 index 0000000..0817903 --- /dev/null +++ b/frontend/src/pages/NFProxy/index.tsx @@ -0,0 +1,91 @@ +import { ActionIcon, Badge, Box, 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 { nfproxyServiceQuery } from '../../components/NFProxy/utils'; +import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa'; +import { VscRegex } from 'react-icons/vsc'; + + +export default function NFProxy({ children }: { children: any }) { + + const navigator = useNavigate() + const [open, setOpen] = useState(false); + const {srv} = useParams() + const queryClient = useQueryClient() + const [tooltipRefreshOpened, setTooltipRefreshOpened] = useState(false); + const [tooltipAddServOpened, setTooltipAddServOpened] = useState(false); + const [tooltipAddOpened, setTooltipAddOpened] = useState(false); + const isMedium = isMediumScreen() + const services = nfproxyServiceQuery() + + useEffect(()=> { + if(services.isError) + errorNotify("NFProxy Update failed!", getErrorMessage(services.error)) + },[services.isError]) + + const closeModal = () => {setOpen(false);} + + return <> + + + <ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy + {isMedium?:} + + General stats: + + Services: {services.isLoading?0:services.data?.length} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)} + + + {isMedium?null:} + + {/* Will become the null a button to edit the source code? TODO */} + { srv?null + : + setOpen(true)} size="lg" radius="md" variant="filled" + onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)} + onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}> + + } + + + queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" + loading={services.isFetching} + onFocus={() => setTooltipRefreshOpened(false)} onBlur={() => setTooltipRefreshOpened(false)} + onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}> + + + + + + {srv?null:<> + + {(services.data && services.data?.length > 0)?services.data.map( srv => { + navigator("/nfproxy/"+srv.service_id) + }} />):<> No services found! Add one clicking the "+" buttons + + + setOpen(true)} size="xl" radius="md" variant="filled" + onFocus={() => setTooltipAddServOpened(false)} onBlur={() => setTooltipAddServOpened(false)} + onMouseEnter={() => setTooltipAddServOpened(true)} onMouseLeave={() => setTooltipAddServOpened(false)}> + + + } + } + + {srv?children:null} + {!srv?:null} + +} + diff --git a/frontend/src/pages/NFRegex/index.tsx b/frontend/src/pages/NFRegex/index.tsx index 6153458..19b0905 100644 --- a/frontend/src/pages/NFRegex/index.tsx +++ b/frontend/src/pages/NFRegex/index.tsx @@ -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 { BsPlusLg } from "react-icons/bs"; +import { BsPlusLg, BsRegex } from "react-icons/bs"; import { useNavigate, useParams } from 'react-router-dom'; import ServiceRow from '../../components/NFRegex/ServiceRow'; import { nfregexServiceQuery } from '../../components/NFRegex/utils'; @@ -9,7 +9,9 @@ import AddEditService from '../../components/NFRegex/AddEditService'; import AddNewRegex from '../../components/AddNewRegex'; import { useQueryClient } from '@tanstack/react-query'; import { TbReload } from 'react-icons/tb'; - +import { FaFilter } from 'react-icons/fa'; +import { FaServer } from "react-icons/fa6"; +import { VscRegex } from "react-icons/vsc"; function NFRegex({ children }: { children: any }) { @@ -33,14 +35,16 @@ function NFRegex({ children }: { children: any }) { return <> - Netfilter Regex + <ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex {isMedium?:} - Services: {services.isLoading?0:services.data?.length} + General stats: - Filtered Connections: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)} + Services: {services.isLoading?0:services.data?.length} - Regexes: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)} + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)} {isMedium?null:} diff --git a/frontend/src/pages/PortHijack/index.tsx b/frontend/src/pages/PortHijack/index.tsx index e4cdc08..e4f3640 100644 --- a/frontend/src/pages/PortHijack/index.tsx +++ b/frontend/src/pages/PortHijack/index.tsx @@ -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 { BsPlusLg } from "react-icons/bs"; import ServiceRow from '../../components/PortHijack/ServiceRow'; @@ -7,6 +7,8 @@ import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; import AddNewService from '../../components/PortHijack/AddNewService'; import { useQueryClient } from '@tanstack/react-query'; import { TbReload } from 'react-icons/tb'; +import { FaServer } from 'react-icons/fa'; +import { GrDirections } from 'react-icons/gr'; function PortHijack() { @@ -30,10 +32,10 @@ function PortHijack() { return <> - Hijack port to proxy + <ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy {isMedium?:} - Services: {services.isLoading?0:services.data?.length} + Services: {services.isLoading?0:services.data?.length} setOpen(true)} size="lg" radius="md" variant="filled" diff --git a/proxy-client/requirements.txt b/proxy-client/requirements.txt deleted file mode 100644 index 593817c..0000000 --- a/proxy-client/requirements.txt +++ /dev/null @@ -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 \ No newline at end of file