From 2d8f19679fba03b2453e760dd0ad5071f84ad77d Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Sun, 2 Feb 2025 19:54:42 +0100 Subject: [PATCH] nfqueue to hyperscan and stream match, removed proxyregex --- Dockerfile | 15 +- backend/app.py | 22 +- backend/binsrc/classes/netfilter.cpp | 485 +++++++++++++++++ backend/binsrc/classes/netfilter.hpp | 294 ----------- backend/binsrc/classes/regex_filter.hpp | 95 ---- backend/binsrc/classes/regex_rules.cpp | 161 ++++++ backend/binsrc/cppqueue | Bin 0 -> 282672 bytes backend/binsrc/nfqueue.cpp | 122 ++++- backend/binsrc/nfqueue_regex/Cargo.lock | 32 -- backend/binsrc/nfqueue_regex/Cargo.toml | 11 - backend/binsrc/nfqueue_regex/src/main.rs | 150 ------ .../binsrc/nfqueue_regex/src/regex_rules.rs | 1 - backend/binsrc/proxy.cpp | 493 ------------------ backend/binsrc/utils.hpp | 2 +- backend/modules/firewall/firewall.py | 4 +- backend/modules/nfregex/firegex.py | 30 +- backend/modules/nfregex/firewall.py | 10 +- backend/modules/nfregex/nftables.py | 47 +- backend/modules/porthijack/firewall.py | 6 +- backend/modules/porthijack/nftables.py | 3 +- backend/modules/regexproxy/proxy.py | 116 ----- backend/modules/regexproxy/utils.py | 199 ------- backend/routers/firewall.py | 2 +- backend/routers/nfregex.py | 12 +- backend/routers/porthijack.py | 6 +- backend/routers/regexproxy.py | 311 ----------- backend/utils/__init__.py | 22 +- backend/utils/loader.py | 15 +- backend/utils/sqlite.py | 31 +- frontend/bun.lockb | Bin 117435 -> 115834 bytes frontend/package.json | 28 +- frontend/src/App.tsx | 5 - frontend/src/components/AddNewRegex.tsx | 20 +- .../src/components/FilterTypeSelector.tsx | 29 -- frontend/src/components/NavBar/index.tsx | 5 - frontend/src/components/PortHijack/utils.ts | 2 +- .../components/RegexProxy/AddNewService.tsx | 117 ----- .../RegexProxy/ServiceRow/ChangePortModal.tsx | 101 ---- .../RegexProxy/ServiceRow/RenameForm.tsx | 68 --- .../RegexProxy/ServiceRow/index.tsx | 205 -------- frontend/src/components/RegexProxy/utils.ts | 97 ---- frontend/src/components/RegexView/index.tsx | 7 - frontend/src/js/models.ts | 1 - frontend/src/js/utils.tsx | 3 - .../src/pages/RegexProxy/ServiceDetails.tsx | 48 -- frontend/src/pages/RegexProxy/index.tsx | 91 ---- tests/api_test.py | 89 +++- tests/benchmark.py | 66 ++- tests/nf_test.py | 127 +++-- tests/ph_test.py | 89 +++- tests/px_test.py | 249 --------- tests/run_tests.sh | 2 - .../regexproxy => tests/utils}/__init__.py | 0 tests/utils/firegexapi.py | 80 +-- 54 files changed, 1134 insertions(+), 3092 deletions(-) create mode 100644 backend/binsrc/classes/netfilter.cpp delete mode 100644 backend/binsrc/classes/netfilter.hpp delete mode 100644 backend/binsrc/classes/regex_filter.hpp create mode 100644 backend/binsrc/classes/regex_rules.cpp create mode 100755 backend/binsrc/cppqueue delete mode 100644 backend/binsrc/nfqueue_regex/Cargo.lock delete mode 100644 backend/binsrc/nfqueue_regex/Cargo.toml delete mode 100644 backend/binsrc/nfqueue_regex/src/main.rs delete mode 100644 backend/binsrc/nfqueue_regex/src/regex_rules.rs delete mode 100644 backend/binsrc/proxy.cpp delete mode 100644 backend/modules/regexproxy/proxy.py delete mode 100644 backend/modules/regexproxy/utils.py delete mode 100644 backend/routers/regexproxy.py delete mode 100644 frontend/src/components/FilterTypeSelector.tsx delete mode 100644 frontend/src/components/RegexProxy/AddNewService.tsx delete mode 100644 frontend/src/components/RegexProxy/ServiceRow/ChangePortModal.tsx delete mode 100644 frontend/src/components/RegexProxy/ServiceRow/RenameForm.tsx delete mode 100644 frontend/src/components/RegexProxy/ServiceRow/index.tsx delete mode 100644 frontend/src/components/RegexProxy/utils.ts delete mode 100644 frontend/src/pages/RegexProxy/ServiceDetails.tsx delete mode 100644 frontend/src/pages/RegexProxy/index.tsx delete mode 100644 tests/px_test.py rename {backend/modules/regexproxy => tests/utils}/__init__.py (100%) diff --git a/Dockerfile b/Dockerfile index c5b567f..1922bea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,15 +17,9 @@ RUN bun run build FROM --platform=$TARGETARCH debian:stable-slim AS base RUN apt-get update -qq && apt-get upgrade -qq && \ apt-get install -qq python3-pip build-essential \ - git libpcre2-dev libnetfilter-queue-dev libssl-dev \ - libnfnetlink-dev libmnl-dev libcap2-bin make cmake \ - nftables libboost-all-dev autoconf automake cargo \ - libffi-dev libvectorscan-dev libtins-dev python3-nftables - -WORKDIR /tmp/ -RUN git clone --single-branch --branch release https://github.com/jpcre2/jpcre2 -WORKDIR /tmp/jpcre2 -RUN ./configure; make -j`nproc`; make install + git libnetfilter-queue-dev libssl-dev \ + libnfnetlink-dev libmnl-dev libcap2-bin \ + nftables libffi-dev libvectorscan-dev libtins-dev python3-nftables RUN mkdir -p /execute/modules WORKDIR /execute @@ -34,8 +28,7 @@ ADD ./backend/requirements.txt /execute/requirements.txt RUN pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt --no-warn-script-location COPY ./backend/binsrc /execute/binsrc -RUN g++ binsrc/nfqueue.cpp -o modules/cppqueue -O3 -lnetfilter_queue -pthread -lpcre2-8 -ltins -lmnl -lnfnetlink -RUN g++ binsrc/proxy.cpp -o modules/proxy -O3 -pthread -lboost_system -lboost_thread -lpcre2-8 +RUN g++ binsrc/nfqueue.cpp -o modules/cppqueue -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) COPY ./backend/ /execute/ COPY --from=frontend /app/dist/ ./frontend/ diff --git a/backend/app.py b/backend/app.py index d4379ef..1ac8138 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,6 +1,10 @@ -import uvicorn, secrets, utils -import os, asyncio, logging -from fastapi import FastAPI, HTTPException, Depends, APIRouter, Request +import uvicorn +import secrets +import utils +import os +import asyncio +import logging +from fastapi import FastAPI, HTTPException, Depends, APIRouter from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import jwt from passlib.context import CryptContext @@ -94,7 +98,8 @@ async def get_app_status(auth: bool = Depends(check_login)): @app.post("/api/login") async def login_api(form: OAuth2PasswordRequestForm = Depends()): """Get a login token to use the firegex api""" - if APP_STATUS() != "run": raise HTTPException(status_code=400) + if APP_STATUS() != "run": + raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} await asyncio.sleep(0.3) # No bruteforce :) @@ -105,7 +110,8 @@ async def login_api(form: OAuth2PasswordRequestForm = Depends()): @app.post('/api/set-password', response_model=ChangePasswordModel) async def set_password(form: PasswordForm): """Set the password of firegex""" - if APP_STATUS() != "init": raise HTTPException(status_code=400) + if APP_STATUS() != "init": + raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} set_psw(form.password) @@ -115,7 +121,8 @@ async def set_password(form: PasswordForm): @api.post('/change-password', response_model=ChangePasswordModel) async def change_password(form: PasswordChangeForm): """Change the password of firegex""" - if APP_STATUS() != "run": raise HTTPException(status_code=400) + if APP_STATUS() != "run": + raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} @@ -144,7 +151,8 @@ async def startup_main(): except Exception as e: logging.error(f"Error setting sysctls: {e}") await startup() - if not JWT_SECRET(): db.put("secret", secrets.token_hex(32)) + if not JWT_SECRET(): + db.put("secret", secrets.token_hex(32)) await refresh_frontend() async def shutdown_main(): diff --git a/backend/binsrc/classes/netfilter.cpp b/backend/binsrc/classes/netfilter.cpp new file mode 100644 index 0000000..39dedf3 --- /dev/null +++ b/backend/binsrc/classes/netfilter.cpp @@ -0,0 +1,485 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Tins::TCPIP::Stream; +using Tins::TCPIP::StreamFollower; +using namespace std; + + +#ifndef NETFILTER_CLASSES_HPP +#define NETFILTER_CLASSES_HPP + +string inline client_endpoint(const Stream& stream) { + ostringstream output; + // Use the IPv4 or IPv6 address depending on which protocol the + // connection uses + if (stream.is_v6()) { + output << stream.client_addr_v6(); + } + else { + output << stream.client_addr_v4(); + } + output << ":" << stream.client_port(); + return output.str(); +} + +// Convert the server endpoint to a readable string +string inline server_endpoint(const Stream& stream) { + ostringstream output; + if (stream.is_v6()) { + output << stream.server_addr_v6(); + } + else { + output << stream.server_addr_v4(); + } + output << ":" << stream.server_port(); + return output.str(); +} + +// Concat both endpoints to get a readable stream identifier +string inline stream_identifier(const Stream& stream) { + ostringstream output; + output << client_endpoint(stream) << " - " << server_endpoint(stream); + return output.str(); +} + +typedef unordered_map matching_map; + +struct packet_info; + +struct tcp_stream_tmp { + bool matching_has_been_called = false; + bool result; + packet_info *pkt_info; +}; + +struct stream_ctx { + matching_map in_hs_streams; + matching_map out_hs_streams; + hs_scratch_t* in_scratch = nullptr; + hs_scratch_t* out_scratch = nullptr; + u_int16_t latest_config_ver = 0; + StreamFollower follower; + mnl_socket* nl; + tcp_stream_tmp tcp_match_util; + + void clean_scratches(){ + if (out_scratch != nullptr){ + hs_free_scratch(out_scratch); + out_scratch = nullptr; + } + if (in_scratch != nullptr){ + hs_free_scratch(in_scratch); + in_scratch = nullptr; + } + } +}; + +struct packet_info { + string packet; + string payload; + string stream_id; + bool is_input; + bool is_tcp; + stream_ctx* sctx; +}; + +typedef bool NetFilterQueueCallback(packet_info &); + + +Tins::PDU * find_transport_layer(Tins::PDU* pkt){ + while(pkt != nullptr){ + if (pkt->pdu_type() == Tins::PDU::TCP || pkt->pdu_type() == Tins::PDU::UDP) { + return pkt; + } + pkt = pkt->inner_pdu(); + } + return nullptr; +} + +template +class NetfilterQueue { + public: + + size_t BUF_SIZE = 0xffff + (MNL_SOCKET_BUFFER_SIZE/2); + char *buf = nullptr; + unsigned int portid; + u_int16_t queue_num; + stream_ctx sctx; + + NetfilterQueue(u_int16_t queue_num): queue_num(queue_num) { + sctx.nl = mnl_socket_open(NETLINK_NETFILTER); + + if (sctx.nl == nullptr) { throw runtime_error( "mnl_socket_open" );} + + if (mnl_socket_bind(sctx.nl, 0, MNL_SOCKET_AUTOPID) < 0) { + mnl_socket_close(sctx.nl); + throw runtime_error( "mnl_socket_bind" ); + } + portid = mnl_socket_get_portid(sctx.nl); + + buf = (char*) malloc(BUF_SIZE); + + if (!buf) { + mnl_socket_close(sctx.nl); + throw runtime_error( "allocate receive buffer" ); + } + + if (send_config_cmd(NFQNL_CFG_CMD_BIND) < 0) { + _clear(); + throw runtime_error( "mnl_socket_send" ); + } + //TEST if BIND was successful + if (send_config_cmd(NFQNL_CFG_CMD_NONE) < 0) { // SEND A NONE cmmand to generate an error meessage + _clear(); + throw runtime_error( "mnl_socket_send" ); + } + if (recv_packet() == -1) { //RECV the error message + _clear(); + throw runtime_error( "mnl_socket_recvfrom" ); + } + + struct nlmsghdr *nlh = (struct nlmsghdr *) buf; + + if (nlh->nlmsg_type != NLMSG_ERROR) { + _clear(); + throw runtime_error( "unexpected packet from kernel (expected NLMSG_ERROR packet)" ); + } + //nfqnl_msg_config_cmd + nlmsgerr* error_msg = (nlmsgerr *)mnl_nlmsg_get_payload(nlh); + + // error code taken from the linux kernel: + // https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27 + #define ENOTSUPP 524 /* Operation is not supported */ + + if (error_msg->error != -ENOTSUPP) { + _clear(); + throw invalid_argument( "queueid is already busy" ); + } + + //END TESTING BIND + nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); + nfq_nlmsg_cfg_put_params(nlh, NFQNL_COPY_PACKET, 0xffff); + + mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO)); + mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO)); + + if (mnl_socket_sendto(sctx.nl, nlh, nlh->nlmsg_len) < 0) { + _clear(); + throw runtime_error( "mnl_socket_send" ); + } + + } + + //Input data filtering + void on_client_data(Stream& stream) { + string data(stream.client_payload().begin(), stream.client_payload().end()); + string stream_id = stream_identifier(stream); + this->sctx.tcp_match_util.pkt_info->is_input = true; + this->sctx.tcp_match_util.pkt_info->stream_id = stream_id; + this->sctx.tcp_match_util.matching_has_been_called = true; + bool result = callback_func(*sctx.tcp_match_util.pkt_info); + if (result){ + this->clean_stream_by_id(stream_id); + stream.ignore_client_data(); + stream.ignore_server_data(); + } + this->sctx.tcp_match_util.result = result; + } + + //Server data filtering + void on_server_data(Stream& stream) { + string data(stream.server_payload().begin(), stream.server_payload().end()); + string stream_id = stream_identifier(stream); + this->sctx.tcp_match_util.pkt_info->is_input = false; + this->sctx.tcp_match_util.pkt_info->stream_id = stream_id; + this->sctx.tcp_match_util.matching_has_been_called = true; + bool result = callback_func(*sctx.tcp_match_util.pkt_info); + if (result){ + this->clean_stream_by_id(stream_id); + stream.ignore_client_data(); + stream.ignore_server_data(); + } + this->sctx.tcp_match_util.result = result; + } + + void on_new_stream(Stream& stream) { + string stream_id = stream_identifier(stream); + if (stream.is_partial_stream()) { + return; + } + cout << "[+] New connection " << stream_id << endl; + stream.auto_cleanup_payloads(true); + stream.client_data_callback( + [&](auto a){this->on_client_data(a);} + ); + stream.server_data_callback( + [&](auto a){this->on_server_data(a);} + ); + } + + void clean_stream_by_id(string stream_id){ + auto stream_search = this->sctx.in_hs_streams.find(stream_id); + hs_stream_t* stream_match; + if (stream_search != this->sctx.in_hs_streams.end()){ + stream_match = stream_search->second; + if (hs_close_stream(stream_match, sctx.in_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + this->sctx.in_hs_streams.erase(stream_search); + } + + stream_search = this->sctx.out_hs_streams.find(stream_id); + if (stream_search != this->sctx.out_hs_streams.end()){ + stream_match = stream_search->second; + if (hs_close_stream(stream_match, sctx.out_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + this->sctx.out_hs_streams.erase(stream_search); + } + } + + // A stream was terminated. The second argument is the reason why it was terminated + void on_stream_terminated(Stream& stream, StreamFollower::TerminationReason reason) { + string stream_id = stream_identifier(stream); + cout << "[+] Connection closed: " << stream_id << endl; + this->clean_stream_by_id(stream_id); + } + + + void run(){ + /* + * ENOBUFS is signalled to userspace when packets were lost + * on kernel side. In most cases, userspace isn't interested + * in this information, so turn it off. + */ + int ret = 1; + mnl_socket_setsockopt(sctx.nl, NETLINK_NO_ENOBUFS, &ret, sizeof(int)); + sctx.follower.new_stream_callback( + [&](auto a){this->on_new_stream(a);} + ); + sctx.follower.stream_termination_callback( + [&](auto a, auto b){this->on_stream_terminated(a, b);} + ); + for (;;) { + ret = recv_packet(); + if (ret == -1) { + throw runtime_error( "mnl_socket_recvfrom" ); + } + + ret = mnl_cb_run(buf, ret, 0, portid, queue_cb, &sctx); + if (ret < 0){ + throw runtime_error( "mnl_cb_run" ); + } + } + } + + + ~NetfilterQueue() { + send_config_cmd(NFQNL_CFG_CMD_UNBIND); + _clear(); + } + private: + + ssize_t send_config_cmd(nfqnl_msg_config_cmds cmd){ + struct nlmsghdr *nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); + nfq_nlmsg_cfg_put_cmd(nlh, AF_INET, cmd); + return mnl_socket_sendto(sctx.nl, nlh, nlh->nlmsg_len); + } + + ssize_t recv_packet(){ + return mnl_socket_recvfrom(sctx.nl, buf, BUF_SIZE); + } + + void _clear(){ + if (buf != nullptr) { + free(buf); + buf = nullptr; + } + mnl_socket_close(sctx.nl); + sctx.nl = nullptr; + sctx.clean_scratches(); + + for(auto ele: sctx.in_hs_streams){ + if (hs_close_stream(ele.second, sctx.in_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + } + sctx.in_hs_streams.clear(); + for(auto ele: sctx.out_hs_streams){ + if (hs_close_stream(ele.second, sctx.out_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + } + sctx.out_hs_streams.clear(); + } + + static int queue_cb(const nlmsghdr *nlh, void *data_ptr) + { + stream_ctx* sctx = (stream_ctx*)data_ptr; + + //Extract attributes from the nlmsghdr + nlattr *attr[NFQA_MAX+1] = {}; + + if (nfq_nlmsg_parse(nlh, attr) < 0) { + perror("problems parsing"); + return MNL_CB_ERROR; + } + if (attr[NFQA_PACKET_HDR] == nullptr) { + fputs("metaheader not set\n", stderr); + return MNL_CB_ERROR; + } + //Get Payload + uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]); + uint8_t *payload = (uint8_t *)mnl_attr_get_payload(attr[NFQA_PAYLOAD]); + + //Return result to the kernel + struct nfqnl_msg_packet_hdr *ph = (nfqnl_msg_packet_hdr*) mnl_attr_get_payload(attr[NFQA_PACKET_HDR]); + struct nfgenmsg *nfg = (nfgenmsg *)mnl_nlmsg_get_payload(nlh); + char buf[MNL_SOCKET_BUFFER_SIZE]; + struct nlmsghdr *nlh_verdict; + struct nlattr *nest; + + nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(nfg->res_id)); + + // Check IP protocol version + Tins::PDU *packet; + if ( ((payload)[0] & 0xf0) == 0x40 ){ + Tins::IP parsed = Tins::IP(payload, plen); + packet = &parsed; + }else{ + Tins::IPv6 parsed = Tins::IPv6(payload, plen); + packet = &parsed; + } + Tins::PDU *transport_layer = find_transport_layer(packet); + if(transport_layer == nullptr || transport_layer->inner_pdu() == nullptr){ + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); + }else{ + bool is_tcp = transport_layer->pdu_type() == Tins::PDU::TCP; + int size = transport_layer->inner_pdu()->size(); + packet_info pktinfo{ + packet: string(payload, payload+plen), + payload: string(payload+plen - size, payload+plen), + stream_id: "", // TODO We need to calculate this + is_input: true, // TODO We need to detect this + is_tcp: is_tcp, + sctx: sctx, + }; + if (is_tcp){ + sctx->tcp_match_util.matching_has_been_called = false; + sctx->tcp_match_util.pkt_info = &pktinfo; + sctx->follower.process_packet(*packet); + if (sctx->tcp_match_util.matching_has_been_called && !sctx->tcp_match_util.result){ + auto tcp_layer = (Tins::TCP *)transport_layer; + tcp_layer->release_inner_pdu(); + tcp_layer->set_flag(Tins::TCP::FIN,1); + tcp_layer->set_flag(Tins::TCP::ACK,1); + tcp_layer->set_flag(Tins::TCP::SYN,0); + nfq_nlmsg_verdict_put_pkt(nlh_verdict, packet->serialize().data(), packet->size()); + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); + delete tcp_layer; + } + }else if(callback_func(pktinfo)){ + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); + } else{ + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_DROP ); + } + } + + /* example to set the connmark. First, start NFQA_CT section: */ + nest = mnl_attr_nest_start(nlh_verdict, NFQA_CT); + + /* then, add the connmark attribute: */ + mnl_attr_put_u32(nlh_verdict, CTA_MARK, htonl(42)); + /* more conntrack attributes, e.g. CTA_LABELS could be set here */ + + /* end conntrack section */ + mnl_attr_nest_end(nlh_verdict, nest); + + if (mnl_socket_sendto(sctx->nl, nlh_verdict, nlh_verdict->nlmsg_len) < 0) { + throw runtime_error( "mnl_socket_send" ); + } + + return MNL_CB_OK; + } + +}; + +template +class NFQueueSequence{ + private: + vector *> nfq; + uint16_t _init; + uint16_t _end; + vector threads; + public: + static const int QUEUE_BASE_NUM = 1000; + + NFQueueSequence(uint16_t seq_len){ + if (seq_len <= 0) throw invalid_argument("seq_len <= 0"); + nfq = vector*>(seq_len); + _init = QUEUE_BASE_NUM; + while(nfq[0] == nullptr){ + if (_init+seq_len-1 >= 65536){ + throw runtime_error("NFQueueSequence: too many queues!"); + } + for (int i=0;i(_init+i); + }catch(const invalid_argument e){ + for(int j = 0; j < i; j++) { + delete nfq[j]; + nfq[j] = nullptr; + } + _init += seq_len - i; + break; + } + } + } + _end = _init + seq_len - 1; + } + + void start(){ + if (threads.size() != 0) throw runtime_error("NFQueueSequence: already started!"); + for (int i=0;i::run, nfq[i])); + } + } + + void join(){ + for (int i=0;i -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef NETFILTER_CLASSES_HPP -#define NETFILTER_CLASSES_HPP - -typedef bool NetFilterQueueCallback(const uint8_t*,uint32_t); - -Tins::PDU * find_transport_layer(Tins::PDU* pkt){ - while(pkt != NULL){ - if (pkt->pdu_type() == Tins::PDU::TCP || pkt->pdu_type() == Tins::PDU::UDP) { - return pkt; - } - pkt = pkt->inner_pdu(); - } - return pkt; -} - -template -class NetfilterQueue { - public: - size_t BUF_SIZE = 0xffff + (MNL_SOCKET_BUFFER_SIZE/2); - char *buf = NULL; - unsigned int portid; - u_int16_t queue_num; - struct mnl_socket* nl = NULL; - - NetfilterQueue(u_int16_t queue_num): queue_num(queue_num) { - - nl = mnl_socket_open(NETLINK_NETFILTER); - - if (nl == NULL) { throw std::runtime_error( "mnl_socket_open" );} - - if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) { - mnl_socket_close(nl); - throw std::runtime_error( "mnl_socket_bind" ); - } - portid = mnl_socket_get_portid(nl); - - buf = (char*) malloc(BUF_SIZE); - - if (!buf) { - mnl_socket_close(nl); - throw std::runtime_error( "allocate receive buffer" ); - } - - if (send_config_cmd(NFQNL_CFG_CMD_BIND) < 0) { - _clear(); - throw std::runtime_error( "mnl_socket_send" ); - } - //TEST if BIND was successful - if (send_config_cmd(NFQNL_CFG_CMD_NONE) < 0) { // SEND A NONE cmmand to generate an error meessage - _clear(); - throw std::runtime_error( "mnl_socket_send" ); - } - if (recv_packet() == -1) { //RECV the error message - _clear(); - throw std::runtime_error( "mnl_socket_recvfrom" ); - } - - struct nlmsghdr *nlh = (struct nlmsghdr *) buf; - - if (nlh->nlmsg_type != NLMSG_ERROR) { - _clear(); - throw std::runtime_error( "unexpected packet from kernel (expected NLMSG_ERROR packet)" ); - } - //nfqnl_msg_config_cmd - nlmsgerr* error_msg = (nlmsgerr *)mnl_nlmsg_get_payload(nlh); - - // error code taken from the linux kernel: - // https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27 - #define ENOTSUPP 524 /* Operation is not supported */ - - if (error_msg->error != -ENOTSUPP) { - _clear(); - throw std::invalid_argument( "queueid is already busy" ); - } - - //END TESTING BIND - nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); - nfq_nlmsg_cfg_put_params(nlh, NFQNL_COPY_PACKET, 0xffff); - - - mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO)); - mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO)); - - if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) { - _clear(); - throw std::runtime_error( "mnl_socket_send" ); - } - - } - - - - void run(){ - /* - * ENOBUFS is signalled to userspace when packets were lost - * on kernel side. In most cases, userspace isn't interested - * in this information, so turn it off. - */ - int ret = 1; - mnl_socket_setsockopt(nl, NETLINK_NO_ENOBUFS, &ret, sizeof(int)); - - for (;;) { - ret = recv_packet(); - if (ret == -1) { - throw std::runtime_error( "mnl_socket_recvfrom" ); - } - - ret = mnl_cb_run(buf, ret, 0, portid, queue_cb, nl); - if (ret < 0){ - throw std::runtime_error( "mnl_cb_run" ); - } - } - } - - ~NetfilterQueue() { - send_config_cmd(NFQNL_CFG_CMD_UNBIND); - _clear(); - } - private: - - ssize_t send_config_cmd(nfqnl_msg_config_cmds cmd){ - struct nlmsghdr *nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); - nfq_nlmsg_cfg_put_cmd(nlh, AF_INET, cmd); - return mnl_socket_sendto(nl, nlh, nlh->nlmsg_len); - } - - ssize_t recv_packet(){ - return mnl_socket_recvfrom(nl, buf, BUF_SIZE); - } - - void _clear(){ - if (buf != NULL) { - free(buf); - buf = NULL; - } - mnl_socket_close(nl); - } - - static int queue_cb(const struct nlmsghdr *nlh, void *data) - { - struct mnl_socket* nl = (struct mnl_socket*)data; - //Extract attributes from the nlmsghdr - struct nlattr *attr[NFQA_MAX+1] = {}; - - if (nfq_nlmsg_parse(nlh, attr) < 0) { - perror("problems parsing"); - return MNL_CB_ERROR; - } - if (attr[NFQA_PACKET_HDR] == NULL) { - fputs("metaheader not set\n", stderr); - return MNL_CB_ERROR; - } - //Get Payload - uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]); - void *payload = mnl_attr_get_payload(attr[NFQA_PAYLOAD]); - - //Return result to the kernel - struct nfqnl_msg_packet_hdr *ph = (nfqnl_msg_packet_hdr*) mnl_attr_get_payload(attr[NFQA_PACKET_HDR]); - struct nfgenmsg *nfg = (nfgenmsg *)mnl_nlmsg_get_payload(nlh); - char buf[MNL_SOCKET_BUFFER_SIZE]; - struct nlmsghdr *nlh_verdict; - struct nlattr *nest; - - nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(nfg->res_id)); - - /* - This define allow to avoid to allocate new heap memory for each packet. - The code under this comment is replicated for ipv6 and ip - Better solutions are welcome. :) - */ - #define PKT_HANDLE \ - Tins::PDU *transport_layer = find_transport_layer(&packet); \ - if(transport_layer->inner_pdu() == nullptr || transport_layer == nullptr){ \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); \ - }else{ \ - int size = transport_layer->inner_pdu()->size(); \ - if(callback_func((const uint8_t*)payload+plen - size, size)){ \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); \ - } else{ \ - if (transport_layer->pdu_type() == Tins::PDU::TCP){ \ - ((Tins::TCP *)transport_layer)->release_inner_pdu(); \ - ((Tins::TCP *)transport_layer)->set_flag(Tins::TCP::FIN,1); \ - ((Tins::TCP *)transport_layer)->set_flag(Tins::TCP::ACK,1); \ - ((Tins::TCP *)transport_layer)->set_flag(Tins::TCP::SYN,0); \ - nfq_nlmsg_verdict_put_pkt(nlh_verdict, packet.serialize().data(), packet.size()); \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); \ - }else{ \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_DROP ); \ - } \ - } \ - } - - // Check IP protocol version - if ( (((uint8_t*)payload)[0] & 0xf0) == 0x40 ){ - Tins::IP packet = Tins::IP((uint8_t*)payload,plen); - PKT_HANDLE - }else{ - Tins::IPv6 packet = Tins::IPv6((uint8_t*)payload,plen); - PKT_HANDLE - } - - /* example to set the connmark. First, start NFQA_CT section: */ - nest = mnl_attr_nest_start(nlh_verdict, NFQA_CT); - - /* then, add the connmark attribute: */ - mnl_attr_put_u32(nlh_verdict, CTA_MARK, htonl(42)); - /* more conntrack attributes, e.g. CTA_LABELS could be set here */ - - /* end conntrack section */ - mnl_attr_nest_end(nlh_verdict, nest); - - if (mnl_socket_sendto(nl, nlh_verdict, nlh_verdict->nlmsg_len) < 0) { - throw std::runtime_error( "mnl_socket_send" ); - } - - return MNL_CB_OK; - } - -}; - -template -class NFQueueSequence{ - private: - std::vector *> nfq; - uint16_t _init; - uint16_t _end; - std::vector threads; - public: - static const int QUEUE_BASE_NUM = 1000; - - NFQueueSequence(uint16_t seq_len){ - if (seq_len <= 0) throw std::invalid_argument("seq_len <= 0"); - nfq = std::vector*>(seq_len); - _init = QUEUE_BASE_NUM; - while(nfq[0] == NULL){ - if (_init+seq_len-1 >= 65536){ - throw std::runtime_error("NFQueueSequence: too many queues!"); - } - for (int i=0;i(_init+i); - }catch(const std::invalid_argument e){ - for(int j = 0; j < i; j++) { - delete nfq[j]; - nfq[j] = nullptr; - } - _init += seq_len - i; - break; - } - } - } - _end = _init + seq_len - 1; - } - - void start(){ - if (threads.size() != 0) throw std::runtime_error("NFQueueSequence: already started!"); - for (int i=0;i::run, nfq[i])); - } - } - - void join(){ - for (int i=0;i -#include -#include -#include -#include "../utils.hpp" - - -#ifndef REGEX_FILTER_HPP -#define REGEX_FILTER_HPP - -typedef jpcre2::select jp; -typedef std::pair regex_rule_pair; -typedef std::vector regex_rule_vector; -struct regex_rules{ - regex_rule_vector output_whitelist, input_whitelist, output_blacklist, input_blacklist; - - regex_rule_vector* getByCode(char code){ - switch(code){ - case 'C': // Client to server Blacklist - return &input_blacklist; break; - case 'c': // Client to server Whitelist - return &input_whitelist; break; - case 'S': // Server to client Blacklist - return &output_blacklist; break; - case 's': // Server to client Whitelist - return &output_whitelist; break; - } - throw std::invalid_argument( "Expected 'C' 'c' 'S' or 's'" ); - } - - int add(const char* arg){ - //Integrity checks - size_t arg_len = strlen(arg); - if (arg_len < 2 || arg_len%2 != 0){ - std::cerr << "[warning] [regex_rules.add] invalid arg passed (" << arg << "), skipping..." << std::endl; - return -1; - } - if (arg[0] != '0' && arg[0] != '1'){ - std::cerr << "[warning] [regex_rules.add] invalid is_case_sensitive (" << arg[0] << ") in '" << arg << "', must be '1' or '0', skipping..." << std::endl; - return -1; - } - if (arg[1] != 'C' && arg[1] != 'c' && arg[1] != 'S' && arg[1] != 's'){ - std::cerr << "[warning] [regex_rules.add] invalid filter_type (" << arg[1] << ") in '" << arg << "', must be 'C', 'c', 'S' or 's', skipping..." << std::endl; - return -1; - } - std::string hex(arg+2), expr; - if (!unhexlify(hex, expr)){ - std::cerr << "[warning] [regex_rules.add] invalid hex regex value (" << hex << "), skipping..." << std::endl; - return -1; - } - //Push regex - jp::Regex regex(expr,arg[0] == '1'?"gS":"giS"); - if (regex){ - std::cerr << "[info] [regex_rules.add] adding new regex filter: '" << expr << "'" << std::endl; - getByCode(arg[1])->push_back(std::make_pair(std::string(arg), regex)); - } else { - std::cerr << "[warning] [regex_rules.add] compiling of '" << expr << "' regex failed, skipping..." << std::endl; - return -1; - } - return 0; - } - - bool check(unsigned char* data, const size_t& bytes_transferred, const bool in_input){ - std::string str_data((char *) data, bytes_transferred); - for (regex_rule_pair ele:(in_input?input_blacklist:output_blacklist)){ - try{ - if(ele.second.match(str_data)){ - std::stringstream msg; - msg << "BLOCKED " << ele.first << "\n"; - std::cout << msg.str() << std::flush; - return false; - } - } catch(...){ - std::cerr << "[info] [regex_rules.check] Error while matching blacklist regex: " << ele.first << std::endl; - } - } - for (regex_rule_pair ele:(in_input?input_whitelist:output_whitelist)){ - try{ - std::cerr << "[debug] [regex_rules.check] regex whitelist match " << ele.second.getPattern() << std::endl; - if(!ele.second.match(str_data)){ - std::stringstream msg; - msg << "BLOCKED " << ele.first << "\n"; - std::cout << msg.str() << std::flush; - return false; - } - } catch(...){ - std::cerr << "[info] [regex_rules.check] Error while matching whitelist regex: " << ele.first << std::endl; - } - } - return true; - } - -}; - -#endif // REGEX_FILTER_HPP \ No newline at end of file diff --git a/backend/binsrc/classes/regex_rules.cpp b/backend/binsrc/classes/regex_rules.cpp new file mode 100644 index 0000000..2c2dc50 --- /dev/null +++ b/backend/binsrc/classes/regex_rules.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include "../utils.hpp" +#include +#include + +using namespace std; + + +#ifndef REGEX_FILTER_HPP +#define REGEX_FILTER_HPP + +enum FilterDirection{ CTOS, STOC }; + +struct decoded_regex { + string regex; + FilterDirection direction; + bool is_case_sensitive; +}; + +struct regex_ruleset { + hs_database_t* hs_db; + char** regexes; +}; + +decoded_regex decode_regex(string regex){ + + size_t arg_len = regex.size(); + if (arg_len < 2 || arg_len%2 != 0){ + cerr << "[warning] [decode_regex] invalid arg passed (" << regex << "), skipping..." << endl; + throw runtime_error( "Invalid expression len (too small)" ); + } + if (regex[0] != '0' && regex[0] != '1'){ + cerr << "[warning] [decode_regex] invalid is_case_sensitive (" << regex[0] << ") in '" << regex << "', must be '1' or '0', skipping..." << endl; + throw runtime_error( "Invalid is_case_sensitive" ); + } + if (regex[1] != 'C' && regex[1] != 'S'){ + cerr << "[warning] [decode_regex] invalid filter_direction (" << regex[1] << ") in '" << regex << "', must be 'C', 'S', skipping..." << endl; + throw runtime_error( "Invalid filter_direction" ); + } + string hex(regex.c_str()+2), expr; + if (!unhexlify(hex, expr)){ + cerr << "[warning] [decode_regex] invalid hex regex value (" << hex << "), skipping..." << endl; + throw runtime_error( "Invalid hex regex encoded value" ); + } + decoded_regex ruleset{ + regex: expr, + direction: regex[1] == 'C'? CTOS : STOC, + is_case_sensitive: regex[0] == '1' + }; + return ruleset; +} + +class RegexRules{ + public: + regex_ruleset output_ruleset, input_ruleset; + + private: + static inline u_int16_t glob_seq = 0; + u_int16_t version; + vector> decoded_input_rules; + vector> decoded_output_rules; + bool is_stream = true; + + void free_dbs(){ + if (output_ruleset.hs_db != nullptr){ + hs_free_database(output_ruleset.hs_db); + } + if (input_ruleset.hs_db != nullptr){ + hs_free_database(input_ruleset.hs_db); + } + } + + void fill_ruleset(vector> & decoded, regex_ruleset & ruleset){ + size_t n_of_regex = decoded.size(); + if (n_of_regex == 0){ + return; + } + const char* regex_match_rules[n_of_regex]; + unsigned int regex_array_ids[n_of_regex]; + unsigned int regex_flags[n_of_regex]; + for(int i = 0; i < n_of_regex; i++){ + regex_match_rules[i] = decoded[i].second.regex.c_str(); + regex_array_ids[i] = i; + regex_flags[i] = HS_FLAG_SINGLEMATCH | HS_FLAG_ALLOWEMPTY; + if (!decoded[i].second.is_case_sensitive){ + regex_flags[i] |= HS_FLAG_CASELESS; + } + } + + hs_database_t* rebuilt_db; + hs_compile_error_t *compile_err; + if ( + hs_compile_multi( + regex_match_rules, + regex_flags, + regex_array_ids, + n_of_regex, + is_stream?HS_MODE_STREAM:HS_MODE_BLOCK, + nullptr,&rebuilt_db, &compile_err + ) != HS_SUCCESS + ) { + cerr << "[warning] [RegexRules.fill_ruleset] hs_db failed to compile: '" << compile_err->message << "' skipping..." << endl; + hs_free_compile_error(compile_err); + throw runtime_error( "Failed to compile hyperscan db" ); + } + ruleset.hs_db = rebuilt_db; + } + + public: + RegexRules(vector raw_rules, bool is_stream){ + this->is_stream = is_stream; + this->version = ++glob_seq; // 0 version is a invalid version (useful for some logics) + for(string ele : raw_rules){ + try{ + decoded_regex rule = decode_regex(ele); + if (rule.direction == FilterDirection::CTOS){ + decoded_input_rules.push_back(make_pair(ele, rule)); + }else{ + decoded_output_rules.push_back(make_pair(ele, rule)); + } + }catch(...){ + throw current_exception(); + } + } + fill_ruleset(decoded_input_rules, input_ruleset); + try{ + fill_ruleset(decoded_output_rules, output_ruleset); + }catch(...){ + free_dbs(); + throw current_exception(); + } + } + + u_int16_t ver(){ + return version; + } + + RegexRules(bool is_stream){ + vector no_rules; + RegexRules(no_rules, is_stream); + } + + bool stream_mode(){ + return is_stream; + } + + + + RegexRules(){ + RegexRules(true); + } + + ~RegexRules(){ + free_dbs(); + } +}; + +#endif // REGEX_FILTER_HPP + diff --git a/backend/binsrc/cppqueue b/backend/binsrc/cppqueue new file mode 100755 index 0000000000000000000000000000000000000000..84696ecc35574763c2913f0ff6d098718a563c61 GIT binary patch literal 282672 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�p9|Dnw?+2=BoPhdiZ8|zgXA` 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

