nfproxy module writing: written part of the firegex lib, frontend refactored and improved, c++ improves
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/frontend/coverage
|
/frontend/coverage
|
||||||
/proxy-client/firegex.egg-info
|
/fgex-lib/firegex.egg-info
|
||||||
/proxy-client/dist
|
/fgex-lib/dist
|
||||||
/proxy-client/fgex-pip/fgex.egg-info
|
/fgex-lib/fgex-pip/fgex.egg-info
|
||||||
/proxy-client/fgex-pip/dist
|
/fgex-lib/fgex-pip/dist
|
||||||
/backend/db/
|
/backend/db/
|
||||||
/backend/db/**
|
/backend/db/**
|
||||||
/frontend/build/
|
/frontend/build/
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ RUN bun i
|
|||||||
COPY ./frontend/ .
|
COPY ./frontend/ .
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
|
|
||||||
#Building main conteiner
|
#Building main conteiner
|
||||||
FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest
|
FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest
|
||||||
RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \
|
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
|
ADD ./backend/requirements.txt /execute/requirements.txt
|
||||||
RUN uv pip install --no-cache --system -r /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
|
COPY ./backend/binsrc /execute/binsrc
|
||||||
RUN g++ binsrc/nfregex.cpp -o modules/cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
RUN g++ binsrc/nfregex.cpp -o modules/cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
||||||
|
|||||||
@@ -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(){
|
FilterAction get_action(){
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -141,7 +150,7 @@ class PktRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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];
|
char buf[MNL_SOCKET_BUFFER_SIZE];
|
||||||
struct nlmsghdr *nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(res_id));
|
struct nlmsghdr *nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(res_id));
|
||||||
switch (action)
|
switch (action)
|
||||||
@@ -153,7 +162,9 @@ class PktRequest {
|
|||||||
nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_DROP );
|
nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_DROP );
|
||||||
break;
|
break;
|
||||||
case FilterAction::MANGLE:{
|
case FilterAction::MANGLE:{
|
||||||
if (is_ipv6){
|
if (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());
|
nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv6->serialize().data(), ipv6->size());
|
||||||
}else{
|
}else{
|
||||||
nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv4->serialize().data(), ipv4->size());
|
nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv4->serialize().data(), ipv4->size());
|
||||||
|
|||||||
@@ -1,18 +1,87 @@
|
|||||||
#define PY_SSIZE_T_CLEAN
|
#define PY_SSIZE_T_CLEAN
|
||||||
#include <Python.h>
|
#include <Python.h>
|
||||||
|
|
||||||
#include "proxytun/settings.cpp"
|
#include "pyproxy/settings.cpp"
|
||||||
#include "proxytun/proxytun.cpp"
|
#include "pyproxy/pyproxy.cpp"
|
||||||
#include "classes/netfilter.cpp"
|
#include "classes/netfilter.cpp"
|
||||||
#include <syncstream>
|
#include <syncstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <endian.h>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace Firegex::PyProxy;
|
using namespace Firegex::PyProxy;
|
||||||
using Firegex::NfQueue::MultiThreadQueue;
|
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
|
||||||
|
<user_code>
|
||||||
|
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 read_check(int __fd, void *__buf, size_t __nbytes){
|
||||||
ssize_t bytes = read(__fd, __buf, __nbytes);
|
ssize_t bytes = read(__fd, __buf, __nbytes);
|
||||||
if (bytes == 0){
|
if (bytes == 0){
|
||||||
@@ -30,7 +99,10 @@ void config_updater (){
|
|||||||
while (true){
|
while (true){
|
||||||
uint32_t code_size;
|
uint32_t code_size;
|
||||||
read_check(STDIN_FILENO, &code_size, 4);
|
read_check(STDIN_FILENO, &code_size, 4);
|
||||||
vector<uint8_t> 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);
|
read_check(STDIN_FILENO, code.data(), code_size);
|
||||||
cerr << "[info] [updater] Updating configuration" << endl;
|
cerr << "[info] [updater] Updating configuration" << endl;
|
||||||
try{
|
try{
|
||||||
@@ -44,10 +116,12 @@ void config_updater (){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]){
|
int main(int argc, char *argv[]){
|
||||||
|
|
||||||
Py_Initialize();
|
Py_Initialize();
|
||||||
atexit(Py_Finalize);
|
atexit(Py_Finalize);
|
||||||
|
init_handle_packet_code(); //Compile the static code used to handle packets
|
||||||
|
|
||||||
if (freopen(nullptr, "rb", stdin) == nullptr){ // We need to read from stdin binary data
|
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;
|
cerr << "[fatal] [main] Failed to reopen stdin in binary mode" << endl;
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
#ifndef PROXY_TUNNEL_CLASS_CPP
|
|
||||||
#define PROXY_TUNNEL_CLASS_CPP
|
|
||||||
|
|
||||||
#include <linux/netfilter/nfnetlink_queue.h>
|
|
||||||
#include <libnetfilter_queue/libnetfilter_queue.h>
|
|
||||||
#include <linux/netfilter/nfnetlink_conntrack.h>
|
|
||||||
#include <tins/tins.h>
|
|
||||||
#include <tins/tcp_ip/stream_follower.h>
|
|
||||||
#include <tins/tcp_ip/stream_identifier.h>
|
|
||||||
#include <libmnl/libmnl.h>
|
|
||||||
#include <linux/netfilter.h>
|
|
||||||
#include <linux/netfilter/nfnetlink.h>
|
|
||||||
#include <linux/types.h>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <thread>
|
|
||||||
#include <syncstream>
|
|
||||||
#include <iostream>
|
|
||||||
#include "../classes/netfilter.cpp"
|
|
||||||
#include "stream_ctx.cpp"
|
|
||||||
#include "settings.cpp"
|
|
||||||
|
|
||||||
using Tins::TCPIP::Stream;
|
|
||||||
using Tins::TCPIP::StreamFollower;
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
namespace Firegex {
|
|
||||||
namespace PyProxy {
|
|
||||||
|
|
||||||
class PyProxyQueue: public NfQueue::ThreadNfQueue<PyProxyQueue> {
|
|
||||||
public:
|
|
||||||
stream_ctx sctx;
|
|
||||||
StreamFollower follower;
|
|
||||||
|
|
||||||
struct {
|
|
||||||
bool matching_has_been_called = false;
|
|
||||||
bool already_closed = false;
|
|
||||||
bool result;
|
|
||||||
NfQueue::PktRequest<PyProxyQueue>* pkt;
|
|
||||||
} match_ctx;
|
|
||||||
|
|
||||||
void before_loop() override {
|
|
||||||
follower.new_stream_callback(bind(on_new_stream, placeholders::_1, this));
|
|
||||||
follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool filter_action(NfQueue::PktRequest<PyProxyQueue>* pkt){
|
|
||||||
shared_ptr<PyCodeConfig> conf = config;
|
|
||||||
|
|
||||||
auto stream_search = sctx.streams_ctx.find(pkt->sid);
|
|
||||||
pyfilter_ctx* stream_match;
|
|
||||||
if (stream_search == sctx.streams_ctx.end()){
|
|
||||||
// TODO: New pyfilter_ctx
|
|
||||||
}else{
|
|
||||||
stream_match = stream_search->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool has_matched = false;
|
|
||||||
//TODO exec filtering action
|
|
||||||
|
|
||||||
if (has_matched){
|
|
||||||
// Say to firegex what filter has matched
|
|
||||||
//osyncstream(cout) << "BLOCKED " << rules_vector[match_res.matched] << endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//If the stream has already been matched, drop all data, and try to close the connection
|
|
||||||
static void keep_fin_packet(PyProxyQueue* pkt){
|
|
||||||
pkt->match_ctx.matching_has_been_called = true;
|
|
||||||
pkt->match_ctx.already_closed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void on_data_recv(Stream& stream, PyProxyQueue* pkt, string data) {
|
|
||||||
pkt->match_ctx.matching_has_been_called = true;
|
|
||||||
pkt->match_ctx.already_closed = false;
|
|
||||||
bool result = pkt->filter_action(pkt->match_ctx.pkt);
|
|
||||||
if (!result){
|
|
||||||
pkt->sctx.clean_stream_by_id(pkt->match_ctx.pkt->sid);
|
|
||||||
stream.client_data_callback(bind(keep_fin_packet, pkt));
|
|
||||||
stream.server_data_callback(bind(keep_fin_packet, pkt));
|
|
||||||
}
|
|
||||||
pkt->match_ctx.result = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Input data filtering
|
|
||||||
static void on_client_data(Stream& stream, PyProxyQueue* pkt) {
|
|
||||||
on_data_recv(stream, pkt, string(stream.client_payload().begin(), stream.client_payload().end()));
|
|
||||||
}
|
|
||||||
|
|
||||||
//Server data filtering
|
|
||||||
static void on_server_data(Stream& stream, PyProxyQueue* pkt) {
|
|
||||||
on_data_recv(stream, pkt, string(stream.server_payload().begin(), stream.server_payload().end()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A stream was terminated. The second argument is the reason why it was terminated
|
|
||||||
static void on_stream_close(Stream& stream, PyProxyQueue* pkt) {
|
|
||||||
stream_id stream_id = stream_id::make_identifier(stream);
|
|
||||||
pkt->sctx.clean_stream_by_id(stream_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void on_new_stream(Stream& stream, PyProxyQueue* pkt) {
|
|
||||||
stream.auto_cleanup_payloads(true);
|
|
||||||
if (stream.is_partial_stream()) {
|
|
||||||
//TODO take a decision about this...
|
|
||||||
stream.enable_recovery_mode(10 * 1024);
|
|
||||||
}
|
|
||||||
stream.client_data_callback(bind(on_client_data, placeholders::_1, pkt));
|
|
||||||
stream.server_data_callback(bind(on_server_data, placeholders::_1, pkt));
|
|
||||||
stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, pkt));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void handle_next_packet(NfQueue::PktRequest<PyProxyQueue>* pkt) override{
|
|
||||||
if (pkt->l4_proto != NfQueue::L4Proto::TCP){
|
|
||||||
throw invalid_argument("Only TCP and UDP are supported");
|
|
||||||
}
|
|
||||||
Tins::PDU* application_layer = pkt->tcp->inner_pdu();
|
|
||||||
u_int16_t payload_size = 0;
|
|
||||||
if (application_layer != nullptr){
|
|
||||||
payload_size = application_layer->size();
|
|
||||||
}
|
|
||||||
match_ctx.matching_has_been_called = false;
|
|
||||||
match_ctx.pkt = pkt;
|
|
||||||
if (pkt->is_ipv6){
|
|
||||||
follower.process_packet(*pkt->ipv6);
|
|
||||||
}else{
|
|
||||||
follower.process_packet(*pkt->ipv4);
|
|
||||||
}
|
|
||||||
// Do an action only is an ordered packet has been received
|
|
||||||
if (match_ctx.matching_has_been_called){
|
|
||||||
bool empty_payload = payload_size == 0;
|
|
||||||
//In this 2 cases we have to remove all data about the stream
|
|
||||||
if (!match_ctx.result || match_ctx.already_closed){
|
|
||||||
sctx.clean_stream_by_id(pkt->sid);
|
|
||||||
//If the packet has data, we have to remove it
|
|
||||||
if (!empty_payload){
|
|
||||||
Tins::PDU* data_layer = pkt->tcp->release_inner_pdu();
|
|
||||||
if (data_layer != nullptr){
|
|
||||||
delete data_layer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//For the first matched data or only for data packets, we set FIN bit
|
|
||||||
//This only for client packets, because this will trigger server to close the connection
|
|
||||||
//Packets will be filtered anyway also if client don't send packets
|
|
||||||
if ((!match_ctx.result || !empty_payload) && pkt->is_input){
|
|
||||||
pkt->tcp->set_flag(Tins::TCP::FIN,1);
|
|
||||||
pkt->tcp->set_flag(Tins::TCP::ACK,1);
|
|
||||||
pkt->tcp->set_flag(Tins::TCP::SYN,0);
|
|
||||||
}
|
|
||||||
//Send the edited packet to the kernel
|
|
||||||
return pkt->mangle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pkt->accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
~PyProxyQueue() {
|
|
||||||
sctx.clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}}
|
|
||||||
#endif // PROXY_TUNNEL_CLASS_CPP
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#ifndef PROXY_TUNNEL_SETTINGS_CPP
|
|
||||||
#define PROXY_TUNNEL_SETTINGS_CPP
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
class PyCodeConfig{
|
|
||||||
public:
|
|
||||||
const vector<uint8_t> code;
|
|
||||||
public:
|
|
||||||
PyCodeConfig(vector<uint8_t> pycode): code(pycode){}
|
|
||||||
PyCodeConfig(): code(vector<uint8_t>()){}
|
|
||||||
|
|
||||||
~PyCodeConfig(){}
|
|
||||||
};
|
|
||||||
|
|
||||||
shared_ptr<PyCodeConfig> config;
|
|
||||||
|
|
||||||
#endif // PROXY_TUNNEL_SETTINGS_CPP
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
#ifndef STREAM_CTX_CPP
|
|
||||||
#define STREAM_CTX_CPP
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <tins/tcp_ip/stream_identifier.h>
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
typedef Tins::TCPIP::StreamIdentifier stream_id;
|
|
||||||
|
|
||||||
struct pyfilter_ctx {
|
|
||||||
void * pyglob; // TODO python glob???
|
|
||||||
string pycode;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef map<stream_id, pyfilter_ctx*> matching_map;
|
|
||||||
|
|
||||||
struct stream_ctx {
|
|
||||||
matching_map streams_ctx;
|
|
||||||
|
|
||||||
void clean_stream_by_id(stream_id sid){
|
|
||||||
auto stream_search = streams_ctx.find(sid);
|
|
||||||
if (stream_search != streams_ctx.end()){
|
|
||||||
auto stream_match = stream_search->second;
|
|
||||||
//DEALLOC PY GLOB TODO
|
|
||||||
delete stream_match;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void clean(){
|
|
||||||
for (auto ele: streams_ctx){
|
|
||||||
//TODO dealloc ele.second.pyglob
|
|
||||||
delete ele.second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // STREAM_CTX_CPP
|
|
||||||
232
backend/binsrc/pyproxy/pyproxy.cpp
Normal file
232
backend/binsrc/pyproxy/pyproxy.cpp
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
#ifndef PROXY_TUNNEL_CLASS_CPP
|
||||||
|
#define PROXY_TUNNEL_CLASS_CPP
|
||||||
|
|
||||||
|
#include <linux/netfilter/nfnetlink_queue.h>
|
||||||
|
#include <libnetfilter_queue/libnetfilter_queue.h>
|
||||||
|
#include <linux/netfilter/nfnetlink_conntrack.h>
|
||||||
|
#include <tins/tins.h>
|
||||||
|
#include <tins/tcp_ip/stream_follower.h>
|
||||||
|
#include <tins/tcp_ip/stream_identifier.h>
|
||||||
|
#include <libmnl/libmnl.h>
|
||||||
|
#include <linux/netfilter.h>
|
||||||
|
#include <linux/netfilter/nfnetlink.h>
|
||||||
|
#include <linux/types.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <thread>
|
||||||
|
#include <syncstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include "../classes/netfilter.cpp"
|
||||||
|
#include "stream_ctx.cpp"
|
||||||
|
#include "settings.cpp"
|
||||||
|
#include <Python.h>
|
||||||
|
|
||||||
|
using Tins::TCPIP::Stream;
|
||||||
|
using Tins::TCPIP::StreamFollower;
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
namespace Firegex {
|
||||||
|
namespace PyProxy {
|
||||||
|
|
||||||
|
class PyProxyQueue: public NfQueue::ThreadNfQueue<PyProxyQueue> {
|
||||||
|
private:
|
||||||
|
u_int16_t latest_config_ver = 0;
|
||||||
|
public:
|
||||||
|
stream_ctx sctx;
|
||||||
|
StreamFollower follower;
|
||||||
|
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<PyProxyQueue>* 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<PyProxyQueue>* 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<PyCodeConfig> 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<PyProxyQueue>* pkt) override{
|
||||||
|
if (pkt->l4_proto != NfQueue::L4Proto::TCP){
|
||||||
|
throw invalid_argument("Only TCP and UDP are supported");
|
||||||
|
}
|
||||||
|
Tins::PDU* application_layer = pkt->tcp->inner_pdu();
|
||||||
|
u_int16_t payload_size = 0;
|
||||||
|
if (application_layer != nullptr){
|
||||||
|
payload_size = application_layer->size();
|
||||||
|
}
|
||||||
|
match_ctx.matching_has_been_called = false;
|
||||||
|
match_ctx.pkt = pkt;
|
||||||
|
if (pkt->is_ipv6){
|
||||||
|
follower.process_packet(*pkt->ipv6);
|
||||||
|
}else{
|
||||||
|
follower.process_packet(*pkt->ipv4);
|
||||||
|
}
|
||||||
|
// Do an action only is an ordered packet has been received
|
||||||
|
if (match_ctx.matching_has_been_called){
|
||||||
|
bool empty_payload = payload_size == 0;
|
||||||
|
//In this 2 cases we have to remove all data about the stream
|
||||||
|
if (!match_ctx.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
|
||||||
71
backend/binsrc/pyproxy/settings.cpp
Normal file
71
backend/binsrc/pyproxy/settings.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#ifndef PROXY_TUNNEL_SETTINGS_CPP
|
||||||
|
#define PROXY_TUNNEL_SETTINGS_CPP
|
||||||
|
|
||||||
|
#include <Python.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
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(), "<pyfilter>", 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<PyCodeConfig> config;
|
||||||
|
PyObject* py_handle_packet_code = nullptr;
|
||||||
|
|
||||||
|
void init_handle_packet_code(){
|
||||||
|
py_handle_packet_code = Py_CompileStringExFlags(
|
||||||
|
"firegex.nfproxy.internals.handle_packet()\n", "<pyfilter>",
|
||||||
|
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
|
||||||
|
|
||||||
202
backend/binsrc/pyproxy/stream_ctx.cpp
Normal file
202
backend/binsrc/pyproxy/stream_ctx.cpp
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
#ifndef STREAM_CTX_CPP
|
||||||
|
#define STREAM_CTX_CPP
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <tins/tcp_ip/stream_identifier.h>
|
||||||
|
#include <map>
|
||||||
|
#include <Python.h>
|
||||||
|
#include "../classes/netfilter.cpp"
|
||||||
|
#include "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<PyProxyQueue>* 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<stream_id, pyfilter_ctx*> matching_map;
|
||||||
|
|
||||||
|
struct stream_ctx {
|
||||||
|
matching_map streams_ctx;
|
||||||
|
|
||||||
|
void clean_stream_by_id(stream_id sid){
|
||||||
|
auto stream_search = streams_ctx.find(sid);
|
||||||
|
if (stream_search != streams_ctx.end()){
|
||||||
|
auto stream_match = stream_search->second;
|
||||||
|
delete stream_match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void clean(){
|
||||||
|
for (auto ele: streams_ctx){
|
||||||
|
delete ele.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}}
|
||||||
|
#endif // STREAM_CTX_CPP
|
||||||
@@ -76,12 +76,11 @@ class RegexRules{
|
|||||||
}else{
|
}else{
|
||||||
hs_free_database(db);
|
hs_free_database(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static inline u_int16_t glob_seq = 0;
|
static inline uint16_t glob_seq = 0;
|
||||||
u_int16_t version;
|
uint16_t version;
|
||||||
vector<pair<string, decoded_regex>> decoded_input_rules;
|
vector<pair<string, decoded_regex>> decoded_input_rules;
|
||||||
vector<pair<string, decoded_regex>> decoded_output_rules;
|
vector<pair<string, decoded_regex>> decoded_output_rules;
|
||||||
bool is_stream = true;
|
bool is_stream = true;
|
||||||
@@ -97,8 +96,6 @@ class RegexRules{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void fill_ruleset(vector<pair<string, decoded_regex>> & decoded, regex_ruleset & ruleset){
|
void fill_ruleset(vector<pair<string, decoded_regex>> & decoded, regex_ruleset & ruleset){
|
||||||
size_t n_of_regex = decoded.size();
|
size_t n_of_regex = decoded.size();
|
||||||
if (n_of_regex == 0){
|
if (n_of_regex == 0){
|
||||||
@@ -150,7 +147,6 @@ class RegexRules{
|
|||||||
public:
|
public:
|
||||||
RegexRules(vector<string> raw_rules, bool is_stream){
|
RegexRules(vector<string> raw_rules, bool is_stream){
|
||||||
this->is_stream = is_stream;
|
this->is_stream = is_stream;
|
||||||
this->version = ++glob_seq; // 0 version is a invalid version (useful for some logics)
|
|
||||||
for(string ele : raw_rules){
|
for(string ele : raw_rules){
|
||||||
try{
|
try{
|
||||||
decoded_regex rule = decode_regex(ele);
|
decoded_regex rule = decode_regex(ele);
|
||||||
@@ -170,6 +166,7 @@ class RegexRules{
|
|||||||
free_dbs();
|
free_dbs();
|
||||||
throw current_exception();
|
throw current_exception();
|
||||||
}
|
}
|
||||||
|
this->version = ++glob_seq; // 0 version is the null version
|
||||||
}
|
}
|
||||||
|
|
||||||
u_int16_t ver(){
|
u_int16_t ver(){
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class FiregexInterceptor:
|
|||||||
self.update_task: asyncio.Task
|
self.update_task: asyncio.Task
|
||||||
self.ack_arrived = False
|
self.ack_arrived = False
|
||||||
self.ack_status = None
|
self.ack_status = None
|
||||||
self.ack_fail_what = ""
|
self.ack_fail_what = "Unknown"
|
||||||
self.ack_lock = asyncio.Lock()
|
self.ack_lock = asyncio.Lock()
|
||||||
|
|
||||||
async def _call_stats_updater_callback(self, filter: PyFilter):
|
async def _call_stats_updater_callback(self, filter: PyFilter):
|
||||||
@@ -79,12 +79,14 @@ class FiregexInterceptor:
|
|||||||
if filter_id in self.filter_map:
|
if filter_id in self.filter_map:
|
||||||
self.filter_map[filter_id].blocked_packets+=1
|
self.filter_map[filter_id].blocked_packets+=1
|
||||||
await self.filter_map[filter_id].update()
|
await self.filter_map[filter_id].update()
|
||||||
if line.startswith("EDITED "):
|
if line.startswith("MANGLED "):
|
||||||
filter_id = line.split()[1]
|
filter_id = line.split()[1]
|
||||||
async with self.filter_map_lock:
|
async with self.filter_map_lock:
|
||||||
if filter_id in self.filter_map:
|
if filter_id in self.filter_map:
|
||||||
self.filter_map[filter_id].edited_packets+=1
|
self.filter_map[filter_id].edited_packets+=1
|
||||||
await self.filter_map[filter_id].update()
|
await self.filter_map[filter_id].update()
|
||||||
|
if line.startswith("EXCEPTION"):
|
||||||
|
print("TODO EXCEPTION HANDLING") # TODO
|
||||||
if line.startswith("ACK "):
|
if line.startswith("ACK "):
|
||||||
self.ack_arrived = True
|
self.ack_arrived = True
|
||||||
self.ack_status = line.split()[1].upper() == "OK"
|
self.ack_status = line.split()[1].upper() == "OK"
|
||||||
@@ -103,10 +105,9 @@ class FiregexInterceptor:
|
|||||||
if self.process and self.process.returncode is None:
|
if self.process and self.process.returncode is None:
|
||||||
self.process.kill()
|
self.process.kill()
|
||||||
|
|
||||||
async def _update_config(self, filters_codes):
|
async def _update_config(self, code):
|
||||||
async with self.update_config_lock:
|
async with self.update_config_lock:
|
||||||
# TODO write compiled code correctly
|
self.process.stdin.write(len(code).to_bytes(4, byteorder='big')+code.encode())
|
||||||
# self.process.stdin.write((" ".join(filters_codes)+"\n").encode())
|
|
||||||
await self.process.stdin.drain()
|
await self.process.stdin.drain()
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(3):
|
async with asyncio.timeout(3):
|
||||||
@@ -114,11 +115,22 @@ class FiregexInterceptor:
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
if not self.ack_arrived or not self.ack_status:
|
if not self.ack_arrived or not self.ack_status:
|
||||||
|
await self.stop()
|
||||||
raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}")
|
raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}")
|
||||||
|
|
||||||
async def reload(self, filters:list[PyFilter]):
|
async def reload(self, filters:list[PyFilter]):
|
||||||
async with self.filter_map_lock:
|
async with self.filter_map_lock:
|
||||||
self.filter_map = self.compile_filters(filters)
|
if os.path.exists(f"db/nfproxy_filters/{self.srv.id}.py"):
|
||||||
# TODO COMPILE CODE
|
with open(f"db/nfproxy_filters/{self.srv.id}.py") as f:
|
||||||
#await self._update_config(filters_codes) TODO pass the compiled code
|
filter_file = f.read()
|
||||||
|
else:
|
||||||
|
filter_file = ""
|
||||||
|
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()"
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class FiregexInterceptor:
|
|||||||
self.update_task: asyncio.Task
|
self.update_task: asyncio.Task
|
||||||
self.ack_arrived = False
|
self.ack_arrived = False
|
||||||
self.ack_status = None
|
self.ack_status = None
|
||||||
self.ack_fail_what = ""
|
self.ack_fail_what = "Unknown"
|
||||||
self.ack_lock = asyncio.Lock()
|
self.ack_lock = asyncio.Lock()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -160,6 +160,7 @@ class FiregexInterceptor:
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
if not self.ack_arrived or not self.ack_status:
|
if not self.ack_arrived or not self.ack_status:
|
||||||
|
await self.stop()
|
||||||
raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}")
|
raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ from modules.nfproxy.firewall import STATUS, FirewallManager
|
|||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
||||||
from utils.models import ResetRequest, StatusMessageModel
|
from utils.models import ResetRequest, StatusMessageModel
|
||||||
|
import os
|
||||||
|
from firegex.nfproxy.internals import get_filter_names
|
||||||
|
from fastapi.responses import PlainTextResponse
|
||||||
|
|
||||||
class ServiceModel(BaseModel):
|
class ServiceModel(BaseModel):
|
||||||
service_id: str
|
service_id: str
|
||||||
@@ -47,6 +50,9 @@ class ServiceAddResponse(BaseModel):
|
|||||||
status:str
|
status:str
|
||||||
service_id: str|None = None
|
service_id: str|None = None
|
||||||
|
|
||||||
|
class SetPyFilterForm(BaseModel):
|
||||||
|
code: str
|
||||||
|
|
||||||
app = APIRouter()
|
app = APIRouter()
|
||||||
|
|
||||||
db = SQLite('db/nft-pyfilters.db', {
|
db = SQLite('db/nft-pyfilters.db', {
|
||||||
@@ -174,6 +180,8 @@ async def service_delete(service_id: str):
|
|||||||
"""Request the deletion of a specific service"""
|
"""Request the deletion of a specific service"""
|
||||||
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
|
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
|
||||||
db.query('DELETE FROM pyfilter WHERE service_id = ?;', service_id)
|
db.query('DELETE FROM pyfilter WHERE service_id = ?;', service_id)
|
||||||
|
if os.path.exists(f"db/nfproxy_filters/{service_id}.py"):
|
||||||
|
os.remove(f"db/nfproxy_filters/{service_id}.py")
|
||||||
await firewall.remove(service_id)
|
await firewall.remove(service_id)
|
||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
@@ -253,17 +261,6 @@ async def get_pyfilter_by_id(filter_id: int):
|
|||||||
raise HTTPException(status_code=400, detail="This filter does not exists!")
|
raise HTTPException(status_code=400, detail="This filter does not exists!")
|
||||||
return res[0]
|
return res[0]
|
||||||
|
|
||||||
@app.delete('/pyfilters/{filter_id}', response_model=StatusMessageModel)
|
|
||||||
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)
|
@app.post('/pyfilters/{filter_id}/enable', response_model=StatusMessageModel)
|
||||||
async def pyfilter_enable(filter_id: int):
|
async def pyfilter_enable(filter_id: int):
|
||||||
"""Request the enabling of a pyfilter"""
|
"""Request the enabling of a pyfilter"""
|
||||||
@@ -304,6 +301,49 @@ async def add_new_service(form: ServiceAddForm):
|
|||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok', 'service_id': srv_id}
|
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
|
#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
|
# 2. a socketio mechanism to lock the previous feature
|
||||||
0
proxy-client/fgex-pip/fgex/__main__.py → fgex-lib/fgex
Normal file → Executable file
0
proxy-client/fgex-pip/fgex/__main__.py → fgex-lib/fgex
Normal file → Executable file
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# TODO implement cli start function
|
|
||||||
from firegex.cli import run
|
from firegex.cli import run
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
1
proxy-client/fgex → fgex-lib/firegex/__main__.py
Executable file → Normal file
1
proxy-client/fgex → fgex-lib/firegex/__main__.py
Executable file → Normal file
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# TODO implement cli start function
|
|
||||||
from firegex.cli import run
|
from firegex.cli import run
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
5
fgex-lib/firegex/cli.py
Normal file
5
fgex-lib/firegex/cli.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
pass # TODO implement me
|
||||||
|
|
||||||
38
fgex-lib/firegex/nfproxy/__init__.py
Normal file
38
fgex-lib/firegex/nfproxy/__init__.py
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
161
fgex-lib/firegex/nfproxy/internals.py
Normal file
161
fgex-lib/firegex/nfproxy/internals.py
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
71
fgex-lib/firegex/nfproxy/params.py
Normal file
71
fgex-lib/firegex/nfproxy/params.py
Normal file
@@ -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"])
|
||||||
|
|
||||||
|
|
||||||
10
fgex-lib/requirements.txt
Normal file
10
fgex-lib/requirements.txt
Normal file
@@ -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
|
||||||
|
|
||||||
@@ -5,17 +5,19 @@
|
|||||||
"name": "firegex-frontend",
|
"name": "firegex-frontend",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^16.6.0",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@mantine/core": "^7.16.2",
|
"@mantine/code-highlight": "^7.17.0",
|
||||||
"@mantine/form": "^7.16.2",
|
"@mantine/core": "^7.16.3",
|
||||||
"@mantine/hooks": "^7.16.2",
|
"@mantine/form": "^7.16.3",
|
||||||
"@mantine/modals": "^7.16.2",
|
"@mantine/hooks": "^7.16.3",
|
||||||
"@mantine/notifications": "^7.16.2",
|
"@mantine/modals": "^7.16.3",
|
||||||
|
"@mantine/notifications": "^7.16.3",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^20.17.16",
|
"@types/node": "^20.17.17",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"install": "^0.13.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
@@ -141,17 +143,19 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
"@mantine/core": ["@mantine/core@7.16.3", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-cxhIpfd2i0Zmk9TKdejYAoIvWouMGhzK3OOX+VRViZ5HEjnTQCGl2h3db56ThqB6NfVPCno6BPbt5lwekTtmuQ=="],
|
"@mantine/code-highlight": ["@mantine/code-highlight@7.17.0", "", { "dependencies": { "clsx": "^2.1.1", "highlight.js": "^11.10.0" }, "peerDependencies": { "@mantine/core": "7.17.0", "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-i6MvxW+PtdRNYHCm8Qa/aiMkLr47EYS0+12rf5XhDVdYZy+0+XiRkwBsxnvzQfKqv0QtH2dchBJDEBMmPB/nVw=="],
|
||||||
|
|
||||||
"@mantine/form": ["@mantine/form@7.16.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-GqomUG2Ri5adxYsTU1S5IhKRPcqTG5JkPvMERns8PQAcUz/lvzsnk3wY1v4K5CEbCAdpimle4bSsZTM9g697vg=="],
|
"@mantine/core": ["@mantine/core@7.17.0", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-AU5UFewUNzBCUXIq5Jk6q402TEri7atZW61qHW6P0GufJ2W/JxGHRvgmHOVHTVIcuWQRCt9SBSqZoZ/vHs9LhA=="],
|
||||||
|
|
||||||
"@mantine/hooks": ["@mantine/hooks@7.16.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-B94FBWk5Sc81tAjV+B3dGh/gKzfqzpzVC/KHyBRWOOyJRqeeRbI/FAaJo4zwppyQo1POSl5ArdyjtDRrRIj2SQ=="],
|
"@mantine/form": ["@mantine/form@7.17.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-LONdeb+wL8h9fvyQ339ZFLxqrvYff+b+H+kginZhnr45OBTZDLXNVAt/YoKVFEkynF9WDJjdBVrXKcOZvPgmrA=="],
|
||||||
|
|
||||||
"@mantine/modals": ["@mantine/modals@7.16.3", "", { "peerDependencies": { "@mantine/core": "7.16.3", "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-BJuDzRugK6xLbuFTTo8NLJumVvVmSYsNVcEtmlXOWTE3NkDGktBXGKo8V1B0XfJ9/d/rZw7HCE0p4i76MtA+bQ=="],
|
"@mantine/hooks": ["@mantine/hooks@7.17.0", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-vo3K49mLy1nJ8LQNb5KDbJgnX0xwt3Y8JOF3ythjB5LEFMptdLSSgulu64zj+QHtzvffFCsMb05DbTLLpVP/JQ=="],
|
||||||
|
|
||||||
"@mantine/notifications": ["@mantine/notifications@7.16.3", "", { "dependencies": { "@mantine/store": "7.16.3", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "7.16.3", "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-wtEME9kSYfXWYmAmQUZ8c+rwNmhdWRBaW1mlPdQsPkzMqkv4q6yy0IpgwcnuHStSG9EHaQBXazmVxMZJdEAWBQ=="],
|
"@mantine/modals": ["@mantine/modals@7.17.0", "", { "peerDependencies": { "@mantine/core": "7.17.0", "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-4sfiFxIxMxfm2RH4jXMN+cr8tFS5AexXG4TY7TRN/ySdkiWtFVvDe5l2/KRWWeWwDUb7wQhht8Ompj5KtexlEA=="],
|
||||||
|
|
||||||
"@mantine/store": ["@mantine/store@7.16.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-6M2M5+0BrRtnVv+PUmr04tY1RjPqyapaHplo90uK1NMhP/1EIqrwTL9KoEtCNCJ5pog1AQtu0bj0QPbqUvxwLg=="],
|
"@mantine/notifications": ["@mantine/notifications@7.17.0", "", { "dependencies": { "@mantine/store": "7.17.0", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "7.17.0", "@mantine/hooks": "7.17.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-xejr1WW02NrrrE4HPDoownILJubcjLLwCDeTk907ZeeHKBEPut7RukEq6gLzOZBhNhKdPM+vCM7GcbXdaLZq/Q=="],
|
||||||
|
|
||||||
|
"@mantine/store": ["@mantine/store@7.17.0", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-nhWRYRLqvAjrD/ApKCXxuHyTWg2b5dC06Z5gmO8udj4pBgndNf9nmCl+Of90H6bgOa56moJA7UQyXoF1SfxqVg=="],
|
||||||
|
|
||||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
|
||||||
|
|
||||||
@@ -205,7 +209,7 @@
|
|||||||
|
|
||||||
"@types/jest": ["@types/jest@27.5.2", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA=="],
|
"@types/jest": ["@types/jest@27.5.2", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@20.17.17", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg=="],
|
"@types/node": ["@types/node@20.17.19", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A=="],
|
||||||
|
|
||||||
"@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="],
|
"@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="],
|
||||||
|
|
||||||
@@ -295,12 +299,16 @@
|
|||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||||
|
|
||||||
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|
||||||
|
"install": ["install@0.13.0", "", {}, "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="],
|
||||||
|
|
||||||
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||||
|
|
||||||
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
|
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
|
||||||
@@ -365,7 +373,7 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
|
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
|
||||||
|
|
||||||
"react-icons": ["react-icons@5.4.0", "", { "peerDependencies": { "react": "*" } }, "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ=="],
|
"react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
|
||||||
|
|
||||||
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
@@ -379,9 +387,9 @@
|
|||||||
|
|
||||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
"react-router": ["react-router@7.1.5", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA=="],
|
"react-router": ["react-router@7.2.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ=="],
|
||||||
|
|
||||||
"react-router-dom": ["react-router-dom@7.1.5", "", { "dependencies": { "react-router": "7.1.5" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ=="],
|
"react-router-dom": ["react-router-dom@7.2.0", "", { "dependencies": { "react-router": "7.2.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q=="],
|
||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,23 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^16.6.0",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@mantine/core": "^7.16.3",
|
"@mantine/code-highlight": "^7.17.0",
|
||||||
"@mantine/form": "^7.16.3",
|
"@mantine/core": "^7.17.0",
|
||||||
"@mantine/hooks": "^7.16.3",
|
"@mantine/form": "^7.17.0",
|
||||||
"@mantine/modals": "^7.16.3",
|
"@mantine/hooks": "^7.17.0",
|
||||||
"@mantine/notifications": "^7.16.3",
|
"@mantine/modals": "^7.17.0",
|
||||||
|
"@mantine/notifications": "^7.17.0",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^20.17.17",
|
"@types/node": "^20.17.19",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"install": "^0.13.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.1.5",
|
"react-router-dom": "^7.2.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails';
|
|||||||
import PortHijack from './pages/PortHijack';
|
import PortHijack from './pages/PortHijack';
|
||||||
import { Firewall } from './pages/Firewall';
|
import { Firewall } from './pages/Firewall';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import NFProxy from './pages/NFProxy';
|
||||||
|
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||||
|
|
||||||
|
|
||||||
const socket = IS_DEV?io("ws://"+DEV_IP_BACKEND, {transports: ["websocket"], path:"/sock/socket.io" }):io({transports: ["websocket"], path:"/sock/socket.io"});
|
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() {
|
|||||||
<Route path="nfregex" element={<NFRegex><Outlet /></NFRegex>} >
|
<Route path="nfregex" element={<NFRegex><Outlet /></NFRegex>} >
|
||||||
<Route path=":srv" element={<ServiceDetailsNFRegex />} />
|
<Route path=":srv" element={<ServiceDetailsNFRegex />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
||||||
|
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
||||||
|
</Route>
|
||||||
<Route path="firewall" element={<Firewall />} />
|
<Route path="firewall" element={<Firewall />} />
|
||||||
<Route path="porthijack" element={<PortHijack />} />
|
<Route path="porthijack" element={<PortHijack />} />
|
||||||
<Route path="*" element={<HomeRedirector />} />
|
<Route path="*" element={<HomeRedirector />} />
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select }
|
|||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RegexAddForm } from '../js/models';
|
import { RegexAddForm } from '../js/models';
|
||||||
import { b64decode, b64encode, getapiobject, okNotify } from '../js/utils';
|
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
|
import { nfregex } from './NFRegex/utils';
|
||||||
|
|
||||||
type RegexAddInfo = {
|
type RegexAddInfo = {
|
||||||
regex:string,
|
regex:string,
|
||||||
@@ -47,7 +48,7 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>
|
|||||||
active: !values.deactive
|
active: !values.deactive
|
||||||
}
|
}
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
getapiobject().regexesadd(request).then( res => {
|
nfregex.regexesadd(request).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
|
|||||||
139
frontend/src/components/NFProxy/AddEditService.tsx
Normal file
139
frontend/src/components/NFProxy/AddEditService.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||||
|
import { ImCross } from "react-icons/im"
|
||||||
|
import { nfproxy, Service } from './utils';
|
||||||
|
import PortAndInterface from '../PortAndInterface';
|
||||||
|
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||||
|
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||||
|
|
||||||
|
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||||
|
|
||||||
|
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
name: "",
|
||||||
|
port:edit?.port??8080,
|
||||||
|
ip_int:edit?.ip_int??"",
|
||||||
|
proto:edit?.proto??"tcp",
|
||||||
|
fail_open: edit?.fail_open??false,
|
||||||
|
autostart: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: initialValues,
|
||||||
|
validate:{
|
||||||
|
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||||
|
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||||
|
proto: (value) => ["tcp","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<string|null>(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 <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
|
{!edit?<TextInput
|
||||||
|
label="Service name"
|
||||||
|
placeholder="Challenge 01"
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>:null}
|
||||||
|
<Space h="md" />
|
||||||
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||||
|
<Space h="md" />
|
||||||
|
|
||||||
|
<Box className='center-flex'>
|
||||||
|
<Box>
|
||||||
|
{!edit?<Switch
|
||||||
|
label="Auto-Start Service"
|
||||||
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
|
/>:null}
|
||||||
|
<Space h="sm" />
|
||||||
|
<Switch
|
||||||
|
label={<Box className='center-flex'>
|
||||||
|
Enable fail-open nfqueue
|
||||||
|
<Space w="xs" />
|
||||||
|
<Tooltip label={<>
|
||||||
|
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||||
|
</>}>
|
||||||
|
<IoMdInformationCircleOutline size={15} />
|
||||||
|
</Tooltip>
|
||||||
|
</Box>}
|
||||||
|
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box className="flex-spacer"></Box>
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'UDP', value: 'udp' },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps('proto')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group justify='flex-end' mt="md" mb="sm">
|
||||||
|
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{error?<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
|
Error: {error}
|
||||||
|
</Notification><Space h="md" />
|
||||||
|
</>:null}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddEditService;
|
||||||
68
frontend/src/components/NFProxy/ServiceRow/RenameForm.tsx
Normal file
68
frontend/src/components/NFProxy/ServiceRow/RenameForm.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Button, Group, Space, TextInput, Notification, Modal } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { okNotify } from '../../../js/utils';
|
||||||
|
import { ImCross } from "react-icons/im"
|
||||||
|
import { nfproxy, Service } from '../utils';
|
||||||
|
|
||||||
|
function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:Service }) {
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: { name:service.name },
|
||||||
|
validate:{ name: (value) => value !== ""? null : "Service name is required" }
|
||||||
|
})
|
||||||
|
|
||||||
|
const close = () =>{
|
||||||
|
onClose()
|
||||||
|
form.reset()
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=> form.setFieldValue("name", service.name),[opened])
|
||||||
|
|
||||||
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
|
const submitRequest = ({ name }:{ name:string }) => {
|
||||||
|
setSubmitLoading(true)
|
||||||
|
nfproxy.servicerename(service.service_id, name).then( res => {
|
||||||
|
if (!res){
|
||||||
|
setSubmitLoading(false)
|
||||||
|
close();
|
||||||
|
okNotify(`Service ${service.name} has been renamed in ${ name }`, `Successfully renamed service on port ${service.port}`)
|
||||||
|
}else{
|
||||||
|
setSubmitLoading(false)
|
||||||
|
setError("Error: [ "+res+" ]")
|
||||||
|
}
|
||||||
|
}).catch( err => {
|
||||||
|
setSubmitLoading(false)
|
||||||
|
setError("Request Failed! [ "+err+" ]")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <Modal size="xl" title={`Rename '${service.name}' service on port ${service.port}`} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
|
<TextInput
|
||||||
|
label="Service Name"
|
||||||
|
placeholder="Awesome Service Name!"
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>
|
||||||
|
<Group mt="md" justify="flex-end" mb="sm">
|
||||||
|
<Button loading={submitLoading} type="submit">Rename</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{error?<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
|
Error: {error}
|
||||||
|
</Notification><Space h="md" />
|
||||||
|
</>:null}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RenameForm;
|
||||||
164
frontend/src/components/NFProxy/ServiceRow/index.tsx
Normal file
164
frontend/src/components/NFProxy/ServiceRow/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||||
|
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||||
|
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||||
|
import YesNoModal from '../../YesNoModal';
|
||||||
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||||
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
|
import { BiRename } from 'react-icons/bi'
|
||||||
|
import RenameForm from './RenameForm';
|
||||||
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { TbPlugConnected } from "react-icons/tb";
|
||||||
|
import { FaFilter } from "react-icons/fa";
|
||||||
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
|
import AddEditService from '../AddEditService';
|
||||||
|
import { FaPencilAlt } from "react-icons/fa";
|
||||||
|
|
||||||
|
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 <>
|
||||||
|
<Box className='firegex__nfregex__rowbox'>
|
||||||
|
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||||
|
<Box>
|
||||||
|
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||||
|
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||||
|
<Title className="firegex__nfregex__name" ml="xs">
|
||||||
|
{service.name}
|
||||||
|
</Title>
|
||||||
|
</Box>
|
||||||
|
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||||
|
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||||
|
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||||
|
:{service.port}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
{isMedium?null:<Space w="xl" />}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||||
|
<Box className="center-flex-row">
|
||||||
|
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||||
|
<Space h="xs" />
|
||||||
|
<Box className='center-flex'>
|
||||||
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||||
|
<Box className="center-flex">
|
||||||
|
<MenuDropDownWithButton>
|
||||||
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
|
<Divider />
|
||||||
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
|
</MenuDropDownWithButton>
|
||||||
|
<Space w="md"/>
|
||||||
|
<Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}>
|
||||||
|
<ActionIcon color="red" loading={buttonLoading}
|
||||||
|
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||||
|
disabled={service.status === "stop"}
|
||||||
|
aria-describedby="tooltip-stop-id"
|
||||||
|
onFocus={() => setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)}
|
||||||
|
onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}>
|
||||||
|
<FaStop size="20px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Space w="md"/>
|
||||||
|
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||||
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||||
|
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||||
|
<FaPlay size="20px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||||
|
{onClick?<Box className='firegex__service_forward_btn'>
|
||||||
|
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||||
|
</Box>:null}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<YesNoModal
|
||||||
|
title='Are you sure to delete this service?'
|
||||||
|
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
|
onClose={()=>setDeleteModal(false) }
|
||||||
|
action={deleteService}
|
||||||
|
opened={deleteModal}
|
||||||
|
/>
|
||||||
|
<RenameForm
|
||||||
|
onClose={()=>setRenameModal(false)}
|
||||||
|
opened={renameModal}
|
||||||
|
service={service}
|
||||||
|
/>
|
||||||
|
<AddEditService
|
||||||
|
opened={editModal}
|
||||||
|
onClose={()=>setEditModal(false)}
|
||||||
|
edit={service}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
99
frontend/src/components/NFProxy/utils.ts
Normal file
99
frontend/src/components/NFProxy/utils.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,6 @@ export type ServiceAddResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const serviceQueryKey = ["nfregex","services"]
|
export const serviceQueryKey = ["nfregex","services"]
|
||||||
export const statsQueryKey = ["nfregex","stats"]
|
|
||||||
|
|
||||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Collapse, Divider, Group, MantineColor, ScrollArea, Text, ThemeIcon, Title, UnstyledButton, Box, AppShell } from "@mantine/core";
|
import { Divider, Group, MantineColor, ScrollArea, Text, ThemeIcon, Title, UnstyledButton, Box, AppShell } from "@mantine/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { IoMdGitNetwork } from "react-icons/io";
|
import { TbPlugConnected } from "react-icons/tb";
|
||||||
import { MdOutlineExpandLess, MdOutlineExpandMore, MdTransform } from "react-icons/md";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { GrDirections } from "react-icons/gr";
|
import { GrDirections } from "react-icons/gr";
|
||||||
import { PiWallLight } from "react-icons/pi";
|
import { PiWallLight } from "react-icons/pi";
|
||||||
import { useNavbarStore } from "../../js/store";
|
import { useNavbarStore } from "../../js/store";
|
||||||
import { getMainPath } from "../../js/utils";
|
import { getMainPath } from "../../js/utils";
|
||||||
|
import { BsRegex } from "react-icons/bs";
|
||||||
|
|
||||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||||
{ navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) {
|
{ navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) {
|
||||||
@@ -36,9 +36,10 @@ export default function NavBar() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Divider my="xs" />
|
<Divider my="xs" />
|
||||||
<Box style={{flexGrow: 1}} component={ScrollArea} px="xs" mt="xs">
|
<Box style={{flexGrow: 1}} component={ScrollArea} px="xs" mt="xs">
|
||||||
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="lime" icon={<IoMdGitNetwork />} />
|
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="grape" icon={<BsRegex size={19} />} />
|
||||||
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight />} />
|
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||||
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections />} />
|
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
||||||
|
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|||||||
@@ -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 React, { useState } from 'react';
|
||||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||||
import { porthijack, Service } from '../utils';
|
import { porthijack, Service } from '../utils';
|
||||||
|
|||||||
50
frontend/src/components/PyFilterView/index.tsx
Normal file
50
frontend/src/components/PyFilterView/index.tsx
Normal file
@@ -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 <Box className="firegex__regexview__box">
|
||||||
|
<Box>
|
||||||
|
<Box className='center-flex' style={{width: "100%"}}>
|
||||||
|
<Box className="firegex__regexview__outer_regex_text">
|
||||||
|
<Text className="firegex__regexview__regex_text">{filterInfo.name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"} opened={statusTooltipOpened}>
|
||||||
|
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
||||||
|
onFocus={() => setStatusTooltipOpened(false)} onBlur={() => setStatusTooltipOpened(false)}
|
||||||
|
onMouseEnter={() => setStatusTooltipOpened(true)} onMouseLeave={() => setStatusTooltipOpened(false)}
|
||||||
|
>{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" mt="sm" ml="xs">
|
||||||
|
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {filterInfo.blocked_packets}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge size="md" color="orange" variant="filled"><FaPencilAlt size={18} /> {filterInfo.edited_packets}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge size="md" color={filterInfo.active?"lime":"red"} variant="filled">{filterInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RegexFilter } from '../../js/models';
|
import { RegexFilter } from '../../js/models';
|
||||||
import { b64decode, errorNotify, getapiobject, isMediumScreen, okNotify } from '../../js/utils';
|
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||||
import { BsTrashFill } from "react-icons/bs"
|
import { BsTrashFill } from "react-icons/bs"
|
||||||
import YesNoModal from '../YesNoModal';
|
import YesNoModal from '../YesNoModal';
|
||||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||||
import { useClipboard } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { FaFilter } from "react-icons/fa";
|
import { FaFilter } from "react-icons/fa";
|
||||||
import { VscRegex } from "react-icons/vsc";
|
|
||||||
|
import { nfregex } from '../NFRegex/utils';
|
||||||
|
|
||||||
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
|||||||
const isMedium = isMediumScreen();
|
const isMedium = isMediumScreen();
|
||||||
|
|
||||||
const deleteRegex = () => {
|
const deleteRegex = () => {
|
||||||
getapiobject().regexdelete(regexInfo.id).then(res => {
|
nfregex.regexdelete(regexInfo.id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
||||||
}else{
|
}else{
|
||||||
@@ -34,9 +35,9 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const changeRegexStatus = () => {
|
const changeRegexStatus = () => {
|
||||||
(regexInfo.active?getapiobject().regexdisable:getapiobject().regexenable)(regexInfo.id).then(res => {
|
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been ${regexInfo.active?"deactivated":"activated"}!`)
|
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { queryClient } from './js/utils';
|
import { queryClient } from './js/utils';
|
||||||
import '@mantine/core/styles.css';
|
import '@mantine/core/styles.css';
|
||||||
import '@mantine/notifications/styles.css';
|
import '@mantine/notifications/styles.css';
|
||||||
|
import '@mantine/code-highlight/styles.css';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
|
|||||||
@@ -49,3 +49,11 @@ export type RegexAddForm = {
|
|||||||
mode:string, // C->S S->C BOTH,
|
mode:string, // C->S S->C BOTH,
|
||||||
active: boolean
|
active: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PyFilter = {
|
||||||
|
filter_id:number,
|
||||||
|
name:string,
|
||||||
|
blocked_packets:number,
|
||||||
|
edited_packets:number,
|
||||||
|
active:boolean
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerRespons
|
|||||||
import { Buffer } from "buffer"
|
import { Buffer } from "buffer"
|
||||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
|
import { nfproxy } from "../components/NFProxy/utils";
|
||||||
|
|
||||||
export const IS_DEV = import.meta.env.DEV
|
export const IS_DEV = import.meta.env.DEV
|
||||||
|
|
||||||
@@ -101,14 +102,6 @@ export function getMainPath(){
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getapiobject(){
|
|
||||||
switch(getMainPath()){
|
|
||||||
case "nfregex":
|
|
||||||
return nfregex
|
|
||||||
}
|
|
||||||
throw new Error('No api for this tool!');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HomeRedirector(){
|
export function HomeRedirector(){
|
||||||
const section = sessionStorage.getItem("home_section")
|
const section = sessionStorage.getItem("home_section")
|
||||||
const path = section?`/${section}`:`/nfregex`
|
const path = section?`/${section}`:`/nfregex`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, FloatingIndicator, LoadingOverlay, Space, Switch, Table, Tabs, TextInput, Title, Tooltip, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Badge, Box, Divider, FloatingIndicator, LoadingOverlay, Space, Switch, Table, Tabs, TextInput, ThemeIcon, Title, Tooltip, useMantineTheme } from "@mantine/core"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { BsPlusLg, BsTrashFill } from "react-icons/bs"
|
import { BsPlusLg, BsTrashFill } from "react-icons/bs"
|
||||||
import { rem } from '@mantine/core';
|
import { rem } from '@mantine/core';
|
||||||
@@ -20,7 +20,8 @@ import { LuArrowBigRightDash } from "react-icons/lu"
|
|||||||
import { ImCheckmark, ImCross } from "react-icons/im";
|
import { ImCheckmark, ImCross } from "react-icons/im";
|
||||||
import { IoSettingsSharp } from "react-icons/io5";
|
import { IoSettingsSharp } from "react-icons/io5";
|
||||||
import { SettingsModal } from "./SettingsModal";
|
import { SettingsModal } from "./SettingsModal";
|
||||||
|
import { FaDirections } from "react-icons/fa";
|
||||||
|
import { PiWallLight } from "react-icons/pi";
|
||||||
|
|
||||||
export const Firewall = () => {
|
export const Firewall = () => {
|
||||||
|
|
||||||
@@ -346,7 +347,7 @@ export const Firewall = () => {
|
|||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<LoadingOverlay visible={rules.isLoading} />
|
<LoadingOverlay visible={rules.isLoading} />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
<Title order={3}>Firewall Rules</Title>
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='red' ><PiWallLight size={20} /></ThemeIcon><Space w="xs" />Firewall Rules</Title>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
Enabled: <Space w="sm" /> <Switch checked={fwEnabled} onChange={switchState} />
|
Enabled: <Space w="sm" /> <Switch checked={fwEnabled} onChange={switchState} />
|
||||||
@@ -361,8 +362,8 @@ export const Firewall = () => {
|
|||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="sm" color="green" variant="filled">Rules: {rules.isLoading?0:rules.data?.rules.length}</Badge>
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaDirections style={{ marginBottom: -1, marginRight: 4}}/>Rules: {rules.isLoading?0:rules.data?.rules.length}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="md" />
|
||||||
<Tooltip label="Add a new rule" position='bottom' color="blue" opened={tooltipAddOpened}>
|
<Tooltip label="Add a new rule" position='bottom' color="blue" opened={tooltipAddOpened}>
|
||||||
<ActionIcon color="blue" onClick={emptyRuleAdd} size="lg" radius="md" variant="filled"
|
<ActionIcon color="blue" onClick={emptyRuleAdd} size="lg" radius="md" variant="filled"
|
||||||
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
|
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
|
||||||
|
|||||||
200
frontend/src/pages/NFProxy/ServiceDetails.tsx
Normal file
200
frontend/src/pages/NFProxy/ServiceDetails.tsx
Normal file
@@ -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 <LoadingOverlay visible={true} />
|
||||||
|
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||||
|
|
||||||
|
let status_color = "gray";
|
||||||
|
switch(serviceInfo.status){
|
||||||
|
case "stop": status_color = "red"; break;
|
||||||
|
case "active": status_color = "teal"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startService = async () => {
|
||||||
|
setButtonLoading(true)
|
||||||
|
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||||
|
if(!res){
|
||||||
|
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||||
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
|
}else{
|
||||||
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||||
|
})
|
||||||
|
setButtonLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteService = () => {
|
||||||
|
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||||
|
if (!res){
|
||||||
|
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||||
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
|
}else
|
||||||
|
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||||
|
}).catch(err => {
|
||||||
|
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopService = async () => {
|
||||||
|
setButtonLoading(true)
|
||||||
|
|
||||||
|
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||||
|
if(!res){
|
||||||
|
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||||
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
|
}else{
|
||||||
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||||
|
})
|
||||||
|
setButtonLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<LoadingOverlay visible={filtersList.isLoading} />
|
||||||
|
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||||
|
<Box>
|
||||||
|
<Title order={1}>
|
||||||
|
<Box className="center-flex">
|
||||||
|
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||||
|
</Box>
|
||||||
|
</Title>
|
||||||
|
</Box>
|
||||||
|
{isMedium?null:<Space h="md" />}
|
||||||
|
<Box className='center-flex'>
|
||||||
|
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||||
|
{serviceInfo.status}
|
||||||
|
</Badge>
|
||||||
|
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||||
|
:{serviceInfo.port}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<MenuDropDownWithButton>
|
||||||
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
|
<Divider />
|
||||||
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
|
</MenuDropDownWithButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{isMedium?null:<Space h="md" />}
|
||||||
|
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||||
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
|
<Box className='center-flex'>
|
||||||
|
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||||
|
</Box>
|
||||||
|
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||||
|
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||||
|
</Box>
|
||||||
|
{isMedium?null:<Space h="xl" />}
|
||||||
|
<Box className='center-flex'>
|
||||||
|
<Tooltip label="Go back" zIndex={0} color="cyan" opened={tooltipBackOpened}>
|
||||||
|
<ActionIcon color="cyan"
|
||||||
|
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||||
|
aria-describedby="tooltip-back-id"
|
||||||
|
onFocus={() => setTooltipBackOpened(false)} onBlur={() => setTooltipBackOpened(false)}
|
||||||
|
onMouseEnter={() => setTooltipBackOpened(true)} onMouseLeave={() => setTooltipBackOpened(false)}>
|
||||||
|
<FaArrowLeft size="25px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Space w="md"/>
|
||||||
|
<Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}>
|
||||||
|
<ActionIcon color="red" loading={buttonLoading}
|
||||||
|
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||||
|
disabled={serviceInfo.status === "stop"}
|
||||||
|
aria-describedby="tooltip-stop-id"
|
||||||
|
onFocus={() => setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)}
|
||||||
|
onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}>
|
||||||
|
<FaStop size="20px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Space w="md"/>
|
||||||
|
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||||
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||||
|
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||||
|
<FaPlay size="20px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Divider my="xl" />
|
||||||
|
{filterCode.data?<>
|
||||||
|
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||||
|
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||||
|
</>: null}
|
||||||
|
<Space h="xl" />
|
||||||
|
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||||
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Edit the proxy file</Title>
|
||||||
|
<Space h="xs" />
|
||||||
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Install the firegex client:<Space w="xs" /><Code mb={-4} >pip install fgex</Code></Title>
|
||||||
|
<Space h="xs" />
|
||||||
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then run the command:<Space w="xs" /><Code mb={-4} >fgex nfproxy</Code></Title>
|
||||||
|
</>:
|
||||||
|
<Grid>
|
||||||
|
{filtersList.data?.map( (filterInfo) => <Grid.Col key={filterInfo.filter_id} span={{ lg:6, xs: 12 }}><PyFilterView filterInfo={filterInfo} /></Grid.Col>)}
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
<YesNoModal
|
||||||
|
title='Are you sure to delete this service?'
|
||||||
|
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
|
onClose={()=>setDeleteModal(false) }
|
||||||
|
action={deleteService}
|
||||||
|
opened={deleteModal}
|
||||||
|
/>
|
||||||
|
<RenameForm
|
||||||
|
onClose={()=>setRenameModal(false)}
|
||||||
|
opened={renameModal}
|
||||||
|
service={serviceInfo}
|
||||||
|
/>
|
||||||
|
<AddEditService
|
||||||
|
opened={editModal}
|
||||||
|
onClose={()=>setEditModal(false)}
|
||||||
|
edit={serviceInfo}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
91
frontend/src/pages/NFProxy/index.tsx
Normal file
91
frontend/src/pages/NFProxy/index.tsx
Normal file
@@ -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 <>
|
||||||
|
<Space h="sm" />
|
||||||
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||||
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
|
<Box className='center-flex' >
|
||||||
|
General stats:
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
</Box>
|
||||||
|
{isMedium?null:<Space h="md" />}
|
||||||
|
<Box className='center-flex' >
|
||||||
|
{/* Will become the null a button to edit the source code? TODO */}
|
||||||
|
{ srv?null
|
||||||
|
: <Tooltip label="Add a new service" position='bottom' color="blue" opened={tooltipAddOpened}>
|
||||||
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"
|
||||||
|
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
|
||||||
|
onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}><BsPlusLg size={18} /></ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
<Space w="xs" />
|
||||||
|
<Tooltip label="Refresh" position='bottom' color="indigo" opened={tooltipRefreshOpened}>
|
||||||
|
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled"
|
||||||
|
loading={services.isFetching}
|
||||||
|
onFocus={() => setTooltipRefreshOpened(false)} onBlur={() => setTooltipRefreshOpened(false)}
|
||||||
|
onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}><TbReload size={18} /></ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Space h="md" />
|
||||||
|
<Box className="center-flex-row" style={{gap: 20}}>
|
||||||
|
{srv?null:<>
|
||||||
|
<LoadingOverlay visible={services.isLoading} />
|
||||||
|
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||||
|
navigator("/nfproxy/"+srv.service_id)
|
||||||
|
}} />):<><Space h="xl"/> <Title className='center-flex' style={{textAlign:"center"}} order={3}>No services found! Add one clicking the "+" buttons</Title>
|
||||||
|
<Box className='center-flex'>
|
||||||
|
<Tooltip label="Add a new service" color="blue" opened={tooltipAddServOpened}>
|
||||||
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||||
|
onFocus={() => setTooltipAddServOpened(false)} onBlur={() => setTooltipAddServOpened(false)}
|
||||||
|
onMouseEnter={() => setTooltipAddServOpened(true)} onMouseLeave={() => setTooltipAddServOpened(false)}><BsPlusLg size="20px" /></ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</>}
|
||||||
|
</>}
|
||||||
|
</Box>
|
||||||
|
{srv?children:null}
|
||||||
|
{!srv?<AddEditService opened={open} onClose={closeModal} />:null}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BsPlusLg } from "react-icons/bs";
|
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||||
@@ -9,7 +9,9 @@ import AddEditService from '../../components/NFRegex/AddEditService';
|
|||||||
import AddNewRegex from '../../components/AddNewRegex';
|
import AddNewRegex from '../../components/AddNewRegex';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbReload } from 'react-icons/tb';
|
import { TbReload } from 'react-icons/tb';
|
||||||
|
import { FaFilter } from 'react-icons/fa';
|
||||||
|
import { FaServer } from "react-icons/fa6";
|
||||||
|
import { VscRegex } from "react-icons/vsc";
|
||||||
|
|
||||||
function NFRegex({ children }: { children: any }) {
|
function NFRegex({ children }: { children: any }) {
|
||||||
|
|
||||||
@@ -33,14 +35,16 @@ function NFRegex({ children }: { children: any }) {
|
|||||||
return <>
|
return <>
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
<Title order={4}>Netfilter Regex</Title>
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
<Badge size="sm" color="green" variant="filled">Services: {services.isLoading?0:services.data?.length}</Badge>
|
General stats:
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="sm" color="yellow" variant="filled">Filtered Connections: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="sm" color="violet" variant="filled">Regexes: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BsPlusLg } from "react-icons/bs";
|
import { BsPlusLg } from "react-icons/bs";
|
||||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||||
@@ -7,6 +7,8 @@ import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
|||||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbReload } from 'react-icons/tb';
|
import { TbReload } from 'react-icons/tb';
|
||||||
|
import { FaServer } from 'react-icons/fa';
|
||||||
|
import { GrDirections } from 'react-icons/gr';
|
||||||
|
|
||||||
|
|
||||||
function PortHijack() {
|
function PortHijack() {
|
||||||
@@ -30,10 +32,10 @@ function PortHijack() {
|
|||||||
return <>
|
return <>
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
<Title order={4}>Hijack port to proxy</Title>
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Badge size="sm" color="yellow" variant="filled">Services: {services.isLoading?0:services.data?.length}</Badge>
|
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Add a new service" position='bottom' color="blue" opened={tooltipAddOpened}>
|
<Tooltip label="Add a new service" position='bottom' color="blue" opened={tooltipAddOpened}>
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user