c*ro_&@K%V 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 using namespace std; -shared_ptr regex_config; +shared_ptr regex_config; void config_updater (){ string line; @@ -21,44 +21,116 @@ void config_updater (){ } cerr << "[info] [updater] Updating configuration with line " << line << endl; istringstream config_stream(line); - regex_rules *regex_new_config = new regex_rules(); + vector raw_rules; + while(!config_stream.eof()){ string data; config_stream >> data; if (data != "" && data != "\n"){ - regex_new_config->add(data.c_str()); + raw_rules.push_back(data); } } - regex_config.reset(regex_new_config); - cerr << "[info] [updater] Config update done" << endl; - + try{ + regex_config.reset(new RegexRules(raw_rules, regex_config->stream_mode())); + cerr << "[info] [updater] Config update done" << endl; + }catch(...){ + cerr << "[error] [updater] Failed to build new configuration!" << endl; + // TODO send a row on stdout for this error + } } } -template -bool filter_callback(const uint8_t *data, uint32_t len){ - shared_ptr current_config = regex_config; - return current_config->check((unsigned char *)data, len, is_input); +void inline scratch_setup(regex_ruleset &conf, hs_scratch_t* & scratch){ + if (scratch == nullptr){ + if (hs_alloc_scratch(conf.hs_db, &scratch) != HS_SUCCESS) { + throw invalid_argument("Cannot alloc scratch"); + } + } } -int main(int argc, char *argv[]) -{ +struct matched_data{ + unsigned int matched = 0; + bool has_matched = false; +}; + +bool filter_callback(packet_info & info){ + shared_ptr conf = regex_config; + if (conf->ver() != info.sctx->latest_config_ver){ + info.sctx->clean_scratches(); + } + scratch_setup(conf->input_ruleset, info.sctx->in_scratch); + scratch_setup(conf->output_ruleset, info.sctx->out_scratch); + + hs_database_t* regex_matcher = info.is_input ? conf->input_ruleset.hs_db : conf->output_ruleset.hs_db; + if (regex_matcher == nullptr){ + return true; + } + matched_data match_res; + hs_error_t err; + hs_scratch_t* scratch_space = info.is_input ? info.sctx->in_scratch: info.sctx->out_scratch; + auto match_func = [](unsigned int id, auto from, auto to, auto flags, auto ctx){ + auto res = (matched_data*)ctx; + res->has_matched = true; + res->matched = id; + return 1; // Stop matching + }; + if (conf->stream_mode()){ + matching_map match_map = info.is_input ? info.sctx->in_hs_streams : info.sctx->out_hs_streams; + auto stream_search = match_map.find(info.stream_id); + hs_stream_t* stream_match; + if (stream_search == match_map.end()){ + if (hs_open_stream(regex_matcher, 0, &stream_match) != HS_SUCCESS) { + cerr << "[error] [filter_callback] Error opening the stream matcher (hs)" << endl; + throw invalid_argument("Cannot open stream match on hyperscan"); + } + match_map[info.stream_id] = stream_match; + }else{ + stream_match = stream_search->second; + } + err = hs_scan_stream( + stream_match,info.payload.c_str(), info.payload.length(), + 0, scratch_space, match_func, &match_res + ); + }else{ + err = hs_scan( + regex_matcher,info.payload.c_str(), info.payload.length(), + 0, scratch_space, match_func, &match_res + ); + } + if (err != HS_SUCCESS) { + cerr << "[error] [filter_callback] Error while matching the stream (hs)" << endl; + throw invalid_argument("Error while matching the stream with hyperscan"); + } + if (match_res.has_matched){ + auto rules_vector = info.is_input ? conf->input_ruleset.regexes : conf->output_ruleset.regexes; + stringstream msg; + msg << "BLOCKED " << rules_vector[match_res.matched] << "\n"; + cout << msg.str() << flush; + return false; + } + return true; +} + +int main(int argc, char *argv[]){ int n_of_threads = 1; char * n_threads_str = getenv("NTHREADS"); - if (n_threads_str != NULL) n_of_threads = ::atoi(n_threads_str); + if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str); if(n_of_threads <= 0) n_of_threads = 1; - if (n_of_threads % 2 != 0 ) n_of_threads++; - cerr << "[info] [main] Using " << n_of_threads << " threads" << endl; - regex_config.reset(new regex_rules()); - NFQueueSequence> input_queues(n_of_threads/2); - input_queues.start(); - NFQueueSequence> output_queues(n_of_threads/2); - output_queues.start(); - cout << "QUEUES INPUT " << input_queues.init() << " " << input_queues.end() << " OUTPUT " << output_queues.init() << " " << output_queues.end() << endl; - cerr << "[info] [main] Input queues: " << input_queues.init() << ":" << input_queues.end() << " threads assigned: " << n_of_threads/2 << endl; - cerr << "[info] [main] Output queues: " << output_queues.init() << ":" << output_queues.end() << " threads assigned: " << n_of_threads/2 << endl; + char * matchmode = getenv("MATCH_MODE"); + bool stream_mode = true; + if (matchmode != nullptr && strcmp(matchmode, "block") == 0){ + stream_mode = false; + } + cerr << "[info] [main] Using " << n_of_threads << " threads" << endl; + regex_config.reset(new RegexRules(stream_mode)); + + NFQueueSequence queues(n_of_threads); + queues.start(); + + cout << "QUEUES " << queues.init() << " " << queues.end() << endl; + cerr << "[info] [main] Queues: " << queues.init() << ":" << queues.end() << " threads assigned: " << n_of_threads << endl; config_updater(); } diff --git a/backend/binsrc/nfqueue_regex/Cargo.lock b/backend/binsrc/nfqueue_regex/Cargo.lock deleted file mode 100644 index 11e8d94..0000000 --- a/backend/binsrc/nfqueue_regex/Cargo.lock +++ /dev/null @@ -1,32 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "atomic_refcell" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" - -[[package]] -name = "libc" -version = "0.2.153" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" - -[[package]] -name = "nfq" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c8f4c88952507d9df9400a6a2e48640fb460e21dcb2b4716eb3ff156d6db9e" -dependencies = [ - "libc", -] - -[[package]] -name = "nfqueue_regex" -version = "0.1.0" -dependencies = [ - "atomic_refcell", - "nfq", -] diff --git a/backend/binsrc/nfqueue_regex/Cargo.toml b/backend/binsrc/nfqueue_regex/Cargo.toml deleted file mode 100644 index b9b0e0a..0000000 --- a/backend/binsrc/nfqueue_regex/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "nfqueue_regex" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -atomic_refcell = "0.1.13" -nfq = "0.2.5" -#hyperscan = "0.3.2" diff --git a/backend/binsrc/nfqueue_regex/src/main.rs b/backend/binsrc/nfqueue_regex/src/main.rs deleted file mode 100644 index 639dfd5..0000000 --- a/backend/binsrc/nfqueue_regex/src/main.rs +++ /dev/null @@ -1,150 +0,0 @@ -use atomic_refcell::AtomicRefCell; -use nfq::{Queue, Verdict}; -use std::cell::{Cell, RefCell}; -use std::env; -use std::pin::Pin; -use std::rc::Rc; -use std::sync::atomic::{AtomicPtr, AtomicU32}; -use std::sync::mpsc::{self, Receiver, Sender}; -use std::sync::Arc; -use std::thread::{self, sleep, sleep_ms, JoinHandle}; - -enum WorkerMessage { - Error(String), - Dropped(usize), -} - -impl ToString for WorkerMessage { - fn to_string(&self) -> String { - match self { - WorkerMessage::Error(e) => format!("E{}", e), - WorkerMessage::Dropped(d) => format!("D{}", d), - } - } -} -struct Pool { - _workers: Vec, - pub start: u16, - pub end: u16, -} - -const QUEUE_BASE_NUM: u16 = 1000; -impl Pool { - fn new(threads: u16, tx: Sender, db: RefCell<&str>) -> Self { - // Find free queues - let mut start = QUEUE_BASE_NUM; - let mut queues: Vec<(Queue, u16)> = vec![]; - while queues.len() != threads.into() { - for queue_num in - (start..start.checked_add(threads + 1).expect("No more queues left")).rev() - { - let mut queue = Queue::open().unwrap(); - if queue.bind(queue_num).is_err() { - start = queue_num; - while let Some((mut q, num)) = queues.pop() { - let _ = q.unbind(num); - } - break; - }; - queues.push((queue, queue_num)); - } - } - - Pool { - _workers: queues - .into_iter() - .map(|(queue, queue_num)| Worker::new(queue, queue_num, tx.clone())) - .collect(), - start, - end: (start + threads), - } - } - - // fn join(self) { - // for worker in self._workers { - // let _ = worker.join(); - // } - // } -} - -struct Worker { - _inner: JoinHandle<()>, -} - -impl Worker { - fn new(mut queue: Queue, _queue_num: u16, tx: Sender) -> Self { - Worker { - _inner: thread::spawn(move || loop { - let mut msg = queue.recv().unwrap_or_else(|_| { - let _ = tx.send(WorkerMessage::Error("Fuck".to_string())); - panic!(""); - }); - - msg.set_verdict(Verdict::Accept); - queue.verdict(msg).unwrap(); - }), - } - } -} -struct InputOuputPools { - pub output_queue: Pool, - pub input_queue: Pool, - rx: Receiver, -} -impl InputOuputPools { - fn new(threads: u16) -> InputOuputPools { - let (tx, rx) = mpsc::channel(); - InputOuputPools { - output_queue: Pool::new(threads / 2, tx.clone(), RefCell::new("ciao")), - input_queue: Pool::new(threads / 2, tx, RefCell::new("miao")), - rx, - } - } - - fn poll_events(&self) { - loop { - let event = self.rx.recv().expect("Channel has hung up"); - println!("{}", event.to_string()); - } - } -} - -static mut DB: AtomicPtr> = AtomicPtr::new(std::ptr::null_mut() as *mut Arc); - -fn main() -> std::io::Result<()> { - let mut my_x: Arc = Arc::new(0); - let my_x_ptr: *mut Arc = std::ptr::addr_of_mut!(my_x); - - unsafe { DB.store(my_x_ptr, std::sync::atomic::Ordering::SeqCst) }; - - thread::spawn(|| loop { - let x_ptr = unsafe { DB.load(std::sync::atomic::Ordering::SeqCst) }; - let x = unsafe { (*x_ptr).clone() }; - dbg!(x); - //sleep_ms(1000); - }); - - for i in 0..1000000000 { - let mut my_x: Arc = Arc::new(i); - let my_x_ptr: *mut Arc = std::ptr::addr_of_mut!(my_x); - unsafe { DB.store(my_x_ptr, std::sync::atomic::Ordering::SeqCst) }; - //sleep_ms(100); - } - - let mut threads = env::var("NPROCS").unwrap_or_default().parse().unwrap_or(2); - if threads % 2 != 0 { - threads += 1; - } - - let in_out_pools = InputOuputPools::new(threads); - eprintln!( - "[info] [main] Input queues: {}:{}", - in_out_pools.input_queue.start, in_out_pools.input_queue.end - ); - eprintln!( - "[info] [main] Output queues: {}:{}", - in_out_pools.output_queue.start, in_out_pools.output_queue.end - ); - in_out_pools.poll_events(); - Ok(()) -} diff --git a/backend/binsrc/nfqueue_regex/src/regex_rules.rs b/backend/binsrc/nfqueue_regex/src/regex_rules.rs deleted file mode 100644 index 8b13789..0000000 --- a/backend/binsrc/nfqueue_regex/src/regex_rules.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/binsrc/proxy.cpp b/backend/binsrc/proxy.cpp deleted file mode 100644 index d572667..0000000 --- a/backend/binsrc/proxy.cpp +++ /dev/null @@ -1,493 +0,0 @@ -/* - Copyright (c) 2007 Arash Partow (http://www.partow.net) - URL: http://www.partow.net/programming/tcpproxy/index.html - Modified and adapted by Pwnzer0tt1 -*/ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -typedef jpcre2::select jp; -using namespace std; - -bool unhexlify(string const &hex, string &newString) { - try{ - int len = hex.length(); - for(int i=0; i< len; i+=2) - { - std::string byte = hex.substr(i,2); - char chr = (char) (int)strtol(byte.c_str(), NULL, 16); - newString.push_back(chr); - } - return true; - } - catch (...){ - return false; - } -} - -typedef pair regex_rule_pair; -typedef vector regex_rule_vector; -struct regex_rules{ - regex_rule_vector regex_s_c_w, regex_c_s_w, regex_s_c_b, regex_c_s_b; - - regex_rule_vector* getByCode(char code){ - switch(code){ - case 'C': // Client to server Blacklist - return ®ex_c_s_b; break; - case 'c': // Client to server Whitelist - return ®ex_c_s_w; break; - case 'S': // Server to client Blacklist - return ®ex_s_c_b; break; - case 's': // Server to client Whitelist - return ®ex_s_c_w; break; - } - throw invalid_argument( "Expected 'C' 'c' 'S' or 's'" ); - } - - void add(const char* arg){ - - //Integrity checks - size_t arg_len = strlen(arg); - if (arg_len < 2 || arg_len%2 != 0) return; - if (arg[0] != '0' && arg[0] != '1') return; - if (arg[1] != 'C' && arg[1] != 'c' && arg[1] != 'S' && arg[1] != 's') return; - string hex(arg+2), expr; - if (!unhexlify(hex, expr)) return; - //Push regex - jp::Regex regex(expr,arg[0] == '1'?"gS":"giS"); - if (regex){ - #ifdef DEBUG - cerr << "Added regex " << expr << " " << arg << endl; - #endif - getByCode(arg[1])->push_back(make_pair(string(arg), regex)); - } else { - cerr << "Regex " << arg << " was not compiled successfully" << endl; - } - } - -}; -shared_ptr regex_config; - -mutex update_mutex; - -bool filter_data(unsigned char* data, const size_t& bytes_transferred, regex_rule_vector const &blacklist, regex_rule_vector const &whitelist){ - #ifdef DEBUG_PACKET - cerr << "---------------- Packet ----------------" << endl; - for(int i=0;i - { - public: - - typedef ip::tcp::socket socket_type; - typedef boost::shared_ptr ptr_type; - - bridge(boost::asio::io_context& ios) - : downstream_socket_(ios), - upstream_socket_ (ios), - thread_safety(ios) - {} - - socket_type& downstream_socket() - { - // Client socket - return downstream_socket_; - } - - socket_type& upstream_socket() - { - // Remote server socket - return upstream_socket_; - } - - void start(const string& upstream_host, unsigned short upstream_port) - { - // Attempt connection to remote server (upstream side) - upstream_socket_.async_connect( - ip::tcp::endpoint( - boost::asio::ip::address::from_string(upstream_host), - upstream_port), - boost::asio::bind_executor(thread_safety, - boost::bind( - &bridge::handle_upstream_connect, - shared_from_this(), - boost::asio::placeholders::error))); - } - - void handle_upstream_connect(const boost::system::error_code& error) - { - if (!error) - { - // Setup async read from remote server (upstream) - - upstream_socket_.async_read_some( - boost::asio::buffer(upstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_upstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - - // Setup async read from client (downstream) - downstream_socket_.async_read_some( - boost::asio::buffer(downstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_downstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - close(); - } - - private: - - /* - Section A: Remote Server --> Proxy --> Client - Process data recieved from remote sever then send to client. - */ - - // Read from remote server complete, now send data to client - void handle_upstream_read(const boost::system::error_code& error, - const size_t& bytes_transferred) // Da Server a Client - { - if (!error) - { - shared_ptr regex_old_config = regex_config; - if (filter_data(upstream_data_, bytes_transferred, regex_old_config->regex_s_c_b, regex_old_config->regex_s_c_w)){ - async_write(downstream_socket_, - boost::asio::buffer(upstream_data_,bytes_transferred), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_downstream_write, - shared_from_this(), - boost::asio::placeholders::error))); - }else{ - close(); - } - } - else - close(); - } - - // Write to client complete, Async read from remote server - void handle_downstream_write(const boost::system::error_code& error) - { - if (!error) - { - - upstream_socket_.async_read_some( - boost::asio::buffer(upstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_upstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - close(); - } - // *** End Of Section A *** - - - /* - Section B: Client --> Proxy --> Remove Server - Process data recieved from client then write to remove server. - */ - - // Read from client complete, now send data to remote server - void handle_downstream_read(const boost::system::error_code& error, - const size_t& bytes_transferred) // Da Client a Server - { - if (!error) - { - shared_ptr regex_old_config = regex_config; - if (filter_data(downstream_data_, bytes_transferred, regex_old_config->regex_c_s_b, regex_old_config->regex_c_s_w)){ - async_write(upstream_socket_, - boost::asio::buffer(downstream_data_,bytes_transferred), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_upstream_write, - shared_from_this(), - boost::asio::placeholders::error))); - }else{ - close(); - } - } - else - close(); - } - - // Write to remote server complete, Async read from client - void handle_upstream_write(const boost::system::error_code& error) - { - if (!error) - { - downstream_socket_.async_read_some( - boost::asio::buffer(downstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_downstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - close(); - } - // *** End Of Section B *** - - void close() - { - boost::mutex::scoped_lock lock(mutex_); - - if (downstream_socket_.is_open()) - { - downstream_socket_.close(); - } - - if (upstream_socket_.is_open()) - { - upstream_socket_.close(); - } - } - - socket_type downstream_socket_; - socket_type upstream_socket_; - - enum { max_data_length = 8192 }; //8KB - unsigned char downstream_data_[max_data_length]; - unsigned char upstream_data_ [max_data_length]; - boost::asio::io_context::strand thread_safety; - boost::mutex mutex_; - public: - - class acceptor - { - public: - - acceptor(boost::asio::io_context& io_context, - const string& local_host, unsigned short local_port, - const string& upstream_host, unsigned short upstream_port) - : io_context_(io_context), - localhost_address(boost::asio::ip::address_v4::from_string(local_host)), - acceptor_(io_context_,ip::tcp::endpoint(localhost_address,local_port)), - upstream_port_(upstream_port), - upstream_host_(upstream_host) - {} - - bool accept_connections() - { - try - { - session_ = boost::shared_ptr(new bridge(io_context_)); - - acceptor_.async_accept(session_->downstream_socket(), - boost::asio::bind_executor(session_->thread_safety, - boost::bind(&acceptor::handle_accept, - this, - boost::asio::placeholders::error))); - } - catch(exception& e) - { - cerr << "acceptor exception: " << e.what() << endl; - return false; - } - - return true; - } - - private: - - void handle_accept(const boost::system::error_code& error) - { - if (!error) - { - session_->start(upstream_host_,upstream_port_); - - if (!accept_connections()) - { - cerr << "Failure during call to accept." << endl; - } - } - else - { - cerr << "Error: " << error.message() << endl; - } - } - - boost::asio::io_context& io_context_; - ip::address_v4 localhost_address; - ip::tcp::acceptor acceptor_; - ptr_type session_; - unsigned short upstream_port_; - string upstream_host_; - }; - - }; -} - -void update_config (boost::asio::streambuf &input_buffer){ - #ifdef DEBUG - cerr << "Updating configuration" << endl; - #endif - std::istream config_stream(&input_buffer); - std::unique_lock lck(update_mutex); - regex_rules *regex_new_config = new regex_rules(); - string data; - while(true){ - config_stream >> data; - if (config_stream.eof()) break; - regex_new_config->add(data.c_str()); - } - regex_config.reset(regex_new_config); -} - -class async_updater -{ -public: - async_updater(boost::asio::io_context& io_context) : input_(io_context, ::dup(STDIN_FILENO)), thread_safety(io_context) - { - - boost::asio::async_read_until(input_, input_buffer_, '\n', - boost::asio::bind_executor(thread_safety, - boost::bind(&async_updater::on_update, this, - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - - void on_update(const boost::system::error_code& error, std::size_t length) - { - if (!error) - { - update_config(input_buffer_); - boost::asio::async_read_until(input_, input_buffer_, '\n', - boost::asio::bind_executor(thread_safety, - boost::bind(&async_updater::on_update, this, - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - { - close(); - } - } - - void close() - { - input_.close(); - } - -private: - boost::asio::posix::stream_descriptor input_; - boost::asio::io_context::strand thread_safety; - boost::asio::streambuf input_buffer_; -}; - - -int main(int argc, char* argv[]) -{ - if (argc < 5) - { - cerr << "usage: tcpproxy_server " << endl; - return 1; - } - - const unsigned short local_port = static_cast(::atoi(argv[2])); - const unsigned short forward_port = static_cast(::atoi(argv[4])); - const string local_host = argv[1]; - const string forward_host = argv[3]; - - int threads = 1; - char * n_threads_str = getenv("NTHREADS"); - if (n_threads_str != NULL) threads = ::atoi(n_threads_str); - - boost::asio::io_context ios; - - boost::asio::streambuf buf; - boost::asio::posix::stream_descriptor cin_in(ios, ::dup(STDIN_FILENO)); - boost::asio::read_until(cin_in, buf,'\n'); - update_config(buf); - - async_updater updater(ios); - - #ifdef DEBUG - cerr << "Starting Proxy" << endl; - #endif - try - { - tcp_proxy::bridge::acceptor acceptor(ios, - local_host, local_port, - forward_host, forward_port); - - acceptor.accept_connections(); - - if (threads > 1){ - boost::thread_group tg; - for (unsigned i = 0; i < threads; ++i) - tg.create_thread(boost::bind(&boost::asio::io_context::run, &ios)); - - tg.join_all(); - }else{ - ios.run(); - } - } - catch(exception& e) - { - cerr << "Error: " << e.what() << endl; - return 1; - } - #ifdef DEBUG - cerr << "Proxy stopped!" << endl; - #endif - - return 0; -} diff --git a/backend/binsrc/utils.hpp b/backend/binsrc/utils.hpp index 9d40366..b61ef22 100644 --- a/backend/binsrc/utils.hpp +++ b/backend/binsrc/utils.hpp @@ -10,7 +10,7 @@ bool unhexlify(std::string const &hex, std::string &newString) { for(int i=0; i< len; i+=2) { std::string byte = hex.substr(i,2); - char chr = (char) (int)strtol(byte.c_str(), NULL, 16); + char chr = (char) (int)strtol(byte.c_str(), nullptr, 16); newString.push_back(chr); } return true; diff --git a/backend/modules/firewall/firewall.py b/backend/modules/firewall/firewall.py index 13c0122..b5bb292 100644 --- a/backend/modules/firewall/firewall.py +++ b/backend/modules/firewall/firewall.py @@ -1,6 +1,6 @@ import asyncio from modules.firewall.nftables import FiregexTables -from modules.firewall.models import * +from modules.firewall.models import Rule, FirewallSettings from utils.sqlite import SQLite from modules.firewall.models import Action @@ -131,5 +131,5 @@ class FirewallManager: return self.db.get("allow_dhcp", "1") == "1" @drop_invalid.setter - def allow_dhcp(self, value): + def allow_dhcp_set(self, value): self.db.set("allow_dhcp", "1" if value else "0") diff --git a/backend/modules/nfregex/firegex.py b/backend/modules/nfregex/firegex.py index 8d57d5d..b04409a 100644 --- a/backend/modules/nfregex/firegex.py +++ b/backend/modules/nfregex/firegex.py @@ -1,7 +1,9 @@ from modules.nfregex.nftables import FiregexTables -from utils import ip_parse, run_func +from utils import run_func from modules.nfregex.models import Service, Regex -import re, os, asyncio +import re +import os +import asyncio import traceback nft = FiregexTables() @@ -20,7 +22,8 @@ class RegexFilter: self.regex = regex self.is_case_sensitive = is_case_sensitive self.is_blacklist = is_blacklist - if input_mode == output_mode: input_mode = output_mode = True # (False, False) == (True, True) + if input_mode == output_mode: + input_mode = output_mode = True # (False, False) == (True, True) self.input_mode = input_mode self.output_mode = output_mode self.blocked = blocked_packets @@ -37,8 +40,10 @@ class RegexFilter: update_func = update_func ) def compile(self): - if isinstance(self.regex, str): self.regex = self.regex.encode() - if not isinstance(self.regex, bytes): raise Exception("Invalid Regex Paramether") + if isinstance(self.regex, str): + self.regex = self.regex.encode() + if not isinstance(self.regex, bytes): + raise Exception("Invalid Regex Paramether") re.compile(self.regex) # raise re.error if it's invalid! case_sensitive = "1" if self.is_case_sensitive else "0" if self.input_mode: @@ -67,9 +72,9 @@ class FiregexInterceptor: self.srv = srv self.filter_map_lock = asyncio.Lock() self.update_config_lock = asyncio.Lock() - input_range, output_range = await self._start_binary() + queue_range = await self._start_binary() self.update_task = asyncio.create_task(self.update_blocked()) - nft.add(self.srv, input_range, output_range) + nft.add(self.srv, queue_range) return self async def _start_binary(self): @@ -87,7 +92,7 @@ class FiregexInterceptor: line = line_fut.decode() if line.startswith("QUEUES "): params = line.split() - return (int(params[2]), int(params[3])), (int(params[5]), int(params[6])) + return (int(params[1]), int(params[2])) else: self.process.kill() raise Exception("Invalid binary output") @@ -102,8 +107,10 @@ class FiregexInterceptor: if regex_id in self.filter_map: self.filter_map[regex_id].blocked+=1 await self.filter_map[regex_id].update() - except asyncio.CancelledError: pass - except asyncio.IncompleteReadError: pass + except asyncio.CancelledError: + pass + except asyncio.IncompleteReadError: + pass except Exception: traceback.print_exc() @@ -135,6 +142,7 @@ class FiregexInterceptor: raw_filters = filter_obj.compile() for filter in raw_filters: res[filter] = filter_obj - except Exception: pass + except Exception: + pass return res diff --git a/backend/modules/nfregex/firewall.py b/backend/modules/nfregex/firewall.py index 9516f63..d0d5479 100644 --- a/backend/modules/nfregex/firewall.py +++ b/backend/modules/nfregex/firewall.py @@ -30,14 +30,15 @@ class ServiceManager: new_filters = set([f.id for f in regexes]) #remove old filters for f in old_filters: - if not f in new_filters: + if f not in new_filters: del self.filters[f] #add new filters for f in new_filters: - if not f in old_filters: + if f not in old_filters: filter = [ele for ele in regexes if ele.id == f][0] self.filters[f] = RegexFilter.from_regex(filter, self._stats_updater) - if self.interceptor: await self.interceptor.reload(self.filters.values()) + if self.interceptor: + await self.interceptor.reload(self.filters.values()) def __update_status_db(self, status): self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.srv.id) @@ -114,4 +115,5 @@ class FirewallManager: else: raise ServiceNotFoundException() -class ServiceNotFoundException(Exception): pass +class ServiceNotFoundException(Exception): + pass diff --git a/backend/modules/nfregex/nftables.py b/backend/modules/nfregex/nftables.py index a0bc917..beb5953 100644 --- a/backend/modules/nfregex/nftables.py +++ b/backend/modules/nfregex/nftables.py @@ -45,36 +45,35 @@ class FiregexTables(NFTableManager): {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}}, ]) - def add(self, srv:Service, queue_range_input, queue_range_output): + def add(self, srv:Service, queue_range): for ele in self.get(): if ele.__eq__(srv): return - init, end = queue_range_output + init, end = queue_range if init > end: init, end = end, init - self.cmd({ "insert":{ "rule": { - "family": "inet", - "table": self.table_name, - "chain": self.output_chain, - "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, - {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}}, - {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} + self.cmd( + { "insert":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": self.output_chain, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, + {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}}, + {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} ] - }}}) - - init, end = queue_range_input - if init > end: init, end = end, init - self.cmd({"insert":{"rule":{ - "family": "inet", - "table": self.table_name, - "chain": self.input_chain, - "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, - {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}}, - {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} - ] - }}}) + }}}, + {"insert":{"rule":{ + "family": "inet", + "table": self.table_name, + "chain": self.input_chain, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, + {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}}, + {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} + ] + }}} + ) def get(self) -> list[FiregexFilter]: diff --git a/backend/modules/porthijack/firewall.py b/backend/modules/porthijack/firewall.py index 29e2f06..03004b2 100644 --- a/backend/modules/porthijack/firewall.py +++ b/backend/modules/porthijack/firewall.py @@ -5,7 +5,8 @@ from utils.sqlite import SQLite nft = FiregexTables() -class ServiceNotFoundException(Exception): pass +class ServiceNotFoundException(Exception): + pass class ServiceManager: def __init__(self, srv: Service, db): @@ -29,7 +30,8 @@ class ServiceManager: async def refresh(self, srv:Service): self.srv = srv - if self.active: await self.restart() + if self.active: + await self.restart() def _set_status(self,active): self.active = active diff --git a/backend/modules/porthijack/nftables.py b/backend/modules/porthijack/nftables.py index 34e0669..1d8dcde 100644 --- a/backend/modules/porthijack/nftables.py +++ b/backend/modules/porthijack/nftables.py @@ -50,7 +50,8 @@ class FiregexTables(NFTableManager): def add(self, srv:Service): for ele in self.get(): - if ele.__eq__(srv): return + if ele.__eq__(srv): + return self.cmd({ "insert":{ "rule": { "family": "inet", diff --git a/backend/modules/regexproxy/proxy.py b/backend/modules/regexproxy/proxy.py deleted file mode 100644 index efaa090..0000000 --- a/backend/modules/regexproxy/proxy.py +++ /dev/null @@ -1,116 +0,0 @@ -import re, os, asyncio - -class Filter: - def __init__(self, regex, is_case_sensitive=True, is_blacklist=True, c_to_s=False, s_to_c=False, blocked_packets=0, code=None): - self.regex = regex - self.is_case_sensitive = is_case_sensitive - self.is_blacklist = is_blacklist - if c_to_s == s_to_c: c_to_s = s_to_c = True # (False, False) == (True, True) - self.c_to_s = c_to_s - self.s_to_c = s_to_c - self.blocked = blocked_packets - self.code = code - - def compile(self): - if isinstance(self.regex, str): self.regex = self.regex.encode() - if not isinstance(self.regex, bytes): raise Exception("Invalid Regex Paramether") - re.compile(self.regex) # raise re.error if is invalid! - case_sensitive = "1" if self.is_case_sensitive else "0" - if self.c_to_s: - yield case_sensitive + "C" + self.regex.hex() if self.is_blacklist else case_sensitive + "c"+ self.regex.hex() - if self.s_to_c: - yield case_sensitive + "S" + self.regex.hex() if self.is_blacklist else case_sensitive + "s"+ self.regex.hex() - -class Proxy: - def __init__(self, internal_port=0, public_port=0, callback_blocked_update=None, filters=None, public_host="0.0.0.0", internal_host="127.0.0.1"): - self.filter_map = {} - self.filter_map_lock = asyncio.Lock() - self.update_config_lock = asyncio.Lock() - self.status_change = asyncio.Lock() - self.public_host = public_host - self.public_port = public_port - self.internal_host = internal_host - self.internal_port = internal_port - self.filters = set(filters) if filters else set([]) - self.process = None - self.callback_blocked_update = callback_blocked_update - - async def start(self, in_pause=False): - await self.status_change.acquire() - if not self.isactive(): - try: - self.filter_map = self.compile_filters() - filters_codes = self.get_filter_codes() if not in_pause else [] - proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../proxy") - - self.process = await asyncio.create_subprocess_exec( - proxy_binary_path, str(self.public_host), str(self.public_port), str(self.internal_host), str(self.internal_port), - stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE - ) - await self.update_config(filters_codes) - finally: - self.status_change.release() - try: - while True: - buff = await self.process.stdout.readuntil() - stdout_line = buff.decode() - if stdout_line.startswith("BLOCKED"): - regex_id = stdout_line.split()[1] - async with self.filter_map_lock: - if regex_id in self.filter_map: - self.filter_map[regex_id].blocked+=1 - if self.callback_blocked_update: self.callback_blocked_update(self.filter_map[regex_id]) - except Exception: - return await self.process.wait() - else: - self.status_change.release() - - - async def stop(self): - async with self.status_change: - if self.isactive(): - self.process.kill() - return False - return True - - async def restart(self, in_pause=False): - status = await self.stop() - await self.start(in_pause=in_pause) - return status - - async def update_config(self, filters_codes): - async with self.update_config_lock: - if (self.isactive()): - self.process.stdin.write((" ".join(filters_codes)+"\n").encode()) - await self.process.stdin.drain() - - async def reload(self): - if self.isactive(): - async with self.filter_map_lock: - self.filter_map = self.compile_filters() - filters_codes = self.get_filter_codes() - await self.update_config(filters_codes) - - def get_filter_codes(self): - filters_codes = list(self.filter_map.keys()) - filters_codes.sort(key=lambda a: self.filter_map[a].blocked, reverse=True) - return filters_codes - - def isactive(self): - return self.process and self.process.returncode is None - - async def pause(self): - if self.isactive(): - await self.update_config([]) - else: - await self.start(in_pause=True) - - def compile_filters(self): - res = {} - for filter_obj in self.filters: - try: - raw_filters = filter_obj.compile() - for filter in raw_filters: - res[filter] = filter_obj - except Exception: pass - return res diff --git a/backend/modules/regexproxy/utils.py b/backend/modules/regexproxy/utils.py deleted file mode 100644 index 2695b87..0000000 --- a/backend/modules/regexproxy/utils.py +++ /dev/null @@ -1,199 +0,0 @@ -import secrets -from modules.regexproxy.proxy import Filter, Proxy -import random, socket, asyncio -from base64 import b64decode -from utils.sqlite import SQLite -from utils import socketio_emit - -class STATUS: - WAIT = "wait" - STOP = "stop" - PAUSE = "pause" - ACTIVE = "active" - -class ServiceNotFoundException(Exception): pass - -class ServiceManager: - def __init__(self, id, db): - self.id = id - self.db = db - self.proxy = Proxy( - internal_host="127.0.0.1", - callback_blocked_update=self._stats_updater - ) - self.status = STATUS.STOP - self.wanted_status = STATUS.STOP - self.filters = {} - self._update_port_from_db() - self._update_filters_from_db() - self.lock = asyncio.Lock() - self.starter = None - - def _update_port_from_db(self): - res = self.db.query(""" - SELECT - public_port, - internal_port - FROM services WHERE service_id = ?; - """, self.id) - if len(res) == 0: raise ServiceNotFoundException() - self.proxy.internal_port = res[0]["internal_port"] - self.proxy.public_port = res[0]["public_port"] - - def _update_filters_from_db(self): - res = self.db.query(""" - SELECT - regex, mode, regex_id `id`, is_blacklist, - blocked_packets n_packets, is_case_sensitive - FROM regexes WHERE service_id = ? AND active=1; - """, self.id) - - #Filter check - old_filters = set(self.filters.keys()) - new_filters = set([f["id"] for f in res]) - - #remove old filters - for f in old_filters: - if not f in new_filters: - del self.filters[f] - - for f in new_filters: - if not f in old_filters: - filter_info = [ele for ele in res if ele["id"] == f][0] - self.filters[f] = Filter( - is_case_sensitive=filter_info["is_case_sensitive"], - c_to_s=filter_info["mode"] in ["C","B"], - s_to_c=filter_info["mode"] in ["S","B"], - is_blacklist=filter_info["is_blacklist"], - regex=b64decode(filter_info["regex"]), - blocked_packets=filter_info["n_packets"], - code=f - ) - self.proxy.filters = list(self.filters.values()) - - def __update_status_db(self, status): - self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.id) - - async def next(self,to): - async with self.lock: - return await self._next(to) - - async def _next(self, to): - if self.status != to: - # ACTIVE -> PAUSE or PAUSE -> ACTIVE - if (self.status, to) in [(STATUS.ACTIVE, STATUS.PAUSE)]: - await self.proxy.pause() - self._set_status(to) - - elif (self.status, to) in [(STATUS.PAUSE, STATUS.ACTIVE)]: - await self.proxy.reload() - self._set_status(to) - - # ACTIVE -> STOP - elif (self.status,to) in [(STATUS.ACTIVE, STATUS.STOP), (STATUS.WAIT, STATUS.STOP), (STATUS.PAUSE, STATUS.STOP)]: #Stop proxy - if self.starter: self.starter.cancel() - await self.proxy.stop() - self._set_status(to) - - # STOP -> ACTIVE or STOP -> PAUSE - elif (self.status, to) in [(STATUS.STOP, STATUS.ACTIVE), (STATUS.STOP, STATUS.PAUSE)]: - self.wanted_status = to - self._set_status(STATUS.WAIT) - self.__proxy_starter(to) - - - def _stats_updater(self,filter:Filter): - self.db.query("UPDATE regexes SET blocked_packets = ? WHERE regex_id = ?;", filter.blocked, filter.code) - - async def update_port(self): - async with self.lock: - self._update_port_from_db() - if self.status in [STATUS.PAUSE, STATUS.ACTIVE]: - next_status = self.status if self.status != STATUS.WAIT else self.wanted_status - await self._next(STATUS.STOP) - await self._next(next_status) - - def _set_status(self,status): - self.status = status - self.__update_status_db(status) - - - async def update_filters(self): - async with self.lock: - self._update_filters_from_db() - if self.status in [STATUS.PAUSE, STATUS.ACTIVE]: - await self.proxy.reload() - - def __proxy_starter(self,to): - async def func(): - try: - while True: - if check_port_is_open(self.proxy.public_port): - self._set_status(to) - await socketio_emit(["regexproxy"]) - await self.proxy.start(in_pause=(to==STATUS.PAUSE)) - self._set_status(STATUS.STOP) - await socketio_emit(["regexproxy"]) - return - else: - await asyncio.sleep(.5) - except asyncio.CancelledError: - self._set_status(STATUS.STOP) - await self.proxy.stop() - self.starter = asyncio.create_task(func()) - -class ProxyManager: - def __init__(self, db:SQLite): - self.db = db - self.proxy_table: dict[str, ServiceManager] = {} - self.lock = asyncio.Lock() - - async def close(self): - for key in list(self.proxy_table.keys()): - await self.remove(key) - - async def remove(self,id): - async with self.lock: - if id in self.proxy_table: - await self.proxy_table[id].next(STATUS.STOP) - del self.proxy_table[id] - - async def reload(self): - async with self.lock: - for srv in self.db.query('SELECT service_id, status FROM services;'): - srv_id, req_status = srv["service_id"], srv["status"] - if srv_id in self.proxy_table: - continue - - self.proxy_table[srv_id] = ServiceManager(srv_id,self.db) - await self.proxy_table[srv_id].next(req_status) - - def get(self,id) -> ServiceManager: - if id in self.proxy_table: - return self.proxy_table[id] - else: - raise ServiceNotFoundException() - -def check_port_is_open(port): - try: - sock = socket.socket() - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(('0.0.0.0',port)) - sock.close() - return True - except Exception: - return False - -def gen_service_id(db): - while True: - res = secrets.token_hex(8) - if len(db.query('SELECT 1 FROM services WHERE service_id = ?;', res)) == 0: - break - return res - -def gen_internal_port(db): - while True: - res = random.randint(30000, 45000) - if len(db.query('SELECT 1 FROM services WHERE internal_port = ?;', res)) == 0: - break - return res \ No newline at end of file diff --git a/backend/routers/firewall.py b/backend/routers/firewall.py index 9f3d311..8801db9 100644 --- a/backend/routers/firewall.py +++ b/backend/routers/firewall.py @@ -5,7 +5,7 @@ from utils import ip_parse, ip_family, socketio_emit from utils.models import ResetRequest, StatusMessageModel from modules.firewall.nftables import FiregexTables from modules.firewall.firewall import FirewallManager -from modules.firewall.models import * +from modules.firewall.models import FirewallSettings, RuleInfo, RuleModel, RuleFormAdd, Mode, Table db = SQLite('db/firewall-rules.db', { 'rules': { diff --git a/backend/routers/nfregex.py b/backend/routers/nfregex.py index 9766a42..d0cdf79 100644 --- a/backend/routers/nfregex.py +++ b/backend/routers/nfregex.py @@ -147,7 +147,8 @@ async def get_service_by_id(service_id: str): FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id WHERE s.service_id = ? GROUP BY s.service_id; """, service_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") + if len(res) == 0: + raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] @app.get('/service/{service_id}/stop', response_model=StatusMessageModel) @@ -177,7 +178,8 @@ async def service_delete(service_id: str): async def service_rename(service_id: str, form: RenameForm): """Request to change the name of a specific service""" form.name = refactor_name(form.name) - if not form.name: raise HTTPException(status_code=400, detail="The name cannot be empty!") + if not form.name: + raise HTTPException(status_code=400, detail="The name cannot be empty!") try: db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) except sqlite3.IntegrityError: @@ -188,7 +190,8 @@ async def service_rename(service_id: str, form: RenameForm): @app.get('/service/{service_id}/regexes', response_model=list[RegexModel]) async def get_service_regexe_list(service_id: str): """Get the list of the regexes of 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!") + 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!") return db.query(""" SELECT regex, mode, regex_id `id`, service_id, is_blacklist, @@ -205,7 +208,8 @@ async def get_regex_by_id(regex_id: int): blocked_packets n_packets, is_case_sensitive, active FROM regexes WHERE `id` = ?; """, regex_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This regex does not exists!") + if len(res) == 0: + raise HTTPException(status_code=400, detail="This regex does not exists!") return res[0] @app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) diff --git a/backend/routers/porthijack.py b/backend/routers/porthijack.py index 6bf8603..8fd3c54 100644 --- a/backend/routers/porthijack.py +++ b/backend/routers/porthijack.py @@ -96,7 +96,8 @@ async def get_service_list(): async def get_service_by_id(service_id: str): """Get info about a specific service using his id""" res = db.query("SELECT service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services WHERE service_id = ?;", service_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") + if len(res) == 0: + raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] @app.get('/service/{service_id}/stop', response_model=StatusMessageModel) @@ -125,7 +126,8 @@ async def service_delete(service_id: str): async def service_rename(service_id: str, form: RenameForm): """Request to change the name of a specific service""" form.name = refactor_name(form.name) - if not form.name: raise HTTPException(status_code=400, detail="The name cannot be empty!") + if not form.name: + raise HTTPException(status_code=400, detail="The name cannot be empty!") try: db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) except sqlite3.IntegrityError: diff --git a/backend/routers/regexproxy.py b/backend/routers/regexproxy.py deleted file mode 100644 index 5360592..0000000 --- a/backend/routers/regexproxy.py +++ /dev/null @@ -1,311 +0,0 @@ -from base64 import b64decode -import sqlite3, re -from fastapi import APIRouter, HTTPException -from pydantic import BaseModel -from modules.regexproxy.utils import STATUS, ProxyManager, gen_internal_port, gen_service_id -from utils.sqlite import SQLite -from utils.models import ResetRequest, StatusMessageModel -from utils import refactor_name, socketio_emit, PortType - -app = APIRouter() -db = SQLite("db/regextcpproxy.db",{ - 'services': { - 'status': 'VARCHAR(100) NOT NULL', - 'service_id': 'VARCHAR(100) PRIMARY KEY', - 'internal_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536)', - 'public_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536) UNIQUE', - 'name': 'VARCHAR(100) NOT NULL UNIQUE' - }, - 'regexes': { - 'regex': 'TEXT NOT NULL', - 'mode': 'VARCHAR(1) NOT NULL', - 'service_id': 'VARCHAR(100) NOT NULL', - 'is_blacklist': 'BOOLEAN NOT NULL CHECK (is_blacklist IN (0, 1))', - 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', - 'regex_id': 'INTEGER PRIMARY KEY', - 'is_case_sensitive' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1))', - 'active' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1)) DEFAULT 1', - 'FOREIGN KEY (service_id)':'REFERENCES services (service_id)', - }, - 'QUERY':[ - "CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,is_blacklist,mode,is_case_sensitive);" - ] -}) - -firewall = ProxyManager(db) - -async def reset(params: ResetRequest): - if not params.delete: - db.backup() - await firewall.close() - if params.delete: - db.delete() - db.init() - else: - db.restore() - await firewall.reload() - - -async def startup(): - db.init() - await firewall.reload() - -async def shutdown(): - db.backup() - await firewall.close() - db.disconnect() - db.restore() - -async def refresh_frontend(additional:list[str]=[]): - await socketio_emit(["regexproxy"]+additional) - -class GeneralStatModel(BaseModel): - closed:int - regexes: int - services: int - -@app.get('/stats', response_model=GeneralStatModel) -async def get_general_stats(): - """Get firegex general status about services""" - return db.query(""" - SELECT - (SELECT COALESCE(SUM(blocked_packets),0) FROM regexes) closed, - (SELECT COUNT(*) FROM regexes) regexes, - (SELECT COUNT(*) FROM services) services - """)[0] - -class ServiceModel(BaseModel): - id:str - status: str - public_port: PortType - internal_port: PortType - name: str - n_regex: int - n_packets: int - -@app.get('/services', response_model=list[ServiceModel]) -async def get_service_list(): - """Get the list of existent firegex services""" - return db.query(""" - SELECT - s.service_id `id`, - s.status status, - s.public_port public_port, - s.internal_port internal_port, - s.name name, - COUNT(r.regex_id) n_regex, - COALESCE(SUM(r.blocked_packets),0) n_packets - FROM services s LEFT JOIN regexes r ON r.service_id = s.service_id - GROUP BY s.service_id; - """) - -@app.get('/service/{service_id}', response_model=ServiceModel) -async def get_service_by_id(service_id: str): - """Get info about a specific service using his id""" - res = db.query(""" - SELECT - s.service_id `id`, - s.status status, - s.public_port public_port, - s.internal_port internal_port, - s.name name, - COUNT(r.regex_id) n_regex, - COALESCE(SUM(r.blocked_packets),0) n_packets - FROM services s LEFT JOIN regexes r ON r.service_id = s.service_id WHERE s.service_id = ? - GROUP BY s.service_id; - """, service_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") - return res[0] - -@app.get('/service/{service_id}/stop', response_model=StatusMessageModel) -async def service_stop(service_id: str): - """Request the stop of a specific service""" - await firewall.get(service_id).next(STATUS.STOP) - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/service/{service_id}/pause', response_model=StatusMessageModel) -async def service_pause(service_id: str): - """Request the pause of a specific service""" - await firewall.get(service_id).next(STATUS.PAUSE) - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/service/{service_id}/start', response_model=StatusMessageModel) -async def service_start(service_id: str): - """Request the start of a specific service""" - await firewall.get(service_id).next(STATUS.ACTIVE) - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/service/{service_id}/delete', response_model=StatusMessageModel) -async def service_delete(service_id: str): - """Request the deletion of a specific service""" - db.query('DELETE FROM services WHERE service_id = ?;', service_id) - db.query('DELETE FROM regexes WHERE service_id = ?;', service_id) - await firewall.remove(service_id) - await refresh_frontend() - return {'status': 'ok'} - - -@app.get('/service/{service_id}/regen-port', response_model=StatusMessageModel) -async def regen_service_port(service_id: str): - """Request the regeneration of a the internal proxy port of a specific service""" - db.query('UPDATE services SET internal_port = ? WHERE service_id = ?;', gen_internal_port(db), service_id) - await firewall.get(service_id).update_port() - await refresh_frontend() - return {'status': 'ok'} - -class ChangePortForm(BaseModel): - port: int|None = None - internalPort: int|None = None - -@app.post('/service/{service_id}/change-ports', response_model=StatusMessageModel) -async def change_service_ports(service_id: str, change_port:ChangePortForm): - """Choose and change the ports of the service""" - if change_port.port is None and change_port.internalPort is None: - raise HTTPException(status_code=400, detail="Invalid Request!") - try: - sql_inj = "" - query:list[str|int] = [] - if not change_port.port is None: - sql_inj+=" public_port = ? " - query.append(change_port.port) - if not change_port.port is None and not change_port.internalPort is None: - sql_inj += "," - if not change_port.internalPort is None: - sql_inj+=" internal_port = ? " - query.append(change_port.internalPort) - query.append(service_id) - db.query(f'UPDATE services SET {sql_inj} WHERE service_id = ?;', *query) - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="Port of the service has been already assigned to another service") - await firewall.get(service_id).update_port() - await refresh_frontend() - return {'status': 'ok'} - -class RegexModel(BaseModel): - regex:str - mode:str - id:int - service_id:str - is_blacklist: bool - n_packets:int - is_case_sensitive:bool - active:bool - -@app.get('/service/{service_id}/regexes', response_model=list[RegexModel]) -async def get_service_regexe_list(service_id: str): - """Get the list of the regexes of 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!") - return db.query(""" - SELECT - regex, mode, regex_id `id`, service_id, is_blacklist, - blocked_packets n_packets, is_case_sensitive, active - FROM regexes WHERE service_id = ?; - """, service_id) - -@app.get('/regex/{regex_id}', response_model=RegexModel) -async def get_regex_by_id(regex_id: int): - """Get regex info using his id""" - res = db.query(""" - SELECT - regex, mode, regex_id `id`, service_id, is_blacklist, - blocked_packets n_packets, is_case_sensitive, active - FROM regexes WHERE `id` = ?; - """, regex_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This regex does not exists!") - return res[0] - -@app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) -async def regex_delete(regex_id: int): - """Delete a regex using his id""" - res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) - if len(res) != 0: - db.query('DELETE FROM regexes WHERE regex_id = ?;', regex_id) - await firewall.get(res[0]["service_id"]).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/regex/{regex_id}/enable', response_model=StatusMessageModel) -async def regex_enable(regex_id: int): - """Request the enabling of a regex""" - res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) - if len(res) != 0: - db.query('UPDATE regexes SET active=1 WHERE regex_id = ?;', regex_id) - await firewall.get(res[0]["service_id"]).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/regex/{regex_id}/disable', response_model=StatusMessageModel) -async def regex_disable(regex_id: int): - """Request the deactivation of a regex""" - res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) - if len(res) != 0: - db.query('UPDATE regexes SET active=0 WHERE regex_id = ?;', regex_id) - await firewall.get(res[0]["service_id"]).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -class RegexAddForm(BaseModel): - service_id: str - regex: str - mode: str - active: bool|None = None - is_blacklist: bool - is_case_sensitive: bool - -@app.post('/regexes/add', response_model=StatusMessageModel) -async def add_new_regex(form: RegexAddForm): - """Add a new regex""" - try: - re.compile(b64decode(form.regex)) - except Exception: - raise HTTPException(status_code=400, detail="Invalid regex") - try: - db.query("INSERT INTO regexes (service_id, regex, is_blacklist, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?, ?);", - form.service_id, form.regex, form.is_blacklist, form.mode, form.is_case_sensitive, True if form.active is None else form.active ) - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="An identical regex already exists") - await firewall.get(form.service_id).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -class ServiceAddForm(BaseModel): - name: str - port: PortType - internalPort: int|None = None - -class ServiceAddStatus(BaseModel): - status:str - id: str|None = None - -class RenameForm(BaseModel): - name:str - -@app.post('/service/{service_id}/rename', response_model=StatusMessageModel) -async def service_rename(service_id: str, form: RenameForm): - """Request to change the name of a specific service""" - form.name = refactor_name(form.name) - if not form.name: raise HTTPException(status_code=400, detail="The name cannot be empty!") - try: - db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="The name is already used!") - await refresh_frontend() - return {'status': 'ok'} - -@app.post('/services/add', response_model=ServiceAddStatus) -async def add_new_service(form: ServiceAddForm): - """Add a new service""" - serv_id = gen_service_id(db) - form.name = refactor_name(form.name) - try: - internal_port = form.internalPort if form.internalPort else gen_internal_port(db) - db.query("INSERT INTO services (name, service_id, internal_port, public_port, status) VALUES (?, ?, ?, ?, ?)", - form.name, serv_id, internal_port, form.port, 'stop') - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="Name or/and ports of the service has been already assigned to another service") - await firewall.reload() - await refresh_frontend() - return {'status': 'ok', "id": serv_id } \ No newline at end of file diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py index fa3fe4f..bb8d985 100644 --- a/backend/utils/__init__.py +++ b/backend/utils/__init__.py @@ -1,10 +1,13 @@ import asyncio from ipaddress import ip_address, ip_interface -import os, socket, psutil, sys, nftables +import os +import socket +import psutil +import sys +import nftables from fastapi_socketio import SocketManager from fastapi import Path from typing import Annotated -import json LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) @@ -31,7 +34,8 @@ async def socketio_emit(elements:list[str]): def refactor_name(name:str): name = name.strip() - while " " in name: name = name.replace(" "," ") + while " " in name: + name = name.replace(" "," ") return name class SysctlManager: @@ -125,8 +129,10 @@ class NFTableManager(Singleton): def cmd(self, *cmds): code, out, err = self.raw_cmd(*cmds) - if code == 0: return out - else: raise Exception(err) + if code == 0: + return out + else: + raise Exception(err) def init(self): self.reset() @@ -138,8 +144,10 @@ class NFTableManager(Singleton): def list_rules(self, tables = None, chains = None): for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]: - if tables and filter["table"] not in tables: continue - if chains and filter["chain"] not in chains: continue + if tables and filter["table"] not in tables: + continue + if chains and filter["chain"] not in chains: + continue yield filter def raw_list(self): diff --git a/backend/utils/loader.py b/backend/utils/loader.py index 3b72b53..179c5d8 100644 --- a/backend/utils/loader.py +++ b/backend/utils/loader.py @@ -1,5 +1,6 @@ -import os, httpx +import os +import httpx from typing import Callable from fastapi import APIRouter from starlette.responses import StreamingResponse @@ -31,7 +32,8 @@ def frontend_deploy(app): return await frontend_debug_proxy(full_path) except Exception: return {"details":"Frontend not started at "+f"http://127.0.0.1:{os.getenv('F_PORT','5173')}"} - else: return await react_deploy(full_path) + else: + return await react_deploy(full_path) def list_routers(): return [ele[:-3] for ele in list_files(ROUTERS_DIR) if ele != "__init__.py" and " " not in ele and ele.endswith(".py")] @@ -79,9 +81,12 @@ def load_routers(app): if router.shutdown: shutdowns.append(router.shutdown) async def reset(reset_option:ResetRequest): - for func in resets: await run_func(func, reset_option) + for func in resets: + await run_func(func, reset_option) async def startup(): - for func in startups: await run_func(func) + for func in startups: + await run_func(func) async def shutdown(): - for func in shutdowns: await run_func(func) + for func in shutdowns: + await run_func(func) return reset, startup, shutdown diff --git a/backend/utils/sqlite.py b/backend/utils/sqlite.py index 426265c..430dbd6 100644 --- a/backend/utils/sqlite.py +++ b/backend/utils/sqlite.py @@ -1,4 +1,6 @@ -import json, sqlite3, os +import json +import sqlite3 +import os from hashlib import md5 class SQLite(): @@ -15,8 +17,10 @@ class SQLite(): self.conn = sqlite3.connect(self.db_name, check_same_thread = False) except Exception: path_name = os.path.dirname(self.db_name) - if not os.path.exists(path_name): os.makedirs(path_name) - with open(self.db_name, 'x'): pass + if not os.path.exists(path_name): + os.makedirs(path_name) + with open(self.db_name, 'x'): + pass self.conn = sqlite3.connect(self.db_name, check_same_thread = False) def dict_factory(cursor, row): d = {} @@ -36,13 +40,15 @@ class SQLite(): with open(self.db_name, "wb") as f: f.write(self.__backup) self.__backup = None - if were_active: self.connect() + if were_active: + self.connect() def delete_backup(self): self.__backup = None def disconnect(self) -> None: - if self.conn: self.conn.close() + if self.conn: + self.conn.close() self.conn = None def create_schema(self, tables = {}) -> None: @@ -50,9 +56,11 @@ class SQLite(): cur = self.conn.cursor() cur.execute("CREATE TABLE IF NOT EXISTS main.keys_values(key VARCHAR(100) PRIMARY KEY, value VARCHAR(100) NOT NULL);") for t in tables: - if t == "QUERY": continue + if t == "QUERY": + continue cur.execute('CREATE TABLE IF NOT EXISTS main.{}({});'.format(t, ''.join([(c + ' ' + tables[t][c] + ', ') for c in tables[t]])[:-2])) - if "QUERY" in tables: [cur.execute(qry) for qry in tables["QUERY"]] + if "QUERY" in tables: + [cur.execute(qry) for qry in tables["QUERY"]] cur.close() def query(self, query, *values): @@ -82,8 +90,10 @@ class SQLite(): raise e finally: cur.close() - try: self.conn.commit() - except Exception: pass + try: + self.conn.commit() + except Exception: + pass def delete(self): self.disconnect() @@ -92,7 +102,8 @@ class SQLite(): def init(self): self.connect() try: - if self.get('DB_VERSION') != self.DB_VER: raise Exception("DB_VERSION is not correct") + if self.get('DB_VERSION') != self.DB_VER: + raise Exception("DB_VERSION is not correct") except Exception: self.delete() self.connect() diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 0bc4166a510cb52a36a68de7020cdb0140832401..6fdb21bd0e8dabca0ac607aa680ed566544d0902 100755 GIT binary patch delta 24254 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 ZhnW30Wi}nc8*;6H#+?hT53>Qv{{un}o4x=5 diff --git a/frontend/package.json b/frontend/package.json index e8b633e..a5fdcdc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,25 +5,25 @@ "private": true, "dependencies": { "@hello-pangea/dnd": "^16.6.0", - "@mantine/core": "^7.13.2", - "@mantine/form": "^7.13.2", - "@mantine/hooks": "^7.13.2", - "@mantine/modals": "^7.13.2", - "@mantine/notifications": "^7.13.2", + "@mantine/core": "^7.16.2", + "@mantine/form": "^7.16.2", + "@mantine/hooks": "^7.16.2", + "@mantine/modals": "^7.16.2", + "@mantine/notifications": "^7.16.2", "@tanstack/react-query": "^4.36.1", "@types/jest": "^27.5.2", - "@types/node": "^20.16.11", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.1", + "@types/node": "^20.17.16", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", "buffer": "^6.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-icons": "^5.3.0", - "react-router-dom": "^6.27.0", - "socket.io-client": "^4.8.0", + "react-icons": "^5.4.0", + "react-router-dom": "^6.29.0", + "socket.io-client": "^4.8.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4", - "zustand": "^5.0.0-rc.2" + "zustand": "^5.0.3" }, "scripts": { "dev": "vite", @@ -50,8 +50,8 @@ }, "devDependencies": { "@tanstack/react-query-devtools": "^4.36.1", - "@vitejs/plugin-react": "^4.3.2", - "vite": "^4.5.5", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^4.5.9", "vite-plugin-svgr": "^3.3.0", "vite-tsconfig-paths": "^4.3.2" } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a027a7..fcfd39b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,9 +8,7 @@ import { PasswordSend, ServerStatusResponse } from './js/models'; import { DEV_IP_BACKEND, errorNotify, getstatus, HomeRedirector, IS_DEV, login, setpassword } from './js/utils'; import NFRegex from './pages/NFRegex'; import io from 'socket.io-client'; -import RegexProxy from './pages/RegexProxy'; import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails'; -import ServiceDetailsProxyRegex from './pages/RegexProxy/ServiceDetails'; import PortHijack from './pages/PortHijack'; import { Firewall } from './pages/Firewall'; import { useQueryClient } from '@tanstack/react-query'; @@ -150,9 +148,6 @@ function App() { } > } /> - } > - } /> - } /> } /> } /> diff --git a/frontend/src/components/AddNewRegex.tsx b/frontend/src/components/AddNewRegex.tsx index 623120a..0f846e1 100644 --- a/frontend/src/components/AddNewRegex.tsx +++ b/frontend/src/components/AddNewRegex.tsx @@ -1,15 +1,12 @@ -import { Button, Group, Space, TextInput, Notification, Switch, NativeSelect, Modal, Alert } from '@mantine/core'; +import { Button, Group, Space, TextInput, Notification, Switch, NativeSelect, Modal } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useState } from 'react'; import { RegexAddForm } from '../js/models'; import { b64decode, b64encode, getapiobject, okNotify } from '../js/utils'; import { ImCross } from "react-icons/im" -import FilterTypeSelector from './FilterTypeSelector'; -import { AiFillWarning } from 'react-icons/ai'; type RegexAddInfo = { regex:string, - type:string, mode:string, is_case_insensitive:boolean, deactive:boolean @@ -20,14 +17,12 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> const form = useForm({ initialValues: { regex:"", - type:"blacklist", mode:"C -> S", is_case_insensitive:false, deactive:false }, validate:{ regex: (value) => value !== "" ? null : "Regex is required", - type: (value) => ["blacklist","whitelist"].includes(value) ? null : "Invalid type", mode: (value) => ['C -> S', 'S -> C', 'C <-> S'].includes(value) ? null : "Invalid mode", } }) @@ -46,7 +41,6 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> const filter_mode = ({'C -> S':'C', 'S -> C':'S', 'C <-> S':'B'}[values.mode]) const request:RegexAddForm = { - is_blacklist:values.type !== "whitelist", is_case_sensitive: !values.is_case_insensitive, service_id: service, mode: filter_mode?filter_mode:"B", @@ -58,7 +52,7 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> if (!res){ setSubmitLoading(false) close(); - okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} ${request.is_blacklist?"blacklist":"whitelist"} regex to ${request.service_id} service`) + okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`) }else if (res.toLowerCase() === "invalid regex"){ setSubmitLoading(false) form.setFieldError("regex", "Invalid Regex") @@ -98,16 +92,6 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> variant="filled" {...form.getInputProps('mode')} /> - - - {form.values.type == "whitelist"?<> - }> - Using whitelist means that EVERY packet that doesn't match the regex will be DROPPED... In most cases this cause the service interruption. - :null} diff --git a/frontend/src/components/FilterTypeSelector.tsx b/frontend/src/components/FilterTypeSelector.tsx deleted file mode 100644 index aa331be..0000000 --- a/frontend/src/components/FilterTypeSelector.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Box, Center, SegmentedControl } from "@mantine/core"; -import { FaListAlt } from "react-icons/fa"; -import { TiCancel } from "react-icons/ti"; - -export default function FilterTypeSelector(props:any){ - return - - Blacklist - - ), - }, - { - value: 'whitelist', - label: ( -

- - Whitelist -
- ), - }, - ]} - {...props} - /> -} \ No newline at end of file diff --git a/frontend/src/components/NavBar/index.tsx b/frontend/src/components/NavBar/index.tsx index 6b06f16..456e12b 100644 --- a/frontend/src/components/NavBar/index.tsx +++ b/frontend/src/components/NavBar/index.tsx @@ -39,11 +39,6 @@ export default function NavBar() { } /> } /> } /> - - : } onClick={()=>setToggleState(!toggle)}/> - - } /> - diff --git a/frontend/src/components/PortHijack/utils.ts b/frontend/src/components/PortHijack/utils.ts index 1421dec..80875cc 100644 --- a/frontend/src/components/PortHijack/utils.ts +++ b/frontend/src/components/PortHijack/utils.ts @@ -1,6 +1,6 @@ import { ServerResponse } from "../../js/models" import { getapi, postapi } from "../../js/utils" -import { UseQueryOptions, useQuery } from "@tanstack/react-query" +import { useQuery } from "@tanstack/react-query" export type GeneralStats = { services:number diff --git a/frontend/src/components/RegexProxy/AddNewService.tsx b/frontend/src/components/RegexProxy/AddNewService.tsx deleted file mode 100644 index a9920be..0000000 --- a/frontend/src/components/RegexProxy/AddNewService.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Button, Group, Space, TextInput, Notification, Modal, Switch } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useState } from 'react'; -import { okNotify } from '../../js/utils'; -import { ImCross } from "react-icons/im" -import { regexproxy } from './utils'; -import PortInput from '../PortInput'; - -type ServiceAddForm = { - name:string, - port:number, - autostart: boolean, - chosenInternalPort: boolean, - internalPort: number -} - -function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { - - const form = useForm({ - initialValues: { - name:"", - port:8080, - internalPort:30001, - chosenInternalPort:false, - autostart: true - }, - validate:{ - name: (value) => value !== ""? null : "Service name is required", - port: (value) => (value>0 && value<65536) ? null : "Invalid port", - internalPort: (value) => (value>0 && value<65536) ? null : "Invalid internal port", - } - }) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = ({ name, port, autostart, chosenInternalPort, internalPort }:ServiceAddForm) =>{ - setSubmitLoading(true) - regexproxy.servicesadd(chosenInternalPort?{ internalPort, name, port }:{ name, port }).then( res => { - if (res.status === "ok"){ - setSubmitLoading(false) - close(); - if (autostart) regexproxy.servicestart(res.id) - okNotify(`Service ${name} has been added`, `Successfully added ${res.id} with port ${port}`) - }else{ - setSubmitLoading(false) - setError("Invalid request! [ "+res.status+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - } - - - return -
- - - - - - {form.values.chosenInternalPort?<> - - - - :null} - - - - - - - - - - - - - - - - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} - - -
- -} - -export default AddNewService; diff --git a/frontend/src/components/RegexProxy/ServiceRow/ChangePortModal.tsx b/frontend/src/components/RegexProxy/ServiceRow/ChangePortModal.tsx deleted file mode 100644 index a211831..0000000 --- a/frontend/src/components/RegexProxy/ServiceRow/ChangePortModal.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Button, Group, Space, Notification, Modal, Center, Title } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import React, { useEffect, useState } from 'react'; -import { ImCross } from "react-icons/im" -import { FaLongArrowAltDown } from 'react-icons/fa'; -import { regexproxy, Service } from '../utils'; -import { okNotify } from '../../../js/utils'; -import PortInput from '../../PortInput'; - -type InputForm = { - internalPort:number, - port:number -} - -function ChangePortModal({ service, opened, onClose }:{ service:Service, opened:boolean, onClose:()=>void }) { - - const form = useForm({ - initialValues: { - internalPort: service.internal_port, - port: service.public_port - }, - validate:{ - internalPort: (value) => (value>0 && value<65536) ? null : "Invalid internal port", - port: (value) => (value>0 && value<65536) ? null : "Invalid public port", - } - }) - - useEffect(()=>{ - form.setValues({internalPort: service.internal_port, port:service.public_port}) - },[opened]) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = (data:InputForm) =>{ - setSubmitLoading(true) - regexproxy.servicechangeport(service.id, data).then( res => { - if (!res){ - setSubmitLoading(false) - close(); - okNotify(`Internal port on ${service.name} service has changed in ${data.internalPort}`, `Successfully changed internal port of service with id ${service.id}`) - }else{ - setSubmitLoading(false) - setError("Invalid request! [ "+res+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - } - - - return -
- - - - -
- - - - - -
The change of the ports will cause a temporarily shutdown of the service! ⚠️
- - - - - - - - - - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} - - -
- -} - -export default ChangePortModal; diff --git a/frontend/src/components/RegexProxy/ServiceRow/RenameForm.tsx b/frontend/src/components/RegexProxy/ServiceRow/RenameForm.tsx deleted file mode 100644 index 6cb96e6..0000000 --- a/frontend/src/components/RegexProxy/ServiceRow/RenameForm.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 { regexproxy, Service } from '../utils'; - -function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:Service }) { - - const form = useForm({ - initialValues: { name:service.name }, - validate:{ name: (value) => value !== ""? null : "Service name is required" } - }) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - useEffect(()=> form.setFieldValue("name", service.name),[opened]) - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = ({ name }:{ name:string }) => { - setSubmitLoading(true) - regexproxy.servicerename(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.public_port}`) - }else{ - setSubmitLoading(false) - setError("Error: [ "+res+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - - } - - - return -
- - - - - - - - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} - - -
- -} - -export default RenameForm; diff --git a/frontend/src/components/RegexProxy/ServiceRow/index.tsx b/frontend/src/components/RegexProxy/ServiceRow/index.tsx deleted file mode 100644 index 3aa5cb0..0000000 --- a/frontend/src/components/RegexProxy/ServiceRow/index.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core'; -import { useState } from 'react'; -import { FaPause, FaPlay, FaStop } from 'react-icons/fa'; -import { MdOutlineArrowForwardIos } from "react-icons/md" -import YesNoModal from '../../YesNoModal'; -import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils'; -import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs'; -import { TbNumbers } from 'react-icons/tb'; -import { BiRename } from 'react-icons/bi' -import ChangePortModal from './ChangePortModal'; -import RenameForm from './RenameForm'; -import { regexproxy, Service } from '../utils'; -import { MenuDropDownWithButton } from '../../MainLayout'; - -//"status":"stop"/"wait"/"active"/"pause", -function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { - - let status_color = "gray"; - switch(service.status){ - case "stop": status_color = "red"; break; - case "wait": status_color = "yellow"; break; - case "active": status_color = "teal"; break; - case "pause": status_color = "cyan"; break; - } - - const [stopModal, setStopModal] = useState(false); - const [buttonLoading, setButtonLoading] = useState(false) - const [tooltipStopOpened, setTooltipStopOpened] = useState(false); - const [deleteModal, setDeleteModal] = useState(false) - const [changePortModal, setChangePortModal] = useState(false) - const [choosePortModal, setChoosePortModal] = useState(false) - const [renameModal, setRenameModal] = useState(false) - - const isMedium = isMediumScreen() - - const stopService = async () => { - setButtonLoading(true) - await regexproxy.servicestop(service.id).then(res => { - if(!res){ - okNotify(`Service ${service.id} stopped successfully!`,`The service ${service.name} has been stopped!`) - }else{ - errorNotify(`An error as occurred during the stopping of the service ${service.id}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the stopping of the service ${service.id}`,`Error: ${err}`) - }) - setButtonLoading(false); - } - - const startService = async () => { - setButtonLoading(true) - await regexproxy.servicestart(service.id).then(res => { - if(!res){ - okNotify(`Service ${service.id} started successfully!`,`The service ${service.name} has been started!`) - }else{ - errorNotify(`An error as occurred during the starting of the service ${service.id}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the starting of the service ${service.id}`,`Error: ${err}`) - }) - setButtonLoading(false) - } - - const pauseService = async () => { - setButtonLoading(true) - await regexproxy.servicepause(service.id).then(res => { - if(!res){ - okNotify(`Service ${service.id} paused successfully!`,`The service ${service.name} has been paused (Transparent mode)!`) - }else{ - errorNotify(`An error as occurred during the pausing of the service ${service.id}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the pausing of the service ${service.id}`,`Error: ${err}`) - }) - setButtonLoading(false) - - } - - const deleteService = () => { - regexproxy.servicedelete(service.id).then(res => { - if (!res){ - okNotify("Service delete complete!",`The service ${service.id} has been deleted!`) - }else - errorNotify("An error occurred while deleting a service",`Error: ${res}`) - }).catch(err => { - errorNotify("An error occurred while deleting a service",`Error: ${err}`) - }) - - } - - const changePort = () => { - regexproxy.serviceregenport(service.id).then(res => { - if (!res){ - okNotify("Service port regeneration completed!",`The service ${service.id} has changed the internal port!`) - }else - errorNotify("An error occurred while changing the internal service port",`Error: ${res}`) - }).catch(err => { - errorNotify("An error occurred while changing the internal service port",`Error: ${err}`) - }) - } - - return <> - - - - {service.name} :{service.public_port} - {service.internal_port} {"->"} {service.public_port} - - {!isMedium?:null} - - - - {!isMedium?:<>} - - Status: {service.status} - Regex: {service.n_regex} - Connections Blocked: {service.n_packets} - - {isMedium?:<>} - - - Rename service - } onClick={()=>setRenameModal(true)}>Change service name - - Change ports - } onClick={()=>setChoosePortModal(true)}>Change port - } onClick={()=>setChangePortModal(true)}>Regen proxy port - - Danger zone - } onClick={()=>setDeleteModal(true)}>Delete Service - - - {["pause","wait"].includes(service.status)? - - - setStopModal(true)} 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)}> - - - : - - - - - - } - - - - - - - - - - {onClick? - - - :null} - {!isMedium?<>:null} - - - - {setStopModal(false);}} - action={stopService} - opened={stopModal} - /> - - setDeleteModal(false) } - action={deleteService} - opened={deleteModal} - /> - setChangePortModal(false)} - action={changePort} - opened={changePortModal} - /> - setChoosePortModal(false)} - opened={choosePortModal} - /> - setRenameModal(false)} - opened={renameModal} - service={service} - /> - -} - -export default ServiceRow; diff --git a/frontend/src/components/RegexProxy/utils.ts b/frontend/src/components/RegexProxy/utils.ts deleted file mode 100644 index f7e882a..0000000 --- a/frontend/src/components/RegexProxy/utils.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { useQuery } from "@tanstack/react-query" -import { RegexAddForm, RegexFilter, ServerResponse } from "../../js/models" -import { getapi, postapi } from "../../js/utils" - -export type Service = { - id:string, - name:string, - status:string, - public_port:number, - internal_port:number, - n_packets:number, - n_regex:number, -} - -export type ServiceAddForm = { - name:string, - port:number, - internalPort?:number -} - -export type ServerResponseWithID = { - status:string, - id:string -} - -export type ChangePort = { - port?: number, - internalPort?: number -} - -export const serviceQueryKey = ["regexproxy","services"] - -export const regexproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:regexproxy.services}) -export const regexproxyServiceRegexesQuery = (service_id:string) => useQuery({ - queryKey:[...serviceQueryKey,service_id,"regexes"], - queryFn:() => regexproxy.serviceregexes(service_id) -}) - - -export const regexproxy = { - services: async() => { - return await getapi("regexproxy/services") as Service[]; - }, - serviceinfo: async (service_id:string) => { - return await getapi(`regexproxy/service/${service_id}`) as Service; - }, - regexdelete: async (regex_id:number) => { - const { status } = await getapi(`regexproxy/regex/${regex_id}/delete`) as ServerResponse; - return status === "ok"?undefined:status - }, - regexenable: async (regex_id:number) => { - const { status } = await getapi(`regexproxy/regex/${regex_id}/enable`) as ServerResponse; - return status === "ok"?undefined:status - }, - regexdisable: async (regex_id:number) => { - const { status } = await getapi(`regexproxy/regex/${regex_id}/disable`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicestart: async (service_id:string) => { - const { status } = await getapi(`regexproxy/service/${service_id}/start`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicestop: async (service_id:string) => { - const { status } = await getapi(`regexproxy/service/${service_id}/stop`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicepause: async (service_id:string) => { - const { status } = await getapi(`regexproxy/service/${service_id}/pause`) as ServerResponse; - return status === "ok"?undefined:status - }, - serviceregenport: async (service_id:string) => { - const { status } = await getapi(`regexproxy/service/${service_id}/regen-port`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicechangeport: async (service_id:string, data:ChangePort) => { - const { status } = await postapi(`regexproxy/service/${service_id}/change-ports`,data) as ServerResponse; - return status === "ok"?undefined:status - }, - servicesadd: async (data:ServiceAddForm) => { - return await postapi("regexproxy/services/add",data) as ServerResponseWithID; - }, - servicerename: async (service_id:string, name: string) => { - const { status } = await postapi(`regexproxy/service/${service_id}/rename`,{ name }) as ServerResponse; - return status === "ok"?undefined:status - }, - servicedelete: async (service_id:string) => { - const { status } = await getapi(`regexproxy/service/${service_id}/delete`) as ServerResponse; - return status === "ok"?undefined:status - }, - regexesadd: async (data:RegexAddForm) => { - const { status } = await postapi("regexproxy/regexes/add",data) as ServerResponse; - return status === "ok"?undefined:status - }, - serviceregexes: async (service_id:string) => { - return await getapi(`regexproxy/service/${service_id}/regexes`) as RegexFilter[]; - } -} \ No newline at end of file diff --git a/frontend/src/components/RegexView/index.tsx b/frontend/src/components/RegexView/index.tsx index 2c3d8dc..20b9d18 100644 --- a/frontend/src/components/RegexView/index.tsx +++ b/frontend/src/components/RegexView/index.tsx @@ -4,7 +4,6 @@ import { RegexFilter } from '../../js/models'; import { b64decode, errorNotify, getapiobject, okNotify } from '../../js/utils'; import { BsTrashFill } from "react-icons/bs" import YesNoModal from '../YesNoModal'; -import FilterTypeSelector from '../FilterTypeSelector'; import { FaPause, FaPlay } from 'react-icons/fa'; import { useClipboard } from '@mantine/hooks'; @@ -74,12 +73,6 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { - Service: {regexInfo.service_id} diff --git a/frontend/src/js/models.ts b/frontend/src/js/models.ts index 43fff7a..3a78c15 100644 --- a/frontend/src/js/models.ts +++ b/frontend/src/js/models.ts @@ -47,7 +47,6 @@ export type RegexAddForm = { service_id:string, regex:string, is_case_sensitive:boolean, - is_blacklist:boolean, mode:string, // C->S S->C BOTH, active: boolean } \ No newline at end of file diff --git a/frontend/src/js/utils.tsx b/frontend/src/js/utils.tsx index 60af1ed..159d6d6 100644 --- a/frontend/src/js/utils.tsx +++ b/frontend/src/js/utils.tsx @@ -3,7 +3,6 @@ import { ImCross } from "react-icons/im"; import { TiTick } from "react-icons/ti" import { Navigate } from "react-router-dom"; import { nfregex } from "../components/NFRegex/utils"; -import { regexproxy } from "../components/RegexProxy/utils"; import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models"; import { Buffer } from "buffer" import { QueryClient, useQuery } from "@tanstack/react-query"; @@ -111,8 +110,6 @@ export function getapiobject(){ switch(getMainPath()){ case "nfregex": return nfregex - case "regexproxy": - return regexproxy } throw new Error('No api for this tool!'); } diff --git a/frontend/src/pages/RegexProxy/ServiceDetails.tsx b/frontend/src/pages/RegexProxy/ServiceDetails.tsx deleted file mode 100644 index d539e89..0000000 --- a/frontend/src/pages/RegexProxy/ServiceDetails.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; -import { useState } from 'react'; -import { Navigate, useParams } from 'react-router-dom'; -import { BsPlusLg } from "react-icons/bs"; -import { regexproxyServiceQuery, regexproxyServiceRegexesQuery } from '../../components/RegexProxy/utils'; -import ServiceRow from '../../components/RegexProxy/ServiceRow'; -import AddNewRegex from '../../components/AddNewRegex'; -import RegexView from '../../components/RegexView'; - -function ServiceDetailsProxyRegex() { - - const {srv} = useParams() - const [open, setOpen] = useState(false) - const services = regexproxyServiceQuery() - const serviceInfo = services.data?.find(s => s.id == srv) - const [tooltipAddRegexOpened, setTooltipAddRegexOpened] = useState(false) - const regexesList = regexproxyServiceRegexesQuery(srv??"") - - if (!srv || !serviceInfo || regexesList.isError) return - - return - - - {(!regexesList.data || regexesList.data.length == 0)?<> - - No regex found for this service! Add one by clicking the "+" buttons - - - - setOpen(true)} size="xl" radius="md" variant="filled" - aria-describedby="tooltip-AddRegex-id" - onFocus={() => setTooltipAddRegexOpened(false)} onBlur={() => setTooltipAddRegexOpened(false)} - onMouseEnter={() => setTooltipAddRegexOpened(true)} onMouseLeave={() => setTooltipAddRegexOpened(false)}> - - - : - - {regexesList.data.map( (regexInfo) => )} - - } - - {srv? {setOpen(false)}} service={srv} />:null} - - - -} - -export default ServiceDetailsProxyRegex; diff --git a/frontend/src/pages/RegexProxy/index.tsx b/frontend/src/pages/RegexProxy/index.tsx deleted file mode 100644 index 4ddfe31..0000000 --- a/frontend/src/pages/RegexProxy/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { ActionIcon, Badge, Box, LoadingOverlay, Space, 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/RegexProxy/ServiceRow'; -import { regexproxyServiceQuery } from '../../components/RegexProxy/utils'; -import { errorNotify, getErrorMessage } from '../../js/utils'; -import AddNewService from '../../components/RegexProxy/AddNewService'; -import AddNewRegex from '../../components/AddNewRegex'; -import { useQueryClient } from '@tanstack/react-query'; -import { TbReload } from 'react-icons/tb'; - - -function RegexProxy({ children }: { children: any }) { - - const navigator = useNavigate() - const [open, setOpen] = useState(false); - const {srv} = useParams() - const [tooltipAddServOpened, setTooltipAddServOpened] = useState(false); - const [tooltipAddOpened, setTooltipAddOpened] = useState(false); - const queryClient = useQueryClient() - const [tooltipRefreshOpened, setTooltipRefreshOpened] = useState(false); - - const services = regexproxyServiceQuery() - - useEffect(()=> { - if(services.isError){ - errorNotify("RegexProxy Update failed!", getErrorMessage(services.error)) - } - },[services.isError]) - - const closeModal = () => {setOpen(false);} - - return <> - - - TCP Proxy Regex Filter (IPv4 Only) - - Services: {services.isLoading?0:services.data?.length} - - Filtered Connections: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)} - - Regexes: {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)} - - { srv? - - setOpen(true)} size="lg" radius="md" variant="filled" - onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)} - onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}> - - : - setOpen(true)} size="lg" radius="md" variant="filled" - onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)} - onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}> - - } - - - queryClient.invalidateQueries(["regexproxy"])} size="lg" radius="md" variant="filled" - loading={services.isFetching} - onFocus={() => setTooltipRefreshOpened(false)} onBlur={() => setTooltipRefreshOpened(false)} - onMouseEnter={() => setTooltipRefreshOpened(true)} onMouseLeave={() => setTooltipRefreshOpened(false)}> - - - - {srv?null:<> - - {(services.data && services.data?.length > 0)?services.data.map( srv => { - navigator("/regexproxy/"+srv.id) - }} />):<> No services found! Add one clicking the "+" buttons - - - - setOpen(true)} size="xl" radius="md" variant="filled" - onFocus={() => setTooltipAddServOpened(false)} onBlur={() => setTooltipAddServOpened(false)} - onMouseEnter={() => setTooltipAddServOpened(true)} onMouseLeave={() => setTooltipAddServOpened(false)}> - - - } - - } - - {srv?children:null} - {srv? - : - - } - -} - -export default RegexProxy; diff --git a/tests/api_test.py b/tests/api_test.py index f21f28d..ccc3097 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -1,60 +1,95 @@ #!/usr/bin/env python3 -from utils.colors import * -from utils.firegexapi import * -import argparse, secrets +from utils.colors import colors, puts, sep +from utils.firegexapi import FiregexAPI +import argparse +import secrets parser = argparse.ArgumentParser() parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password') args = parser.parse_args() sep() -puts(f"Testing will start on ", color=colors.cyan, end="") +puts("Testing will start on ", color=colors.cyan, end="") puts(f"{args.address}", color=colors.yellow) firegex = FiregexAPI(args.address) #Connect to Firegex if firegex.status()["status"] =="init": - if (firegex.set_password(args.password)): puts(f"Sucessfully set password to {args.password} ✔", color=colors.green) - else: puts(f"Test Failed: Unknown response or password already put ✗", color=colors.red); exit(1) + if (firegex.set_password(args.password)): + puts(f"Sucessfully set password to {args.password} ✔", color=colors.green) + else: + puts("Test Failed: Unknown response or password already put ✗", color=colors.red) + exit(1) else: - if (firegex.login(args.password)): puts(f"Sucessfully logged in ✔", color=colors.green) - else: puts(f"Test Failed: Unknown response or wrong passowrd ✗", color=colors.red); exit(1) + if (firegex.login(args.password)): + puts("Sucessfully logged in ✔", color=colors.green) + else: + puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red) + exit(1) -if(firegex.status()["loggined"]): puts(f"Correctly received status ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or not logged in✗", color=colors.red); exit(1) +if(firegex.status()["loggined"]): + puts("Correctly received status ✔", color=colors.green) +else: + puts("Test Failed: Unknown response or not logged in✗", color=colors.red) + exit(1) #Prepare second instance firegex2 = FiregexAPI(args.address) -if (firegex2.login(args.password)): puts(f"Sucessfully logged in on second instance ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red); exit(1) +if (firegex2.login(args.password)): + puts("Sucessfully logged in on second instance ✔", color=colors.green) +else: + puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red) + exit(1) -if(firegex2.status()["loggined"]): puts(f"Correctly received status on second instance✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or not logged in on second instance✗", color=colors.red); exit(1) +if(firegex2.status()["loggined"]): + puts("Correctly received status on second instance✔", color=colors.green) +else: + puts("Test Failed: Unknown response or not logged in on second instance✗", color=colors.red) + exit(1) #Change password new_password = secrets.token_hex(10) -if (firegex.change_password(new_password,expire=True)): puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green) -else: puts(f"Test Failed: Coundl't change the password ✗", color=colors.red); exit(1) +if (firegex.change_password(new_password,expire=True)): + puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green) +else: + puts("Test Failed: Coundl't change the password ✗", color=colors.red) + exit(1) #Check if we are still logged in -if(firegex.status()["loggined"]): puts(f"Correctly received status after password change ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or not logged after password change ✗", color=colors.red); exit(1) +if(firegex.status()["loggined"]): + puts("Correctly received status after password change ✔", color=colors.green) +else: + puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red) + exit(1) #Check if second session expired and relog -if(not firegex2.status()["loggined"]): puts(f"Second instance was expired currectly ✔", color=colors.green) -else: puts(f"Test Failed: Still logged in on second instance, expire expected ✗", color=colors.red); exit(1) -if (firegex2.login(new_password)): puts(f"Sucessfully logged in on second instance ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red); exit(1) +if(not firegex2.status()["loggined"]): + puts("Second instance was expired currectly ✔", color=colors.green) +else: + puts("Test Failed: Still logged in on second instance, expire expected ✗", color=colors.red) + exit(1) +if (firegex2.login(new_password)): + puts("Sucessfully logged in on second instance ✔", color=colors.green) +else: + puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red) + exit(1) #Change it back -if (firegex.change_password(args.password,expire=False)): puts(f"Sucessfully restored the password ✔", color=colors.green) -else: puts(f"Test Failed: Coundl't change the password ✗", color=colors.red); exit(1) +if (firegex.change_password(args.password,expire=False)): + puts("Sucessfully restored the password ✔", color=colors.green) +else: + puts("Test Failed: Coundl't change the password ✗", color=colors.red) + exit(1) #Check if we are still logged in -if(firegex2.status()["loggined"]): puts(f"Correctly received status after password change ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or not logged after password change ✗", color=colors.red); exit(1) +if(firegex2.status()["loggined"]): + puts("Correctly received status after password change ✔", color=colors.green) +else: + puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red) + exit(1) puts("List of available interfaces:", color=colors.yellow) -for interface in firegex.get_interfaces(): puts("name: {}, address: {}".format(interface["name"], interface["addr"]), color=colors.yellow) \ No newline at end of file +for interface in firegex.get_interfaces(): + puts("name: {}, address: {}".format(interface["name"], interface["addr"]), color=colors.yellow) \ No newline at end of file diff --git a/tests/benchmark.py b/tests/benchmark.py index 6625b53..0ff5a63 100644 --- a/tests/benchmark.py +++ b/tests/benchmark.py @@ -1,24 +1,25 @@ #!/usr/bin/env python3 -from utils.colors import * -from utils.firegexapi import * -from utils.tcpserver import * +from utils.colors import colors, puts, sep +from utils.firegexapi import FiregexAPI from multiprocessing import Process from time import sleep -import iperf3, csv, argparse, base64, secrets +import iperf3 +import csv +import argparse +import base64 +import secrets #TODO: make it work with Proxy and not only netfilter parser = argparse.ArgumentParser() parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") parser.add_argument("--port", "-P", type=int , required=False, help='Port of the Benchmark service', default=1337) -parser.add_argument("--internal-port", "-I", type=int , required=False, help='Internal port of the Benchmark service', default=1338) parser.add_argument("--service-name", "-n", type=str , required=False, help='Name of the Benchmark service', default="Benchmark Service") parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password') parser.add_argument("--num-of-regexes", "-r", type=int, required=True, help='Number of regexes to benchmark with') parser.add_argument("--duration", "-d", type=int, required=False, help='Duration of the Benchmark in seconds', default=5) parser.add_argument("--output-file", "-o", type=str, required=False, help='Output results csv file', default="benchmark.csv") parser.add_argument("--num-of-streams", "-s", type=int, required=False, help='Output results csv file', default=1) -parser.add_argument("--mode", "-m" , type=str, required=True, choices=["netfilter","proxy"], help='Type of filtering') args = parser.parse_args() sep() @@ -28,22 +29,36 @@ puts(f"{args.address}", color=colors.yellow) firegex = FiregexAPI(args.address) #Connect to Firegex -if (firegex.login(args.password)): puts(f"Sucessfully logged in ✔", color=colors.green) -else: puts(f"Benchmark Failed: Unknown response or wrong passowrd ✗", color=colors.red); exit(1) +if (firegex.login(args.password)): + puts("Sucessfully logged in ✔", color=colors.green) +else: + puts("Benchmark Failed: Unknown response or wrong passowrd ✗", color=colors.red) + exit(1) + +def exit_test(code): + if service_id: + server.stop() + if(firegex.nf_delete_service(service_id)): + puts("Sucessfully deleted service ✔", color=colors.green) + else: + puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red) + exit_test(1) + exit(code) #Create new Service -if args.mode == "netfilter": - service_id = firegex.nf_add_service(args.service_name, args.port, "tcp", "127.0.0.1/24") + +service_id = firegex.nf_add_service(args.service_name, args.port, "tcp", "127.0.0.1/24") +if service_id: + puts(f"Sucessfully created service {service_id} ✔", color=colors.green) else: - service_id = firegex.px_add_service(args.service_name, args.port, internalPort=args.internal_port) -if service_id: puts(f"Sucessfully created service {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Failed to create service ✗", color=colors.red); exit(1) + puts("Test Failed: Failed to create service ✗", color=colors.red) + exit(1) #Start iperf3 def startServer(): server = iperf3.Server() server.bind_address = '127.0.0.1' - server.port = args.port if args.mode == "netfilter" else args.internal_port + server.port = args.port server.verbose = False while True: server.run() @@ -63,27 +78,29 @@ sleep(1) #Get baseline reading -puts(f"Baseline without proxy: ", color=colors.blue, end='') -print(f"{getReading(args.port if args.mode == 'netfilter' else args.internal_port)} MB/s") +puts("Baseline without proxy: ", color=colors.blue, end='') +print(f"{getReading(args.port)} MB/s") #Start firewall -if(firegex.nf_start_service(service_id) if args.mode == "netfilter" else firegex.px_start_service(service_id)): - puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) +if firegex.nf_start_service(service_id): + puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) else: - puts(f"Benchmark Failed: Coulnd't start the service ✗", color=colors.red); exit_test(1) + puts("Benchmark Failed: Coulnd't start the service ✗", color=colors.red) + exit_test(1) #Get no regexs reading results = [] -puts(f"Performance with no regexes: ", color=colors.yellow , end='') +puts("Performance with no regexes: ", color=colors.yellow , end='') results.append(getReading(args.port)) print(f"{results[0]} MB/s") #Add all the regexs for i in range(1,args.num_of_regexes+1): regex = base64.b64encode(bytes(secrets.token_hex(16).encode())).decode() - if(not (firegex.nf_add_regex if args.mode == "netfilter" else firegex.px_add_regex)(service_id,regex,"B",active=True,is_blacklist=True,is_case_sensitive=False) ): - puts(f"Benchmark Failed: Coulnd't add the regex ✗", color=colors.red); exit_test(1) + if not firegex.nf_add_regex(service_id,regex,"B",active=True,is_blacklist=True,is_case_sensitive=False): + puts("Benchmark Failed: Coulnd't add the regex ✗", color=colors.red) + exit_test(1) puts(f"Performance with {i} regex(s): ", color=colors.red, end='') results.append(getReading(args.port)) print(f"{results[i]} MB/s") @@ -96,9 +113,10 @@ with open(args.output_file,'w') as f: puts(f"Sucessfully written results to {args.output_file} ✔", color=colors.magenta) #Delete the Service -if(firegex.nf_delete_service(service_id) if args.mode == "netfilter" else firegex.px_delete_service(service_id)): +if firegex.nf_delete_service(service_id): puts(f"Sucessfully delete service with id {service_id} ✔", color=colors.green) else: - puts(f"Test Failed: Couldn't delete service ✗", color=colors.red); exit(1) + puts("Test Failed: Couldn't delete service ✗", color=colors.red) + exit(1) server.terminate() \ No newline at end of file diff --git a/tests/nf_test.py b/tests/nf_test.py index 67c0046..7ff0405 100644 --- a/tests/nf_test.py +++ b/tests/nf_test.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 -from utils.colors import * -from utils.firegexapi import * +from utils.colors import colors, puts, sep +from utils.firegexapi import FiregexAPI from utils.tcpserver import TcpServer from utils.udpserver import UdpServer -import argparse, secrets, base64,time +import argparse +import secrets +import base64 +import time parser = argparse.ArgumentParser() parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") @@ -15,14 +18,17 @@ parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp"," args = parser.parse_args() sep() -puts(f"Testing will start on ", color=colors.cyan, end="") +puts("Testing will start on ", color=colors.cyan, end="") puts(f"{args.address}", color=colors.yellow) firegex = FiregexAPI(args.address) #Login -if (firegex.login(args.password)): puts(f"Sucessfully logged in ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or wrong passowrd ✗", color=colors.red); exit(1) +if (firegex.login(args.password)): + puts("Sucessfully logged in ✔", color=colors.green) +else: + puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red) + exit(1) #Create server server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port,ipv6=args.ipv6) @@ -30,30 +36,42 @@ server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port,ipv6=args.i def exit_test(code): if service_id: server.stop() - if(firegex.nf_delete_service(service_id)): puts(f"Sucessfully deleted service ✔", color=colors.green) - else: puts(f"Test Failed: Coulnd't delete serivce ✗", color=colors.red); exit_test(1) + if(firegex.nf_delete_service(service_id)): + puts("Sucessfully deleted service ✔", color=colors.green) + else: + puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red) + exit_test(1) exit(code) service_id = firegex.nf_add_service(args.service_name, args.port, args.proto , "::1" if args.ipv6 else "127.0.0.1" ) -if service_id: puts(f"Sucessfully created service {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Failed to create service ✗", color=colors.red); exit(1) +if service_id: + puts("Sucessfully created service {service_id} ✔", color=colors.green) +else: + puts("Test Failed: Failed to create service ✗", color=colors.red) + exit(1) -if(firegex.nf_start_service(service_id)): puts(f"Sucessfully started service ✔", color=colors.green) -else: puts(f"Test Failed: Failed to start service ✗", color=colors.red); exit_test(1) +if(firegex.nf_start_service(service_id)): + puts("Sucessfully started service ✔", color=colors.green) +else: + puts("Test Failed: Failed to start service ✗", color=colors.red) + exit_test(1) server.start() time.sleep(0.5) if server.sendCheckData(secrets.token_bytes(432)): - puts(f"Successfully tested first proxy with no regex ✔", color=colors.green) + puts("Successfully tested first proxy with no regex ✔", color=colors.green) else: - puts(f"Test Failed: Data was corrupted ", color=colors.red); exit_test(1) + puts("Test Failed: Data was corrupted ", color=colors.red) + exit_test(1) #Add new regex secret = bytes(secrets.token_hex(16).encode()) regex = base64.b64encode(secret).decode() -if(firegex.nf_add_regex(service_id,regex,"B",active=True,is_blacklist=True,is_case_sensitive=True)): +if firegex.nf_add_regex(service_id,regex,"B",active=True,is_blacklist=True,is_case_sensitive=True): puts(f"Sucessfully added regex {str(secret)} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't add the regex {str(secret)} ✗", color=colors.red); exit_test(1) +else: + puts("Test Failed: Coulnd't add the regex {str(secret)} ✗", color=colors.red) + exit_test(1) #Check if regex is present in the service n_blocked = 0 @@ -66,35 +84,45 @@ def checkRegex(regex, should_work=True, upper=False): #Test the regex s = base64.b64decode(regex).upper() if upper else base64.b64decode(regex) if not server.sendCheckData(secrets.token_bytes(200) + s + secrets.token_bytes(200)): - puts(f"The malicious request was successfully blocked ✔", color=colors.green) + puts("The malicious request was successfully blocked ✔", color=colors.green) n_blocked += 1 time.sleep(1) if firegex.nf_get_regex(r["id"])["n_packets"] == n_blocked: - puts(f"The packed was reported as blocked ✔", color=colors.green) + puts("The packed was reported as blocked ✔", color=colors.green) else: - puts(f"Test Failed: The packed wasn't reported as blocked ✗", color=colors.red); exit_test(1) + puts("Test Failed: The packed wasn't reported as blocked ✗", color=colors.red) + exit_test(1) else: - puts(f"Test Failed: The request wasn't blocked ✗", color=colors.red);exit_test(1) + puts("Test Failed: The request wasn't blocked ✗", color=colors.red) + exit_test(1) return - puts(f"Test Failed: The regex wasn't found ✗", color=colors.red); exit_test(1) + puts("Test Failed: The regex wasn't found ✗", color=colors.red) + exit_test(1) else: if server.sendCheckData(secrets.token_bytes(200) + base64.b64decode(regex) + secrets.token_bytes(200)): - puts(f"The request wasn't blocked ✔", color=colors.green) + puts("The request wasn't blocked ✔", color=colors.green) else: - puts(f"Test Failed: The request was blocked when it shouldn't have", color=colors.red); exit_test(1) + puts("Test Failed: The request was blocked when it shouldn't have", color=colors.red) + exit_test(1) checkRegex(regex) #Pause the proxy -if(firegex.nf_stop_service(service_id)): puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't pause the service ✗", color=colors.red); exit_test(1) +if(firegex.nf_stop_service(service_id)): + puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green) +else: + puts("Test Failed: Coulnd't pause the service ✗", color=colors.red) + exit_test(1) #Check if it's actually paused checkRegex(regex,should_work=False) #Start firewall -if(firegex.nf_start_service(service_id)): puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't start the service ✗", color=colors.red); exit_test(1) +if(firegex.nf_start_service(service_id)): + puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) +else: + puts("Test Failed: Coulnd't start the service ✗", color=colors.red) + exit_test(1) checkRegex(regex) @@ -104,7 +132,8 @@ for r in firegex.nf_get_service_regexes(service_id): if(firegex.nf_disable_regex(r["id"])): puts(f"Sucessfully disabled regex with id {r['id']} ✔", color=colors.green) else: - puts(f"Test Failed: Coulnd't disable the regex ✗", color=colors.red); exit_test(1) + puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red) + exit_test(1) break #Check if it's actually disabled @@ -116,7 +145,8 @@ for r in firegex.nf_get_service_regexes(service_id): if(firegex.nf_enable_regex(r["id"])): puts(f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green) else: - puts(f"Test Failed: Coulnd't enable the regex ✗", color=colors.red); exit_test(1) + puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red) + exit_test(1) break checkRegex(regex) @@ -128,7 +158,8 @@ for r in firegex.nf_get_service_regexes(service_id): if(firegex.nf_delete_regex(r["id"])): puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green) else: - puts(f"Test Failed: Coulnd't delete the regex ✗", color=colors.red); exit_test(1) + puts("Test Failed: Coulnd't delete the regex ✗", color=colors.red) + exit_test(1) break #Check if it's actually deleted @@ -137,7 +168,9 @@ checkRegex(regex,should_work=False) #Add case insensitive regex if(firegex.nf_add_regex(service_id,regex,"B",active=True,is_blacklist=True,is_case_sensitive=False)): puts(f"Sucessfully added case insensitive regex {str(secret)} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗", color=colors.red); exit_test(1) +else: + puts(f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗", color=colors.red) + exit_test(1) checkRegex(regex,upper=True) checkRegex(regex) @@ -149,36 +182,22 @@ for r in firegex.nf_get_service_regexes(service_id): if(firegex.nf_delete_regex(r["id"])): puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green) else: - puts(f"Test Failed: Coulnd't delete the regex ✗", color=colors.red); exit_test(1) - break - -#Add whitelist regex -if(firegex.nf_add_regex(service_id,regex,"B",active=True,is_blacklist=False,is_case_sensitive=True)): - puts(f"Sucessfully added case whitelist regex {str(secret)} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't add the case whiteblist regex {str(secret)} ✗", color=colors.red); exit_test(1) - -checkRegex(regex,should_work=False) -checkRegex(regex,upper=True) #Dirty way to test the whitelist :p - -#Delete regex -n_blocked = 0 -for r in firegex.nf_get_service_regexes(service_id): - if r["regex"] == regex: - if(firegex.nf_delete_regex(r["id"])): - puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green) - else: - puts(f"Test Failed: Coulnd't delete the regex ✗", color=colors.red); exit_test(1) + puts("Test Failed: Coulnd't delete the regex ✗", color=colors.red) + exit_test(1) break #Rename service -if(firegex.nf_rename_service(service_id,f"{args.service_name}2")): puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't rename service ✗", color=colors.red); exit_test(1) +if(firegex.nf_rename_service(service_id,f"{args.service_name}2")): + puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green) +else: + puts("Test Failed: Coulnd't rename service ✗", color=colors.red) + exit_test(1) #Check if service was renamed correctly for services in firegex.nf_get_services(): if services["name"] == f"{args.service_name}2": - puts(f"Checked that service was renamed correctly ✔", color=colors.green) + puts("Checked that service was renamed correctly ✔", color=colors.green) exit_test(0) -puts(f"Test Failed: Service wasn't renamed correctly ✗", color=colors.red); exit_test(1) +puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red) exit_test(1) diff --git a/tests/ph_test.py b/tests/ph_test.py index 02cf222..98bae5a 100644 --- a/tests/ph_test.py +++ b/tests/ph_test.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -from utils.colors import * -from utils.firegexapi import * +from utils.colors import colors, puts, sep +from utils.firegexapi import FiregexAPI from utils.tcpserver import TcpServer from utils.udpserver import UdpServer -import argparse, secrets, base64,time +import argparse +import secrets +import time parser = argparse.ArgumentParser() parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") @@ -15,14 +17,17 @@ parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp"," args = parser.parse_args() sep() -puts(f"Testing will start on ", color=colors.cyan, end="") +puts("Testing will start on ", color=colors.cyan, end="") puts(f"{args.address}", color=colors.yellow) firegex = FiregexAPI(args.address) #Login -if (firegex.login(args.password)): puts(f"Sucessfully logged in ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or wrong passowrd ✗", color=colors.red); exit(1) +if (firegex.login(args.password)): + puts("Sucessfully logged in ✔", color=colors.green) +else: + puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red) + exit(1) #Create server server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+1,ipv6=args.ipv6,proxy_port=args.port) @@ -30,17 +35,26 @@ server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+1,ipv6=args def exit_test(code): if service_id: server.stop() - if(firegex.ph_delete_service(service_id)): puts(f"Sucessfully deleted service ✔", color=colors.green) - else: puts(f"Test Failed: Coulnd't delete serivce ✗", color=colors.red); exit_test(1) + if(firegex.ph_delete_service(service_id)): + puts("Sucessfully deleted service ✔", color=colors.green) + else: + puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red) + exit_test(1) exit(code) #Create and start serivce service_id = firegex.ph_add_service(args.service_name, args.port, args.port+1, args.proto , "::1" if args.ipv6 else "127.0.0.1", "::1" if args.ipv6 else "127.0.0.1") -if service_id: puts(f"Sucessfully created service {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Failed to create service ✗", color=colors.red); exit(1) +if service_id: + puts("Sucessfully created service {service_id} ✔", color=colors.green) +else: + puts("Test Failed: Failed to create service ✗", color=colors.red) + exit(1) -if(firegex.ph_start_service(service_id)): puts(f"Sucessfully started service ✔", color=colors.green) -else: puts(f"Test Failed: Failed to start service ✗", color=colors.red); exit_test(1) +if(firegex.ph_start_service(service_id)): + puts("Sucessfully started service ✔", color=colors.green) +else: + puts("Test Failed: Failed to start service ✗", color=colors.red) + exit_test(1) server.start() time.sleep(0.5) @@ -48,33 +62,49 @@ time.sleep(0.5) #Check if it started def checkData(should_work): res = None - try: res = server.sendCheckData(secrets.token_bytes(432)) - except (ConnectionRefusedError, TimeoutError): res = None + try: + res = server.sendCheckData(secrets.token_bytes(432)) + except (ConnectionRefusedError, TimeoutError): + res = None if res: - if should_work: puts(f"Successfully received data ✔", color=colors.green) - else: puts("Test Failed: Connection wasn't blocked ✗", color=colors.red); exit_test(1) + if should_work: + puts("Successfully received data ✔", color=colors.green) + else: + puts("Test Failed: Connection wasn't blocked ✗", color=colors.red) + exit_test(1) else: - if should_work: puts(f"Test Failed: Data wans't received ✗", color=colors.red); exit_test(1) - else: puts(f"Successfully blocked connection ✔", color=colors.green) + if should_work: + puts("Test Failed: Data wans't received ✗", color=colors.red) + exit_test(1) + else: + puts("Successfully blocked connection ✔", color=colors.green) checkData(True) #Pause the proxy -if(firegex.ph_stop_service(service_id)): puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't pause the service ✗", color=colors.red); exit_test(1) +if(firegex.ph_stop_service(service_id)): + puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green) +else: + puts("Test Failed: Coulnd't pause the service ✗", color=colors.red) + exit_test(1) checkData(False) #Start firewall -if(firegex.ph_start_service(service_id)): puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't start the service ✗", color=colors.red); exit_test(1) +if(firegex.ph_start_service(service_id)): + puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) +else: + puts("Test Failed: Coulnd't start the service ✗", color=colors.red) + exit_test(1) checkData(True) #Change port if(firegex.ph_change_destination(service_id, "::1" if args.ipv6 else "127.0.0.1", args.port+2)): - puts(f"Sucessfully changed port ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't change destination ✗", color=colors.red); exit_test(1) + puts("Sucessfully changed port ✔", color=colors.green) +else: + puts("Test Failed: Coulnd't change destination ✗", color=colors.red) + exit_test(1) checkData(False) @@ -86,14 +116,17 @@ time.sleep(0.5) checkData(True) #Rename service -if(firegex.ph_rename_service(service_id,f"{args.service_name}2")): puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't rename service ✗", color=colors.red); exit_test(1) +if(firegex.ph_rename_service(service_id,f"{args.service_name}2")): + puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green) +else: + puts("Test Failed: Coulnd't rename service ✗", color=colors.red) + exit_test(1) #Check if service was renamed correctly for services in firegex.ph_get_services(): if services["name"] == f"{args.service_name}2": - puts(f"Checked that service was renamed correctly ✔", color=colors.green) + puts("Checked that service was renamed correctly ✔", color=colors.green) exit_test(0) -puts(f"Test Failed: Service wasn't renamed correctly ✗", color=colors.red); exit_test(1) +puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red) exit_test(1) diff --git a/tests/px_test.py b/tests/px_test.py deleted file mode 100644 index 46eccf9..0000000 --- a/tests/px_test.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/env python3 -from utils.colors import * -from utils.firegexapi import * -from utils.tcpserver import TcpServer -import argparse, secrets, base64,time,random - -parser = argparse.ArgumentParser() -parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") -parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password') -parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service") -parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337) -args = parser.parse_args() -sep() -puts(f"Testing will start on ", color=colors.cyan, end="") -puts(f"{args.address}", color=colors.yellow) - -#Create and start server -server = TcpServer(args.port,ipv6=False) -server.start() -time.sleep(0.5) - -firegex = FiregexAPI(args.address) - -#Login -if (firegex.login(args.password)): puts(f"Sucessfully logged in ✔", color=colors.green) -else: puts(f"Test Failed: Unknown response or wrong passowrd ✗", color=colors.red); exit(1) - -def exit_test(code): - if service_id: - server.stop() - if(firegex.px_delete_service(service_id)): puts(f"Sucessfully deleted service ✔", color=colors.green) - else: puts(f"Test Failed: Coulnd't deleted serivce ✗", color=colors.red); exit_test(1) - exit(code) - -#Create service -service_id = firegex.px_add_service(args.service_name, args.port, 6140) -if service_id: puts(f"Sucessfully created service {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Failed to create service ✗", color=colors.red); exit(1) - -if(firegex.px_start_service(service_id)): puts(f"Sucessfully started service ✔", color=colors.green) -else: puts(f"Test Failed: Failed to start service ✗", color=colors.red); exit_test(1) - -#Check if service is in wait mode -if(firegex.px_get_service(service_id)["status"] == "wait"): puts(f"Sucessfully started service in WAIT mode ✔", color=colors.green) -else: puts(f"Test Failed: Service not in WAIT mode ✗", color=colors.red); exit_test(1) - -#Get inernal_port -internal_port = firegex.px_get_service(service_id)["internal_port"] -if (internal_port): puts(f"Sucessfully got internal port {internal_port} ✔", color=colors.green) -else: puts(f"Test Failed: Coundn't get internal_port ✗", color=colors.red); exit_test(1) - -server.stop() -server = TcpServer(internal_port,ipv6=False, proxy_port=args.port) -server.start() -time.sleep(1) - -if(firegex.px_get_service(service_id)["status"] == "active"): puts(f"Service went in ACTIVE mode ✔", color=colors.green) -else: puts(f"Test Failed: Service not in ACTIVE mode ✗", color=colors.red); exit_test(1) - -if server.sendCheckData(secrets.token_bytes(432)): - puts(f"Successfully tested first proxy with no regex ✔", color=colors.green) -else: - puts(f"Test Failed: Data was corrupted ", color=colors.red); exit_test(1) - -#Add new regex -secret = bytes(secrets.token_hex(16).encode()) -regex = base64.b64encode(secret).decode() -if(firegex.px_add_regex(service_id,regex,"B",active=True,is_blacklist=True,is_case_sensitive=True)): - puts(f"Sucessfully added regex {str(secret)} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't add the regex {str(secret)} ✗", color=colors.red); exit_test(1) - -#Check if regex is present in the service -n_blocked = 0 - -def checkRegex(regex, should_work=True, upper=False): - if should_work: - global n_blocked - for r in firegex.px_get_service_regexes(service_id): - if r["regex"] == regex: - #Test the regex - s = base64.b64decode(regex).upper() if upper else base64.b64decode(regex) - if not server.sendCheckData(secrets.token_bytes(200) + s + secrets.token_bytes(200)): - puts(f"The malicious request was successfully blocked ✔", color=colors.green) - n_blocked += 1 - time.sleep(0.5) - if firegex.px_get_regex(r["id"])["n_packets"] == n_blocked: - puts(f"The packed was reported as blocked ✔", color=colors.green) - else: - puts(f"Test Failed: The packed wasn't reported as blocked ✗", color=colors.red); exit_test(1) - else: - puts(f"Test Failed: The request wasn't blocked ✗", color=colors.red);exit_test(1) - return - puts(f"Test Failed: The regex wasn't found ✗", color=colors.red); exit_test(1) - else: - if server.sendCheckData(secrets.token_bytes(200) + base64.b64decode(regex) + secrets.token_bytes(200)): - puts(f"The request wasn't blocked ✔", color=colors.green) - else: - puts(f"Test Failed: The request was blocked when it shouldn't have", color=colors.red); exit_test(1) - -checkRegex(regex) - -#Pause the proxy -if(firegex.px_pause_service(service_id)): puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't pause the service ✗", color=colors.red); exit_test(1) - -#Check if it's actually paused -checkRegex(regex,should_work=False) - -#Start firewall -if(firegex.px_start_service(service_id)): puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't start the service ✗", color=colors.red); exit_test(1) - -checkRegex(regex) - -#Stop firewall -if(firegex.px_stop_service(service_id)): puts(f"Sucessfully stopped service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't stop the service ✗", color=colors.red); exit_test(1) - -try: - checkRegex(regex) - puts(f"Test Failed: The service was still active ✗", color=colors.red); exit_test(1) -except Exception: - puts(f"Service was correctly stopped ✔", color=colors.green) - -#Start firewall in pause -if(firegex.px_pause_service(service_id)): puts(f"Sucessfully started service in pause mode with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't start the service ✗", color=colors.red); exit_test(1) - -time.sleep(0.5) -#Check if it's actually paused -checkRegex(regex,should_work=False) - -#Start firewall -if(firegex.px_start_service(service_id)): puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't start the service ✗", color=colors.red); exit_test(1) - -checkRegex(regex) - -#Disable regex -for r in firegex.px_get_service_regexes(service_id): - if r["regex"] == regex: - if(firegex.px_disable_regex(r["id"])): - puts(f"Sucessfully disabled regex with id {r['id']} ✔", color=colors.green) - else: - puts(f"Test Failed: Coulnd't disable the regex ✗", color=colors.red); exit_test(1) - break - -#Check if it's actually disabled -checkRegex(regex,should_work=False) - -#Enable regex -for r in firegex.px_get_service_regexes(service_id): - if r["regex"] == regex: - if(firegex.px_enable_regex(r["id"])): - puts(f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green) - else: - puts(f"Test Failed: Coulnd't enable the regex ✗", color=colors.red); exit_test(1) - break - -checkRegex(regex) - -#Delete regex -n_blocked = 0 -for r in firegex.px_get_service_regexes(service_id): - if r["regex"] == regex: - if(firegex.px_delete_regex(r["id"])): - puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green) - else: - puts(f"Test Failed: Coulnd't delete the regex ✗", color=colors.red); exit_test(1) - break - -#Check if it's actually deleted -checkRegex(regex,should_work=False) - -#Add case insensitive regex -if(firegex.px_add_regex(service_id,regex,"B",active=True,is_blacklist=True,is_case_sensitive=False)): - puts(f"Sucessfully added case insensitive regex {str(secret)} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗", color=colors.red); exit_test(1) - -checkRegex(regex,upper=True) -checkRegex(regex) - -#Delete regex -n_blocked = 0 -for r in firegex.px_get_service_regexes(service_id): - if r["regex"] == regex: - if(firegex.px_delete_regex(r["id"])): - puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green) - else: - puts(f"Test Failed: Coulnd't delete the regex ✗", color=colors.red); exit_test(1) - break - -#Add whitelist regex -if(firegex.px_add_regex(service_id,regex,"B",active=True,is_blacklist=False,is_case_sensitive=True)): - puts(f"Sucessfully added case whitelist regex {str(secret)} ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't add the case whiteblist regex {str(secret)} ✗", color=colors.red); exit_test(1) - -checkRegex(regex,should_work=False) -checkRegex(regex,upper=True) #Dirty way to test the whitelist :p - -#Delete regex -n_blocked = 0 -for r in firegex.px_get_service_regexes(service_id): - if r["regex"] == regex: - if(firegex.px_delete_regex(r["id"])): - puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green) - else: - puts(f"Test Failed: Coulnd't delete the regex ✗", color=colors.red); exit_test(1) - break - -#Rename service -if(firegex.px_rename_service(service_id,f"{args.service_name}2")): puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green) -else: puts(f"Test Failed: Coulnd't rename service ✗", color=colors.red); exit_test(1) - -#Check if service was renamed correctly -found = False -for services in firegex.px_get_services(): - if services["name"] == f"{args.service_name}2": - puts(f"Checked that service was renamed correctly ✔", color=colors.green) - found = True - break - -if not found: - puts(f"Test Failed: Service wasn't renamed correctly ✗", color=colors.red); exit_test(1) - exit(1) - -#Change service port -new_internal_port = random.randrange(6000,9000) -if(firegex.px_change_service_port(service_id,internalPort=new_internal_port)): - puts(f"Sucessfully changed internal_port to {new_internal_port} ✔", color=colors.green) -else: - puts(f"Test Failed: Coulnd't change intenral port ✗", color=colors.red); exit_test(1) - -#Get inernal_port -internal_port = firegex.px_get_service(service_id)["internal_port"] -if (internal_port == new_internal_port): puts(f"Sucessfully got internal port {internal_port} ✔", color=colors.green) -else: puts(f"Test Failed: Coundn't get internal_port or port changed incorrectly ✗", color=colors.red); exit_test(1) - -if(firegex.px_regen_service_port(service_id)): - puts(f"Sucessfully changed internal_port to {new_internal_port} ✔", color=colors.green) -else: - puts(f"Test Failed: Coulnd't change internal port ✗", color=colors.red); exit_test(1) - -#Get regenerated inernal_port -new_internal_port = firegex.px_get_service(service_id)["internal_port"] -if (internal_port != new_internal_port): puts(f"Sucessfully got regenerated port {new_internal_port} ✔", color=colors.green) -else: puts(f"Test Failed: Coundn't get internal port, or it was the same as previous ✗", color=colors.red); exit_test(1) - -exit_test(0) diff --git a/tests/run_tests.sh b/tests/run_tests.sh index e38aa49..cfe00d9 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -18,8 +18,6 @@ echo "Running Netfilter Regex UDP ipv4" python3 nf_test.py -p $PASSWORD -m udp || ERROR=1 echo "Running Netfilter Regex UDP ipv6" python3 nf_test.py -p $PASSWORD -m udp -6 || ERROR=1 -echo "Running Proxy Regex" -python3 px_test.py -p $PASSWORD || ERROR=1 echo "Running Port Hijack TCP ipv4" python3 ph_test.py -p $PASSWORD -m tcp || ERROR=1 echo "Running Port Hijack TCP ipv6" diff --git a/backend/modules/regexproxy/__init__.py b/tests/utils/__init__.py similarity index 100% rename from backend/modules/regexproxy/__init__.py rename to tests/utils/__init__.py diff --git a/tests/utils/firegexapi.py b/tests/utils/firegexapi.py index 51da773..ad2930b 100644 --- a/tests/utils/firegexapi.py +++ b/tests/utils/firegexapi.py @@ -70,7 +70,7 @@ class FiregexAPI: return req.json() def reset(self, delete: bool): - req = self.s.post(f"{self.address}api/reset", json={"delete":delete}) + self.s.post(f"{self.address}api/reset", json={"delete":delete}) #Netfilter regex def nf_get_stats(self): @@ -131,84 +131,6 @@ class FiregexAPI: json={"name":name,"port":port, "proto": proto, "ip_int": ip_int}) return req.json()["service_id"] if verify(req) else False - #Proxy regex - def px_get_stats(self): - req = self.s.get(f"{self.address}api/regexproxy/stats") - return req.json() - - def px_get_services(self): - req = self.s.get(f"{self.address}api/regexproxy/services") - return req.json() - - def px_get_service(self,service_id: str): - req = self.s.get(f"{self.address}api/regexproxy/service/{service_id}") - return req.json() - - def px_stop_service(self,service_id: str): - req = self.s.get(f"{self.address}api/regexproxy/service/{service_id}/stop") - return verify(req) - - def px_pause_service(self,service_id: str): - req = self.s.get(f"{self.address}api/regexproxy/service/{service_id}/pause") - return verify(req) - - def px_start_service(self,service_id: str): - req = self.s.get(f"{self.address}api/regexproxy/service/{service_id}/start") - return verify(req) - - def px_delete_service(self,service_id: str): - req = self.s.get(f"{self.address}api/regexproxy/service/{service_id}/delete") - return verify(req) - - def px_regen_service_port(self,service_id: str): - req = self.s.get(f"{self.address}api/regexproxy/service/{service_id}/regen-port") - return verify(req) - - def px_change_service_port(self,service_id: str, port:int =None, internalPort:int =None): - payload = {} - if port: payload["port"] = port - if internalPort: payload["internalPort"] = internalPort - req = self.s.post(f"{self.address}api/regexproxy/service/{service_id}/change-ports", json=payload) - return req.json() if verify(req) else False - - def px_get_service_regexes(self,service_id: str): - req = self.s.get(f"{self.address}api/regexproxy/service/{service_id}/regexes") - return req.json() - - def px_get_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/regexproxy/regex/{regex_id}") - return req.json() - - def px_delete_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/regexproxy/regex/{regex_id}/delete") - return verify(req) - - def px_enable_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/regexproxy/regex/{regex_id}/enable") - return verify(req) - - def px_disable_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/regexproxy/regex/{regex_id}/disable") - return verify(req) - - def px_add_regex(self, service_id: str, regex: str, mode: str, active: bool, is_blacklist: bool, is_case_sensitive: bool): - req = self.s.post(f"{self.address}api/regexproxy/regexes/add", - json={"service_id": service_id, "regex": regex, "mode": mode, "active": active, "is_blacklist": is_blacklist, "is_case_sensitive": is_case_sensitive}) - return verify(req) - - def px_rename_service(self,service_id: str, newname: str): - req = self.s.post(f"{self.address}api/regexproxy/service/{service_id}/rename" , json={"name":newname}) - return verify(req) - - def px_add_service(self, name: str, port: int, internalPort:int = None): - payload = {} - payload["name"] = name - payload["port"] = port - if internalPort: - payload["internalPort"] = internalPort - req = self.s.post(f"{self.address}api/regexproxy/services/add" , json=payload) - return req.json()["id"] if verify(req) else False - #PortHijack def ph_get_services(self): req = self.s.get(f"{self.address}api/porthijack/services")