diff --git a/.dockerignore b/.dockerignore index 897cfef..0758864 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,7 +19,7 @@ Dockerfile /frontend/build/ /frontend/build/** /frontend/node_modules/ -/backend/modules/cppqueue +/backend/modules/cppregex /backend/modules/proxy docker-compose.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 0db7d38..96ff219 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -20,12 +20,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Build and run firegex - run: python3 start.py start --psw-no-interactive testpassword - - - name: Run tests - run: cd tests && ./run_tests.sh - - name: Set up QEMU uses: docker/setup-qemu-action@master with: @@ -41,13 +35,20 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - + - name: Extract tag name + id: tag + run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT + - name: Update version in setup.py + run: >- + sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py; + sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" proxy-client/setup.py; + sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" proxy-client/firegex/__init__.py; - name: Build and push Docker image uses: docker/build-push-action@v5 with: @@ -59,5 +60,3 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - - diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 0000000..dbfe476 --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,47 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: + - published + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Extract tag name + id: tag + run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT + - name: Update version in setup.py + run: >- + sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" proxy-client/setup.py; + sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" proxy-client/firegex/__init__.py; + - name: Build package + run: cd client && python -m build && mv ./dist ../ + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 4221f2b..cf7f774 100644 --- a/.gitignore +++ b/.gitignore @@ -11,20 +11,25 @@ # testing /frontend/coverage - +/proxy-client/firegex.egg-info +/proxy-client/dist +/proxy-client/fgex-pip/fgex.egg-info +/proxy-client/fgex-pip/dist /backend/db/ /backend/db/** /frontend/build/ /frontend/build/** /frontend/dist/ /frontend/dist/** -/backend/modules/cppqueue -/backend/binsrc/cppqueue +/backend/modules/cppregex +/backend/binsrc/cppregex +/backend/modules/cpproxy +/backend/binsrc/cpproxy /backend/modules/proxy -docker-compose.yml -firegex-compose.yml -firegex-compose-tmp-file.yml -firegex.py +/docker-compose.yml +/firegex-compose.yml +/firegex-compose-tmp-file.yml +/firegex.py /tests/benchmark.csv # misc **/.DS_Store diff --git a/Dockerfile b/Dockerfile index 74a1d6d..d8270d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,11 +14,10 @@ RUN bun run build #Building main conteiner -FROM --platform=$TARGETARCH debian:trixie-slim AS base -RUN apt-get update -qq && apt-get upgrade -qq && \ - apt-get install -qq python3-pip build-essential \ - libnetfilter-queue-dev libnfnetlink-dev libmnl-dev libcap2-bin\ - nftables libvectorscan-dev libtins-dev python3-nftables +FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest +RUN dnf -y update && dnf install -y python3.13-devel python3-pip @development-tools gcc-c++ \ + libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils nftables \ + vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel RUN mkdir -p /execute/modules WORKDIR /execute @@ -27,7 +26,8 @@ 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 -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) +RUN g++ binsrc/nfregex.cpp -o modules/cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) +#RUN g++ binsrc/nfproxy.cpp -o modules/cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.13 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3) COPY ./backend/ /execute/ COPY --from=frontend /app/dist/ ./frontend/ diff --git a/README.md b/README.md index aa4bc3a..1e4e8b5 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ This means that firegex is projected to avoid any possibility to have the servic Initiially the project was based only on regex filters, and also now the main function uses regexes, but firegex have and will have also other filtering tools. # Credits -- Copyright (c) 2022 Pwnzer0tt1 +- Copyright (c) 2022-2025 Pwnzer0tt1 ## Star History diff --git a/backend/app.py b/backend/app.py index 2f6f7e1..b6646f2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -8,13 +8,13 @@ from fastapi import FastAPI, HTTPException, Depends, APIRouter from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import jwt from passlib.context import CryptContext -from fastapi_socketio import SocketManager from utils.sqlite import SQLite from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager from utils.loader import frontend_deploy, load_routers from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel from contextlib import asynccontextmanager from fastapi.middleware.cors import CORSMiddleware +import socketio # DB init db = SQLite('db/firegex.db') @@ -42,7 +42,6 @@ app = FastAPI( title="Firegex API", version=API_VERSION, ) -utils.socketio = SocketManager(app, "/sock", socketio_path="") if DEBUG: app.add_middleware( @@ -54,6 +53,15 @@ if DEBUG: ) +utils.socketio = socketio.AsyncServer( + async_mode="asgi", + cors_allowed_origins=[], + transports=["websocket"] +) + +sio_app = socketio.ASGIApp(utils.socketio, socketio_path="/sock/socket.io", other_asgi_app=app) +app.mount("/sock", sio_app) + def APP_STATUS(): return "init" if db.get("password") is None else "run" def JWT_SECRET(): return db.get("secret") @@ -190,10 +198,11 @@ if __name__ == '__main__': os.chdir(os.path.dirname(os.path.realpath(__file__))) uvicorn.run( "app:app", - host=None, #"::" if DEBUG else None, + host="::" if DEBUG else None, port=FIREGEX_PORT, - reload=False,#DEBUG, + reload=DEBUG, access_log=True, - workers=1, # Multiple workers will cause a crash due to the creation - # of multiple processes with separated memory + workers=1, # Firewall module can't be replicated in multiple workers + # Later the firewall module will be moved to a separate process + # The webserver will communicate using redis (redis is also needed for websockets) ) diff --git a/backend/binsrc/classes/netfilter.cpp b/backend/binsrc/classes/netfilter.cpp index 257e983..5705c5a 100644 --- a/backend/binsrc/classes/netfilter.cpp +++ b/backend/binsrc/classes/netfilter.cpp @@ -1,527 +1,101 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include -#include +#include +#include "../utils.cpp" +#include "nfqueue.cpp" -using Tins::TCPIP::Stream; -using Tins::TCPIP::StreamFollower; -using namespace std; +#ifndef NETFILTER_CLASS_CPP +#define NETFILTER_CLASS_CPP -#ifndef NETFILTER_CLASSES_HPP -#define NETFILTER_CLASSES_HPP -typedef Tins::TCPIP::StreamIdentifier stream_id; -typedef map matching_map; +namespace Firegex { +namespace NfQueue { -/* Considering to use unorder_map using this hash of stream_id +template +class ThreadNfQueue { +public: + ThreadNfQueue() = default; + virtual ~ThreadNfQueue() = default; -namespace std { - template<> - struct hash { - size_t operator()(const stream_id& sid) const - { - return std::hash()(sid.max_address[0] + sid.max_address[1] + sid.max_address[2] + sid.max_address[3] + sid.max_address_port + sid.min_address[0] + sid.min_address[1] + sid.min_address[2] + sid.min_address[3] + sid.min_address_port); - } - }; -} + std::thread thr; + BlockingQueue*> queue; -*/ + virtual void before_loop() {} + virtual void handle_next_packet(PktRequest* pkt){} + + void loop() { + static_cast(this)->before_loop(); + PktRequest* pkt; + for(;;) { + queue.take(pkt); + static_cast(this)->handle_next_packet(pkt); + delete pkt; + } + } -#ifdef DEBUG -ostream& operator<<(ostream& os, const Tins::TCPIP::StreamIdentifier::address_type &sid){ - bool first_print = false; - for (auto ele: sid){ - if (first_print || ele){ - first_print = true; - os << (int)ele << "."; - } - } - return os; -} - -ostream& operator<<(ostream& os, const stream_id &sid){ - os << sid.max_address << ":" << sid.max_address_port << " -> " << sid.min_address << ":" << sid.min_address_port; - return os; -} -#endif - -struct packet_info; - -struct tcp_stream_tmp { - bool matching_has_been_called = false; - bool result; - packet_info *pkt_info; + void run_thread_loop() { + thr = std::thread([this]() { this->loop(); }); + } }; -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; +template , Worker>> +void __real_handler(PktRequest>* pkt) { + const size_t idx = hash_stream_id(pkt->sid) % pkt->ctx->size(); - 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; - } - } - - void clean_stream_by_id(stream_id sid){ - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.clean_stream_by_id] Cleaning stream context of " << sid << endl; - #endif - auto stream_search = in_hs_streams.find(sid); - hs_stream_t* stream_match; - if (stream_search != in_hs_streams.end()){ - stream_match = stream_search->second; - if (hs_close_stream(stream_match, 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"); - } - in_hs_streams.erase(stream_search); - } - - stream_search = out_hs_streams.find(sid); - if (stream_search != out_hs_streams.end()){ - stream_match = stream_search->second; - if (hs_close_stream(stream_match, 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"); - } - out_hs_streams.erase(stream_search); - } - } - - void clean(){ - - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.clean] Cleaning stream context" << endl; - #endif - - if (in_scratch){ - for(auto ele: in_hs_streams){ - if (hs_close_stream(ele.second, 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"); - } - } - in_hs_streams.clear(); - } - - if (out_scratch){ - for(auto ele: out_hs_streams){ - if (hs_close_stream(ele.second, 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"); - } - } - out_hs_streams.clear(); - } - clean_scratches(); - } -}; - -struct packet_info { - string packet; - string payload; - stream_id sid; - bool is_input; - bool is_tcp; - stream_ctx* sctx; -}; - -typedef bool NetFilterQueueCallback(packet_info &); - -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" ); - } - - } - - static void on_data_recv(Stream& stream, stream_ctx* sctx, string data) { - sctx->tcp_match_util.matching_has_been_called = true; - bool result = callback_func(*sctx->tcp_match_util.pkt_info); - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.on_data_recv] result: " << result << endl; - #endif - if (!result){ - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.on_data_recv] Stream matched, removing all data about it" << endl; - #endif - sctx->clean_stream_by_id(sctx->tcp_match_util.pkt_info->sid); - stream.ignore_client_data(); - stream.ignore_server_data(); - } - sctx->tcp_match_util.result = result; - } - - //Input data filtering - static void on_client_data(Stream& stream, stream_ctx* sctx) { - on_data_recv(stream, sctx, string(stream.client_payload().begin(), stream.client_payload().end())); - } - - //Server data filtering - static void on_server_data(Stream& stream, stream_ctx* sctx) { - on_data_recv(stream, sctx, string(stream.server_payload().begin(), stream.server_payload().end())); - } - - static void on_new_stream(Stream& stream, stream_ctx* sctx) { - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.on_new_stream] New stream detected" << endl; - #endif - if (stream.is_partial_stream()) { - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.on_new_stream] Partial stream detected, skipping" << endl; - #endif - return; - } - stream.auto_cleanup_payloads(true); - stream.client_data_callback(bind(on_client_data, placeholders::_1, sctx)); - stream.server_data_callback(bind(on_server_data, placeholders::_1, sctx)); - stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, sctx)); - } - - // A stream was terminated. The second argument is the reason why it was terminated - static void on_stream_close(Stream& stream, stream_ctx* sctx) { - stream_id stream_id = stream_id::make_identifier(stream); - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.on_stream_close] Stream terminated, deleting all data" << endl; - #endif - sctx->clean_stream_by_id(stream_id); - } + auto* converted_pkt = reinterpret_cast*>(pkt); + converted_pkt->ctx = &((*pkt->ctx)[idx]); + + converted_pkt->ctx->queue.put(converted_pkt); +} - 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(bind(on_new_stream, placeholders::_1, &sctx)); - sctx.follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, &sctx)); +template , Worker>> +class MultiThreadQueue { + static_assert(std::is_base_of_v, Worker>, + "Worker must inherit from ThreadNfQueue"); - 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" ); - } - } - } +private: + std::vector workers; + NfQueue, __real_handler> * nfq; + uint16_t queue_num_; - - ~NetfilterQueue() { - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.~NetfilterQueue] Destructor called" << endl; - #endif - send_config_cmd(NFQNL_CFG_CMD_UNBIND); - _clear(); - } - private: + +public: + const size_t n_threads; + static constexpr int QUEUE_BASE_NUM = 1000; - 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); - } + explicit MultiThreadQueue(size_t n_threads) + : n_threads(n_threads), workers(n_threads) + { + if(n_threads == 0) throw std::invalid_argument("At least 1 thread required"); + + for(uint16_t qnum = QUEUE_BASE_NUM; ; qnum++) { + try { + nfq = new NfQueue, __real_handler>(qnum); + queue_num_ = qnum; + break; + } + catch(const std::invalid_argument&) { + if(qnum == std::numeric_limits::max()) + throw std::runtime_error("No available queue numbers"); + } + } + } - ssize_t recv_packet(){ - return mnl_socket_recvfrom(sctx.nl, buf, BUF_SIZE); - } + ~MultiThreadQueue() { + delete nfq; + } - void _clear(){ - if (buf != nullptr) { - free(buf); - buf = nullptr; + void start() { + for(auto& worker : workers) { + worker.run_thread_loop(); + } + for (;;){ + nfq->handle_next_packet(&workers); } - mnl_socket_close(sctx.nl); - sctx.nl = nullptr; - sctx.clean(); - } - - template - static void build_verdict(T packet, uint8_t *payload, uint16_t plen, nlmsghdr *nlh_verdict, nfqnl_msg_packet_hdr *ph, stream_ctx* sctx, bool is_input){ - Tins::TCP* tcp = packet.template find_pdu(); - - if (tcp){ - Tins::PDU* application_layer = tcp->inner_pdu(); - u_int16_t payload_size = 0; - if (application_layer != nullptr){ - payload_size = application_layer->size(); - } - packet_info pktinfo{ - packet: string(payload, payload+plen), - payload: string(payload+plen - payload_size, payload+plen), - sid: stream_id::make_identifier(packet), - is_input: is_input, - is_tcp: true, - sctx: sctx, - }; - sctx->tcp_match_util.matching_has_been_called = false; - sctx->tcp_match_util.pkt_info = &pktinfo; - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.build_verdict] TCP Packet received " << packet.src_addr() << ":" << tcp->sport() << " -> " << packet.dst_addr() << ":" << tcp->dport() << " thr: " << this_thread::get_id() << ", sending to libtins StreamFollower" << endl; - #endif - sctx->follower.process_packet(packet); - #ifdef DEBUG - if (sctx->tcp_match_util.matching_has_been_called){ - cerr << "[DEBUG] [NetfilterQueue.build_verdict] StreamFollower has called matching functions" << endl; - }else{ - cerr << "[DEBUG] [NetfilterQueue.build_verdict] StreamFollower has NOT called matching functions" << endl; - } - #endif - if (sctx->tcp_match_util.matching_has_been_called && !sctx->tcp_match_util.result){ - Tins::PDU* data_layer = tcp->release_inner_pdu(); - if (data_layer != nullptr){ - delete data_layer; - } - tcp->set_flag(Tins::TCP::FIN,1); - tcp->set_flag(Tins::TCP::ACK,1); - tcp->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{ - Tins::UDP* udp = packet.template find_pdu(); - if (!udp){ - throw invalid_argument("Only TCP and UDP are supported"); - } - Tins::PDU* application_layer = udp->inner_pdu(); - u_int16_t payload_size = 0; - if (application_layer != nullptr){ - payload_size = application_layer->size(); - } - if((udp->inner_pdu() == nullptr)){ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); - } - packet_info pktinfo{ - packet: string(payload, payload+plen), - payload: string(payload+plen - payload_size, payload+plen), - sid: stream_id::make_identifier(packet), - is_input: is_input, - is_tcp: false, - sctx: sctx, - }; - 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 ); - } - } - } - - 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; - } - if (attr[NFQA_MARK] == nullptr) { - fputs("mark 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)); - - bool is_input = ntohl(mnl_attr_get_u32(attr[NFQA_MARK])) & 0x1; // == 0x1337 that is odd - #ifdef DEBUG - cerr << "[DEBUG] [NetfilterQueue.queue_cb] Packet received" << endl; - cerr << "[DEBUG] [NetfilterQueue.queue_cb] Packet ID: " << ntohl(ph->packet_id) << endl; - cerr << "[DEBUG] [NetfilterQueue.queue_cb] Payload size: " << plen << endl; - cerr << "[DEBUG] [NetfilterQueue.queue_cb] Is input: " << is_input << endl; - #endif - - // Check IP protocol version - if ( (payload[0] & 0xf0) == 0x40 ){ - build_verdict(Tins::IP(payload, plen), payload, plen, nlh_verdict, ph, sctx, is_input); - }else{ - build_verdict(Tins::IPv6(payload, plen), payload, plen, nlh_verdict, ph, sctx, is_input); - } - - nest = mnl_attr_nest_start(nlh_verdict, NFQA_CT); - mnl_attr_put_u32(nlh_verdict, CTA_MARK, htonl(42)); - 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; - } + } + uint16_t queue_num() const { return queue_num_; } }; -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 + +using namespace std; + +namespace Firegex{ +namespace NfQueue{ + +enum class FilterAction{ DROP, ACCEPT, MANGLE, NOACTION }; +enum class L4Proto { TCP, UDP, RAW }; +typedef Tins::TCPIP::StreamIdentifier stream_id; + + +template +class PktRequest { + private: + FilterAction action = FilterAction::NOACTION; + mnl_socket* nl = nullptr; + uint16_t res_id; + uint32_t packet_id; + public: + bool is_ipv6; + Tins::IP* ipv4 = nullptr; + Tins::IPv6* ipv6 = nullptr; + Tins::TCP* tcp = nullptr; + Tins::UDP* udp = nullptr; + L4Proto l4_proto; + bool is_input; + + string packet; + char* data; + size_t data_size; + stream_id sid; + + T* ctx; + + private: + + inline void fetch_data_size(Tins::PDU* pdu){ + auto inner = pdu->inner_pdu(); + if (inner == nullptr){ + data_size = 0; + }else{ + data_size = inner->size(); + } + } + + L4Proto fill_l4_info(){ + if (is_ipv6){ + tcp = ipv6->find_pdu(); + if (tcp == nullptr){ + udp = ipv6->find_pdu(); + if (udp == nullptr){ + fetch_data_size(ipv6); + return L4Proto::RAW; + }else{ + fetch_data_size(udp); + return L4Proto::UDP; + } + }else{ + fetch_data_size(tcp); + return L4Proto::TCP; + } + }else{ + tcp = ipv4->find_pdu(); + if (tcp == nullptr){ + udp = ipv4->find_pdu(); + if (udp == nullptr){ + fetch_data_size(ipv4); + return L4Proto::RAW; + }else{ + fetch_data_size(udp); + return L4Proto::UDP; + } + }else{ + fetch_data_size(tcp); + return L4Proto::TCP; + } + } + } + + public: + + PktRequest(const char* payload, size_t plen, T* ctx, mnl_socket* nl, nfgenmsg *nfg, nfqnl_msg_packet_hdr *ph, bool is_input): + ctx(ctx), nl(nl), res_id(nfg->res_id), + packet_id(ph->packet_id), is_input(is_input), + packet(string(payload, plen)), + is_ipv6((payload[0] & 0xf0) == 0x60){ + if (is_ipv6){ + ipv6 = new Tins::IPv6((uint8_t*)packet.c_str(), plen); + sid = stream_id::make_identifier(*ipv6); + }else{ + ipv4 = new Tins::IP((uint8_t*)packet.c_str(), plen); + sid = stream_id::make_identifier(*ipv4); + } + l4_proto = fill_l4_info(); + data = packet.data()+(plen-data_size); + } + + void drop(){ + if (action == FilterAction::NOACTION){ + action = FilterAction::DROP; + perfrom_action(); + }else{ + throw invalid_argument("Cannot drop a packet that has already been dropped or accepted"); + } + } + + void accept(){ + if (action == FilterAction::NOACTION){ + action = FilterAction::ACCEPT; + perfrom_action(); + }else{ + throw invalid_argument("Cannot accept a packet that has already been dropped or accepted"); + } + } + + void mangle(){ + if (action == FilterAction::NOACTION){ + action = FilterAction::MANGLE; + perfrom_action(); + }else{ + throw invalid_argument("Cannot mangle a packet that has already been accepted or dropped"); + } + } + + FilterAction get_action(){ + return action; + } + + ~PktRequest(){ + delete ipv4; + delete ipv6; + } + + private: + void perfrom_action(){ + char buf[MNL_SOCKET_BUFFER_SIZE]; + struct nlmsghdr *nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(res_id)); + switch (action) + { + case FilterAction::ACCEPT: + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_ACCEPT ); + break; + case FilterAction::DROP: + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_DROP ); + break; + case FilterAction::MANGLE:{ + if (is_ipv6){ + nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv6->serialize().data(), ipv6->size()); + }else{ + nfq_nlmsg_verdict_put_pkt(nlh_verdict, ipv4->serialize().data(), ipv4->size()); + } + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(packet_id), NF_ACCEPT ); + break; + } + default: + throw invalid_argument("Invalid action"); + } + if (mnl_socket_sendto(nl, nlh_verdict, nlh_verdict->nlmsg_len) < 0) { + throw runtime_error( "mnl_socket_send" ); + } + } + +}; + +struct internal_nfqueue_execution_data_tmp{ + mnl_socket* nl = nullptr; + void *data = nullptr; +}; + +const size_t NFQUEUE_BUFFER_SIZE = 0xffff + (MNL_SOCKET_BUFFER_SIZE/2); +/* NfQueue wrapper class to handle nfqueue packets + this class is made to be possible enqueue multiple packets to multiple threads + --> handle function is responsable to delete the PktRequest object */ +template *)> +class NfQueue { + private: + mnl_socket* nl = nullptr; + unsigned int portid; + public: + char* queue_msg_buffer = nullptr; + const uint16_t queue_num; + + NfQueue(u_int16_t queue_num): queue_num(queue_num) { + queue_msg_buffer = new char[NFQUEUE_BUFFER_SIZE]; + nl = mnl_socket_open(NETLINK_NETFILTER); + + if (nl == nullptr) { throw runtime_error( "mnl_socket_open" );} + + if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) { + mnl_socket_close(nl); + throw runtime_error( "mnl_socket_bind" ); + } + portid = mnl_socket_get_portid(nl); + + 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 command 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 *) queue_msg_buffer; + + 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(queue_msg_buffer, NFQNL_MSG_CONFIG, queue_num); + nfq_nlmsg_cfg_put_params(nlh, NFQNL_COPY_PACKET, 0xffff); + + char * enable_fail_open = getenv("FIREGEX_NFQUEUE_FAIL_OPEN"); + + if (strcmp(enable_fail_open, "1") == 0){ + mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO|NFQA_CFG_F_FAIL_OPEN)); + mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO|NFQA_CFG_F_FAIL_OPEN)); + }else{ + 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 runtime_error( "mnl_socket_send" ); + } + + /* + * 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 tmp = 1; + mnl_socket_setsockopt(nl, NETLINK_NO_ENOBUFS, &tmp, sizeof(int)); + + } + + void handle_next_packet(D* data){ + int ret = _recv_packet(); + if (ret == -1) { + throw runtime_error( "mnl_socket_recvfrom" ); + } + internal_nfqueue_execution_data_tmp raw_ptr = { + nl: nl, + data: data + }; + + ret = mnl_cb_run(queue_msg_buffer, ret, 0, portid, _real_queue_cb, &raw_ptr); + if (ret <= 0){ + cerr << "[error] [NfQueue.handle_next_packet] mnl_cb_run error with: " << ret << endl; + throw runtime_error( "mnl_cb_run error!" ); + } + } + + ~NfQueue() { + _send_config_cmd(NFQNL_CFG_CMD_UNBIND); + _clear(); + } + + private: + + static int _real_queue_cb(const nlmsghdr *nlh, void *data_ptr) { + + internal_nfqueue_execution_data_tmp* info = (internal_nfqueue_execution_data_tmp*) data_ptr; + + //Extract attributes from the nlmsghdr + nlattr *attr[NFQA_MAX+1] = {}; + + if (nfq_nlmsg_parse(nlh, attr) < 0) { + cerr << "[error] [NfQueue._real_queue_cb] problems parsing" << endl; + return MNL_CB_ERROR; + } + if (attr[NFQA_PACKET_HDR] == nullptr) { + cerr << "[error] [NfQueue._real_queue_cb] packet header not set" << endl; + return MNL_CB_ERROR; + } + if (attr[NFQA_MARK] == nullptr) { + cerr << "[error] [NfQueue._real_queue_cb] mark not set" << endl; + return MNL_CB_ERROR; + } + + //Get Payload + uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]); + char *payload = (char *)mnl_attr_get_payload(attr[NFQA_PAYLOAD]); + + //Return result to the kernel + struct nfqnl_msg_packet_hdr *ph = (nfqnl_msg_packet_hdr*) mnl_attr_get_payload(attr[NFQA_PACKET_HDR]); + struct nfgenmsg *nfg = (nfgenmsg *)mnl_nlmsg_get_payload(nlh); + + bool is_input = ntohl(mnl_attr_get_u32(attr[NFQA_MARK])) & 0x1; // == 0x1337 that is odd + handle_func(new PktRequest( + payload, plen, (D*)info->data, info->nl, nfg, ph, is_input + )); + + return MNL_CB_OK; + } + + inline void _clear(){ + if (nl != nullptr) { + mnl_socket_close(nl); + nl = nullptr; + } + delete[] queue_msg_buffer; + } + + inline ssize_t _send_config_cmd(nfqnl_msg_config_cmds cmd){ + struct nlmsghdr *nlh = nfq_nlmsg_put(queue_msg_buffer, NFQNL_MSG_CONFIG, queue_num); + nfq_nlmsg_cfg_put_cmd(nlh, AF_INET, cmd); + return mnl_socket_sendto(nl, nlh, nlh->nlmsg_len); + } + + inline ssize_t _recv_packet(){ + return mnl_socket_recvfrom(nl, queue_msg_buffer, NFQUEUE_BUFFER_SIZE); + } + +}; + + + +uint32_t hash_stream_id(const stream_id &sid) { + uint32_t addr_hash = 0; + const uint32_t* min_addr = reinterpret_cast(sid.min_address.data()); + const uint32_t* max_addr = reinterpret_cast(sid.max_address.data()); + addr_hash ^= min_addr[0] ^ min_addr[1] ^ min_addr[2] ^ min_addr[3]; + addr_hash ^= max_addr[0] ^ max_addr[1] ^ max_addr[2] ^ max_addr[3]; + + uint32_t ports = (static_cast(sid.min_address_port) << 16) | sid.max_address_port; + + uint32_t hash = addr_hash ^ ports; + + hash *= 0x9e3779b9; + + return hash; +} + +}} +#endif // NFQUEUE_CLASS_CPP \ No newline at end of file diff --git a/backend/binsrc/nfproxy.cpp b/backend/binsrc/nfproxy.cpp new file mode 100644 index 0000000..520292b --- /dev/null +++ b/backend/binsrc/nfproxy.cpp @@ -0,0 +1,72 @@ +#define PY_SSIZE_T_CLEAN +#include + +#include "proxytun/settings.cpp" +#include "proxytun/proxytun.cpp" +#include "classes/netfilter.cpp" +#include +#include +#include +#include + +using namespace std; +using namespace Firegex::PyProxy; +using Firegex::NfQueue::MultiThreadQueue; + +ssize_t read_check(int __fd, void *__buf, size_t __nbytes){ + ssize_t bytes = read(__fd, __buf, __nbytes); + if (bytes == 0){ + cerr << "[fatal] [updater] read() returned EOF" << endl; + throw invalid_argument("read() returned EOF"); + } + if (bytes < 0){ + cerr << "[fatal] [updater] read() returned an error" << bytes << endl; + throw invalid_argument("read() returned an error"); + } + return bytes; +} + +void config_updater (){ + while (true){ + uint32_t code_size; + read_check(STDIN_FILENO, &code_size, 4); + vector code(code_size); + read_check(STDIN_FILENO, code.data(), code_size); + cerr << "[info] [updater] Updating configuration" << endl; + try{ + config.reset(new PyCodeConfig(code)); + cerr << "[info] [updater] Config update done" << endl; + osyncstream(cout) << "ACK OK" << endl; + }catch(const std::exception& e){ + cerr << "[error] [updater] Failed to build new configuration!" << endl; + osyncstream(cout) << "ACK FAIL " << e.what() << endl; + } + } +} + +int main(int argc, char *argv[]){ + + Py_Initialize(); + atexit(Py_Finalize); + + if (freopen(nullptr, "rb", stdin) == nullptr){ // We need to read from stdin binary data + cerr << "[fatal] [main] Failed to reopen stdin in binary mode" << endl; + return 1; + } + int n_of_threads = 1; + char * n_threads_str = getenv("NTHREADS"); + if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str); + if(n_of_threads <= 0) n_of_threads = 1; + + config.reset(new PyCodeConfig()); + MultiThreadQueue queue(n_of_threads); + + osyncstream(cout) << "QUEUE " << queue.queue_num() << endl; + cerr << "[info] [main] Queue: " << queue.queue_num() << " threads assigned: " << n_of_threads << endl; + + thread qthr([&](){ + queue.start(); + }); + config_updater(); + qthr.join(); +} diff --git a/backend/binsrc/nfqueue.cpp b/backend/binsrc/nfqueue.cpp deleted file mode 100644 index a97fd88..0000000 --- a/backend/binsrc/nfqueue.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#include "classes/regex_rules.cpp" -#include "classes/netfilter.cpp" -#include "utils.hpp" -#include - -using namespace std; - -shared_ptr regex_config; - -void config_updater (){ - string line; - while (true){ - getline(cin, line); - if (cin.eof()){ - cerr << "[fatal] [updater] cin.eof()" << endl; - exit(EXIT_FAILURE); - } - if (cin.bad()){ - cerr << "[fatal] [updater] cin.bad()" << endl; - exit(EXIT_FAILURE); - } - cerr << "[info] [updater] Updating configuration with line " << line << endl; - istringstream config_stream(line); - vector raw_rules; - - while(!config_stream.eof()){ - string data; - config_stream >> data; - if (data != "" && data != "\n"){ - raw_rules.push_back(data); - } - } - try{ - regex_config.reset(new RegexRules(raw_rules, regex_config->stream_mode())); - cerr << "[info] [updater] Config update done to ver "<< regex_config->ver() << endl; - cout << "ACK OK" << endl; - }catch(const std::exception& e){ - cerr << "[error] [updater] Failed to build new configuration!" << endl; - cout << "ACK FAIL " << e.what() << endl; - } - } - -} - -void inline scratch_setup(regex_ruleset &conf, hs_scratch_t* & scratch){ - if (scratch == nullptr && conf.hs_db != nullptr){ - if (hs_alloc_scratch(conf.hs_db, &scratch) != HS_SUCCESS) { - throw invalid_argument("Cannot alloc scratch"); - } - } -} - -struct matched_data{ - unsigned int matched = 0; - bool has_matched = false; -}; - - -bool filter_callback(packet_info& info){ - shared_ptr conf = regex_config; - auto current_version = conf->ver(); - if (current_version != info.sctx->latest_config_ver){ - #ifdef DEBUG - cerr << "[DEBUG] [filter_callback] Configuration has changed (" << current_version << "!=" << info.sctx->latest_config_ver << "), cleaning scratch spaces" << endl; - #endif - info.sctx->clean(); - info.sctx->latest_config_ver = current_version; - } - 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; - } - - #ifdef DEBUG - cerr << "[DEBUG] [filter_callback] Matching packet with " << (info.is_input ? "input" : "output") << " ruleset" << endl; - #endif - - 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 - }; - hs_stream_t* stream_match; - if (conf->stream_mode()){ - matching_map* match_map = info.is_input ? &info.sctx->in_hs_streams : &info.sctx->out_hs_streams; - #ifdef DEBUG - cerr << "[DEBUG] [filter_callback] Dumping match_map " << match_map << endl; - for (auto ele: *match_map){ - cerr << "[DEBUG] [filter_callback] " << ele.first << " -> " << ele.second << endl; - } - cerr << "[DEBUG] [filter_callback] End of match_map" << endl; - #endif - auto stream_search = match_map->find(info.sid); - - if (stream_search == match_map->end()){ - - #ifdef DEBUG - cerr << "[DEBUG] [filter_callback] Creating new stream matcher for " << info.sid << endl; - #endif - 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"); - } - if (info.is_tcp){ - match_map->insert_or_assign(info.sid, stream_match); - } - }else{ - stream_match = stream_search->second; - } - #ifdef DEBUG - cerr << "[DEBUG] [filter_callback] Matching as a stream" << endl; - #endif - err = hs_scan_stream( - stream_match,info.payload.c_str(), info.payload.length(), - 0, scratch_space, match_func, &match_res - ); - }else{ - #ifdef DEBUG - cerr << "[DEBUG] [filter_callback] Matching as a block" << endl; - #endif - err = hs_scan( - regex_matcher,info.payload.c_str(), info.payload.length(), - 0, scratch_space, match_func, &match_res - ); - } - if ( - !info.is_tcp && conf->stream_mode() && - hs_close_stream(stream_match, scratch_space, nullptr, nullptr) != HS_SUCCESS - ){ - cerr << "[error] [filter_callback] Error closing the stream matcher (hs)" << endl; - throw invalid_argument("Cannot close stream match on hyperscan"); - } - if (err != HS_SUCCESS && err != HS_SCAN_TERMINATED) { - 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 != nullptr) n_of_threads = ::atoi(n_threads_str); - if(n_of_threads <= 0) n_of_threads = 1; - - char * matchmode = getenv("MATCH_MODE"); - bool stream_mode = true; - if (matchmode != nullptr && strcmp(matchmode, "block") == 0){ - stream_mode = false; - } - - 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 << " stream mode: " << stream_mode << endl; - - config_updater(); -} diff --git a/backend/binsrc/nfregex.cpp b/backend/binsrc/nfregex.cpp new file mode 100644 index 0000000..dacd6c1 --- /dev/null +++ b/backend/binsrc/nfregex.cpp @@ -0,0 +1,78 @@ +#include "regex/regex_rules.cpp" +#include "regex/regexfilter.cpp" +#include "classes/netfilter.cpp" +#include +#include + +using namespace std; +using namespace Firegex::Regex; +using Firegex::NfQueue::MultiThreadQueue; + +/* +Compile options: +USE_PIPES_FOR_BLOKING_QUEUE - use pipes instead of conditional variable, queue and mutex for blocking queue +*/ + + +void config_updater (){ + string line; + while (true){ + getline(cin, line); + if (cin.eof()){ + cerr << "[fatal] [updater] cin.eof()" << endl; + exit(EXIT_FAILURE); + } + if (cin.bad()){ + cerr << "[fatal] [updater] cin.bad()" << endl; + exit(EXIT_FAILURE); + } + cerr << "[info] [updater] Updating configuration with line " << line << endl; + istringstream config_stream(line); + vector raw_rules; + + while(!config_stream.eof()){ + string data; + config_stream >> data; + if (data != "" && data != "\n"){ + raw_rules.push_back(data); + } + } + try{ + regex_config.reset(new RegexRules(raw_rules, regex_config->stream_mode())); + cerr << "[info] [updater] Config update done to ver "<< regex_config->ver() << endl; + osyncstream(cout) << "ACK OK" << endl; + }catch(const std::exception& e){ + cerr << "[error] [updater] Failed to build new configuration!" << endl; + osyncstream(cout) << "ACK FAIL " << e.what() << endl; + } + } + +} + +int main(int argc, char *argv[]){ + int n_of_threads = 1; + char * n_threads_str = getenv("NTHREADS"); + if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str); + if(n_of_threads <= 0) n_of_threads = 1; + + char * matchmode = getenv("MATCH_MODE"); + bool stream_mode = true; + if (matchmode != nullptr && strcmp(matchmode, "block") == 0){ + stream_mode = false; + } + + bool fail_open = strcmp(getenv("FIREGEX_NFQUEUE_FAIL_OPEN"), "1") == 0; + + regex_config.reset(new RegexRules(stream_mode)); + + MultiThreadQueue queue_manager(n_of_threads); + osyncstream(cout) << "QUEUE " << queue_manager.queue_num() << endl; + cerr << "[info] [main] Queue: " << queue_manager.queue_num() << " threads assigned: " << n_of_threads << " stream mode: " << stream_mode << " fail open: " << fail_open << endl; + + thread qthr([&](){ + queue_manager.start(); + }); + config_updater(); + qthr.join(); + +} diff --git a/backend/binsrc/proxytun/proxytun.cpp b/backend/binsrc/proxytun/proxytun.cpp new file mode 100644 index 0000000..910a86b --- /dev/null +++ b/backend/binsrc/proxytun/proxytun.cpp @@ -0,0 +1,165 @@ +#ifndef PROXY_TUNNEL_CLASS_CPP +#define PROXY_TUNNEL_CLASS_CPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../classes/netfilter.cpp" +#include "stream_ctx.cpp" +#include "settings.cpp" + +using Tins::TCPIP::Stream; +using Tins::TCPIP::StreamFollower; +using namespace std; + +namespace Firegex { +namespace PyProxy { + +class PyProxyQueue: public NfQueue::ThreadNfQueue { + public: + stream_ctx sctx; + StreamFollower follower; + + struct { + bool matching_has_been_called = false; + bool already_closed = false; + bool result; + NfQueue::PktRequest* pkt; + } match_ctx; + + void before_loop() override { + follower.new_stream_callback(bind(on_new_stream, placeholders::_1, this)); + follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, this)); + } + + bool filter_action(NfQueue::PktRequest* pkt){ + shared_ptr conf = config; + + auto stream_search = sctx.streams_ctx.find(pkt->sid); + pyfilter_ctx* stream_match; + if (stream_search == sctx.streams_ctx.end()){ + // TODO: New pyfilter_ctx + }else{ + stream_match = stream_search->second; + } + + bool has_matched = false; + //TODO exec filtering action + + if (has_matched){ + // Say to firegex what filter has matched + //osyncstream(cout) << "BLOCKED " << rules_vector[match_res.matched] << endl; + return false; + } + return true; + } + + //If the stream has already been matched, drop all data, and try to close the connection + static void keep_fin_packet(PyProxyQueue* pkt){ + pkt->match_ctx.matching_has_been_called = true; + pkt->match_ctx.already_closed = true; + } + + static void on_data_recv(Stream& stream, PyProxyQueue* pkt, string data) { + pkt->match_ctx.matching_has_been_called = true; + pkt->match_ctx.already_closed = false; + bool result = pkt->filter_action(pkt->match_ctx.pkt); + if (!result){ + pkt->sctx.clean_stream_by_id(pkt->match_ctx.pkt->sid); + stream.client_data_callback(bind(keep_fin_packet, pkt)); + stream.server_data_callback(bind(keep_fin_packet, pkt)); + } + pkt->match_ctx.result = result; + } + + //Input data filtering + static void on_client_data(Stream& stream, PyProxyQueue* pkt) { + on_data_recv(stream, pkt, string(stream.client_payload().begin(), stream.client_payload().end())); + } + + //Server data filtering + static void on_server_data(Stream& stream, PyProxyQueue* pkt) { + on_data_recv(stream, pkt, string(stream.server_payload().begin(), stream.server_payload().end())); + } + + // A stream was terminated. The second argument is the reason why it was terminated + static void on_stream_close(Stream& stream, PyProxyQueue* pkt) { + stream_id stream_id = stream_id::make_identifier(stream); + pkt->sctx.clean_stream_by_id(stream_id); + } + + static void on_new_stream(Stream& stream, PyProxyQueue* pkt) { + stream.auto_cleanup_payloads(true); + if (stream.is_partial_stream()) { + //TODO take a decision about this... + stream.enable_recovery_mode(10 * 1024); + } + stream.client_data_callback(bind(on_client_data, placeholders::_1, pkt)); + stream.server_data_callback(bind(on_server_data, placeholders::_1, pkt)); + stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, pkt)); + } + + + void handle_next_packet(NfQueue::PktRequest* pkt) override{ + if (pkt->l4_proto != NfQueue::L4Proto::TCP){ + throw invalid_argument("Only TCP and UDP are supported"); + } + Tins::PDU* application_layer = pkt->tcp->inner_pdu(); + u_int16_t payload_size = 0; + if (application_layer != nullptr){ + payload_size = application_layer->size(); + } + match_ctx.matching_has_been_called = false; + match_ctx.pkt = pkt; + if (pkt->is_ipv6){ + follower.process_packet(*pkt->ipv6); + }else{ + follower.process_packet(*pkt->ipv4); + } + // Do an action only is an ordered packet has been received + if (match_ctx.matching_has_been_called){ + bool empty_payload = payload_size == 0; + //In this 2 cases we have to remove all data about the stream + if (!match_ctx.result || match_ctx.already_closed){ + sctx.clean_stream_by_id(pkt->sid); + //If the packet has data, we have to remove it + if (!empty_payload){ + Tins::PDU* data_layer = pkt->tcp->release_inner_pdu(); + if (data_layer != nullptr){ + delete data_layer; + } + } + //For the first matched data or only for data packets, we set FIN bit + //This only for client packets, because this will trigger server to close the connection + //Packets will be filtered anyway also if client don't send packets + if ((!match_ctx.result || !empty_payload) && pkt->is_input){ + pkt->tcp->set_flag(Tins::TCP::FIN,1); + pkt->tcp->set_flag(Tins::TCP::ACK,1); + pkt->tcp->set_flag(Tins::TCP::SYN,0); + } + //Send the edited packet to the kernel + return pkt->mangle(); + } + } + return pkt->accept(); + } + + ~PyProxyQueue() { + sctx.clean(); + } + +}; + +}} +#endif // PROXY_TUNNEL_CLASS_CPP \ No newline at end of file diff --git a/backend/binsrc/proxytun/settings.cpp b/backend/binsrc/proxytun/settings.cpp new file mode 100644 index 0000000..f4adae4 --- /dev/null +++ b/backend/binsrc/proxytun/settings.cpp @@ -0,0 +1,22 @@ +#ifndef PROXY_TUNNEL_SETTINGS_CPP +#define PROXY_TUNNEL_SETTINGS_CPP + +#include +#include + +using namespace std; + +class PyCodeConfig{ + public: + const vector code; + public: + PyCodeConfig(vector pycode): code(pycode){} + PyCodeConfig(): code(vector()){} + + ~PyCodeConfig(){} +}; + +shared_ptr config; + +#endif // PROXY_TUNNEL_SETTINGS_CPP + diff --git a/backend/binsrc/proxytun/stream_ctx.cpp b/backend/binsrc/proxytun/stream_ctx.cpp new file mode 100644 index 0000000..3057ac8 --- /dev/null +++ b/backend/binsrc/proxytun/stream_ctx.cpp @@ -0,0 +1,39 @@ + +#ifndef STREAM_CTX_CPP +#define STREAM_CTX_CPP + +#include +#include +#include + +using namespace std; + +typedef Tins::TCPIP::StreamIdentifier stream_id; + +struct pyfilter_ctx { + void * pyglob; // TODO python glob??? + string pycode; +}; + +typedef map matching_map; + +struct stream_ctx { + matching_map streams_ctx; + + void clean_stream_by_id(stream_id sid){ + auto stream_search = streams_ctx.find(sid); + if (stream_search != streams_ctx.end()){ + auto stream_match = stream_search->second; + //DEALLOC PY GLOB TODO + delete stream_match; + } + } + void clean(){ + for (auto ele: streams_ctx){ + //TODO dealloc ele.second.pyglob + delete ele.second; + } + } +}; + +#endif // STREAM_CTX_CPP \ No newline at end of file diff --git a/backend/binsrc/classes/regex_rules.cpp b/backend/binsrc/regex/regex_rules.cpp similarity index 91% rename from backend/binsrc/classes/regex_rules.cpp rename to backend/binsrc/regex/regex_rules.cpp index c01b2a2..c59ad45 100644 --- a/backend/binsrc/classes/regex_rules.cpp +++ b/backend/binsrc/regex/regex_rules.cpp @@ -1,14 +1,18 @@ +#ifndef REGEX_FILTER_CPP +#define REGEX_FILTER_CPP + #include #include #include -#include "../utils.hpp" +#include "../utils.cpp" #include #include +#include using namespace std; -#ifndef REGEX_FILTER_HPP -#define REGEX_FILTER_HPP +namespace Firegex { +namespace Regex { enum FilterDirection{ CTOS, STOC }; @@ -170,5 +174,16 @@ class RegexRules{ } }; -#endif // REGEX_FILTER_HPP +shared_ptr regex_config; + +void inline scratch_setup(regex_ruleset &conf, hs_scratch_t* & scratch){ + if (scratch == nullptr && conf.hs_db != nullptr){ + if (hs_alloc_scratch(conf.hs_db, &scratch) != HS_SUCCESS) { + throw invalid_argument("Cannot alloc scratch"); + } + } +} + +}} +#endif // REGEX_FILTER_CPP diff --git a/backend/binsrc/regex/regexfilter.cpp b/backend/binsrc/regex/regexfilter.cpp new file mode 100644 index 0000000..0ea15d2 --- /dev/null +++ b/backend/binsrc/regex/regexfilter.cpp @@ -0,0 +1,229 @@ +#ifndef REGEX_FILTER_CLASS_CPP +#define REGEX_FILTER_CLASS_CPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../classes/netfilter.cpp" +#include "stream_ctx.cpp" +#include "regex_rules.cpp" + +using namespace std; + + +namespace Firegex { +namespace Regex { + +using Tins::TCPIP::Stream; +using Tins::TCPIP::StreamFollower; + + + +class RegexNfQueue : public NfQueue::ThreadNfQueue { +public: + stream_ctx sctx; + u_int16_t latest_config_ver = 0; + StreamFollower follower; + struct { + bool matching_has_been_called = false; + bool already_closed = false; + bool result; + NfQueue::PktRequest* pkt; + } match_ctx; + + + bool filter_action(NfQueue::PktRequest* pkt){ + shared_ptr conf = regex_config; + + auto current_version = conf->ver(); + if (current_version != latest_config_ver){ + sctx.clean(); + latest_config_ver = current_version; + } + scratch_setup(conf->input_ruleset, sctx.in_scratch); + scratch_setup(conf->output_ruleset, sctx.out_scratch); + + hs_database_t* regex_matcher = pkt->is_input ? conf->input_ruleset.hs_db : conf->output_ruleset.hs_db; + if (regex_matcher == nullptr){ + return true; + } + + struct matched_data{ + unsigned int matched = 0; + bool has_matched = false; + } match_res; + + hs_error_t err; + hs_scratch_t* scratch_space = pkt->is_input ? sctx.in_scratch: 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 + }; + hs_stream_t* stream_match; + if (conf->stream_mode()){ + matching_map* match_map = pkt->is_input ? &sctx.in_hs_streams : &sctx.out_hs_streams; + auto stream_search = match_map->find(pkt->sid); + + 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"); + } + if (pkt->l4_proto == NfQueue::L4Proto::TCP){ + match_map->insert_or_assign(pkt->sid, stream_match); + } + }else{ + stream_match = stream_search->second; + } + err = hs_scan_stream( + stream_match,pkt->data, pkt->data_size, + 0, scratch_space, match_func, &match_res + ); + }else{ + err = hs_scan( + regex_matcher,pkt->data, pkt->data_size, + 0, scratch_space, match_func, &match_res + ); + } + if ( + pkt->l4_proto != NfQueue::L4Proto::TCP && conf->stream_mode() && + hs_close_stream(stream_match, scratch_space, nullptr, nullptr) != HS_SUCCESS + ){ + cerr << "[error] [filter_callback] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + if (err != HS_SUCCESS && err != HS_SCAN_TERMINATED) { + 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 = pkt->is_input ? conf->input_ruleset.regexes : conf->output_ruleset.regexes; + osyncstream(cout) << "BLOCKED " << rules_vector[match_res.matched] << endl; + return false; + } + return true; + } + + void handle_next_packet(NfQueue::PktRequest* pkt) override{ + bool empty_payload = pkt->data_size == 0; + if (pkt->tcp){ + match_ctx.matching_has_been_called = false; + match_ctx.pkt = pkt; + + if (pkt->ipv4){ + follower.process_packet(*pkt->ipv4); + }else{ + follower.process_packet(*pkt->ipv6); + } + + // Do an action only is an ordered packet has been received + if (match_ctx.matching_has_been_called){ + + //In this 2 cases we have to remove all data about the stream + if (!match_ctx.result || match_ctx.already_closed){ + sctx.clean_stream_by_id(pkt->sid); + //If the packet has data, we have to remove it + if (!empty_payload){ + Tins::PDU* data_layer = pkt->tcp->release_inner_pdu(); + if (data_layer != nullptr){ + delete data_layer; + } + } + //For the first matched data or only for data packets, we set FIN bit + //This only for client packets, because this will trigger server to close the connection + //Packets will be filtered anyway also if client don't send packets + if ((!match_ctx.result || !empty_payload) && pkt->is_input){ + pkt->tcp->set_flag(Tins::TCP::FIN,1); + pkt->tcp->set_flag(Tins::TCP::ACK,1); + pkt->tcp->set_flag(Tins::TCP::SYN,0); + } + //Send the edited packet to the kernel + return pkt->mangle(); + } + } + return pkt->accept(); + }else{ + if (!pkt->udp){ + throw invalid_argument("Only TCP and UDP are supported"); + } + if(empty_payload){ + return pkt->accept(); + }else if (filter_action(pkt)){ + return pkt->accept(); + }else{ + return pkt->drop(); + } + } + } + //If the stream has already been matched, drop all data, and try to close the connection + static void keep_fin_packet(RegexNfQueue* nfq){ + nfq->match_ctx.matching_has_been_called = true; + nfq->match_ctx.already_closed = true; + } + + static void on_data_recv(Stream& stream, RegexNfQueue* nfq, string data) { + nfq->match_ctx.matching_has_been_called = true; + nfq->match_ctx.already_closed = false; + bool result = nfq->filter_action(nfq->match_ctx.pkt); + if (!result){ + nfq->sctx.clean_stream_by_id(nfq->match_ctx.pkt->sid); + stream.client_data_callback(bind(keep_fin_packet, nfq)); + stream.server_data_callback(bind(keep_fin_packet, nfq)); + } + nfq->match_ctx.result = result; + } + + //Input data filtering + static void on_client_data(Stream& stream, RegexNfQueue* nfq) { + on_data_recv(stream, nfq, string(stream.client_payload().begin(), stream.client_payload().end())); + } + + //Server data filtering + static void on_server_data(Stream& stream, RegexNfQueue* nfq) { + on_data_recv(stream, nfq, string(stream.server_payload().begin(), stream.server_payload().end())); + } + + // A stream was terminated. The second argument is the reason why it was terminated + static void on_stream_close(Stream& stream, RegexNfQueue* nfq) { + stream_id stream_id = stream_id::make_identifier(stream); + nfq->sctx.clean_stream_by_id(stream_id); + } + + static void on_new_stream(Stream& stream, RegexNfQueue* nfq) { + stream.auto_cleanup_payloads(true); + if (stream.is_partial_stream()) { + stream.enable_recovery_mode(10 * 1024); + } + stream.client_data_callback(bind(on_client_data, placeholders::_1, nfq)); + stream.server_data_callback(bind(on_server_data, placeholders::_1, nfq)); + stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, nfq)); + } + + void before_loop() override{ + follower.new_stream_callback(bind(on_new_stream, placeholders::_1, this)); + follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, this)); + } + + ~RegexNfQueue(){ + sctx.clean(); + } + +}; + +}} +#endif // REGEX_FILTER_CLASS_CPP \ No newline at end of file diff --git a/backend/binsrc/regex/stream_ctx.cpp b/backend/binsrc/regex/stream_ctx.cpp new file mode 100644 index 0000000..dc1c3fe --- /dev/null +++ b/backend/binsrc/regex/stream_ctx.cpp @@ -0,0 +1,103 @@ + +#ifndef STREAM_CTX_CPP +#define STREAM_CTX_CPP + +#include +#include +#include +#include +#include +#include "regexfilter.cpp" + +using namespace std; + +namespace Firegex { +namespace Regex { + +typedef Tins::TCPIP::StreamIdentifier stream_id; +typedef map matching_map; + +#ifdef DEBUG +ostream& operator<<(ostream& os, const Tins::TCPIP::StreamIdentifier::address_type &sid){ + bool first_print = false; + for (auto ele: sid){ + if (first_print || ele){ + first_print = true; + os << (int)ele << "."; + } + } + return os; +} + +ostream& operator<<(ostream& os, const stream_id &sid){ + os << sid.max_address << ":" << sid.max_address_port << " -> " << sid.min_address << ":" << sid.min_address_port; + return os; +} +#endif + +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; + + 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; + } + } + + void clean_stream_by_id(stream_id sid){ + auto stream_search = in_hs_streams.find(sid); + hs_stream_t* stream_match; + if (stream_search != in_hs_streams.end()){ + stream_match = stream_search->second; + if (hs_close_stream(stream_match, 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"); + } + in_hs_streams.erase(stream_search); + } + + stream_search = out_hs_streams.find(sid); + if (stream_search != out_hs_streams.end()){ + stream_match = stream_search->second; + if (hs_close_stream(stream_match, 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"); + } + out_hs_streams.erase(stream_search); + } + } + + void clean(){ + if (in_scratch){ + for(auto ele: in_hs_streams){ + if (hs_close_stream(ele.second, 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"); + } + } + in_hs_streams.clear(); + } + + if (out_scratch){ + for(auto ele: out_hs_streams){ + if (hs_close_stream(ele.second, 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"); + } + } + out_hs_streams.clear(); + } + clean_scratches(); + } +}; + +}} +#endif // STREAM_CTX_CPP \ No newline at end of file diff --git a/backend/binsrc/utils.cpp b/backend/binsrc/utils.cpp new file mode 100644 index 0000000..a4d889a --- /dev/null +++ b/backend/binsrc/utils.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include + +#ifndef UTILS_CPP +#define UTILS_CPP + +bool unhexlify(std::string const &hex, std::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(), nullptr, 16); + newString.push_back(chr); + } + return true; + } + catch (...){ + return false; + } +} + +#ifdef USE_PIPES_FOR_BLOKING_QUEUE + +template +class BlockingQueue +{ +private: + int pipefd[2]; +public: + BlockingQueue(){ + if (pipe(pipefd) == -1) { + throw std::runtime_error("pipe"); + } + } + + void put(T new_value) + { + if (write(pipefd[1], &new_value, sizeof(T)) == -1) { + throw std::runtime_error("write"); + } + } + void take(T& value) + { + if (read(pipefd[0], &value, sizeof(T)) == -1) { + throw std::runtime_error("read"); + } + } +}; + +#else + +template //same of kernel nfqueue max +class BlockingQueue +{ +private: + std::mutex mut; + std::queue private_std_queue; + std::condition_variable condNotEmpty; + std::condition_variable condNotFull; + size_t count; // Guard with Mutex +public: + + void put(T new_value) + { + + std::unique_lock lk(mut); + //Condition takes a unique_lock and waits given the false condition + condNotFull.wait(lk,[this]{ + if (count == MAX) { + return false; + }else{ + return true; + } + + }); + private_std_queue.push(new_value); + count++; + condNotEmpty.notify_one(); + } + void take(T& value) + { + std::unique_lock lk(mut); + //Condition takes a unique_lock and waits given the false condition + condNotEmpty.wait(lk,[this]{return !private_std_queue.empty();}); + value=private_std_queue.front(); + private_std_queue.pop(); + count--; + condNotFull.notify_one(); + } +}; + +#endif + +#endif // UTILS_CPP \ No newline at end of file diff --git a/backend/binsrc/utils.hpp b/backend/binsrc/utils.hpp deleted file mode 100644 index b61ef22..0000000 --- a/backend/binsrc/utils.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#include -#include - -#ifndef UTILS_HPP -#define UTILS_HPP - -bool unhexlify(std::string const &hex, std::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(), nullptr, 16); - newString.push_back(chr); - } - return true; - } - catch (...){ - return false; - } -} - -#endif \ No newline at end of file diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index b8e84cd..b329d1d 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -4,5 +4,3 @@ chown nobody -R /execute/ exec capsh --caps="cap_net_admin+eip cap_setpcap,cap_setuid,cap_setgid+ep" \ --keep=1 --user=nobody --addamb=cap_net_admin -- -c "python3 /execute/app.py DOCKER" - - diff --git a/backend/modules/firewall/firewall.py b/backend/modules/firewall/firewall.py index b5bb292..171e34f 100644 --- a/backend/modules/firewall/firewall.py +++ b/backend/modules/firewall/firewall.py @@ -130,6 +130,7 @@ class FirewallManager: def allow_dhcp(self): return self.db.get("allow_dhcp", "1") == "1" - @drop_invalid.setter - def allow_dhcp_set(self, value): + @allow_dhcp.setter + def allow_dhcp(self, value): self.db.set("allow_dhcp", "1" if value else "0") + diff --git a/backend/modules/nfproxy/__init__.py b/backend/modules/nfproxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/modules/nfproxy/firegex.py b/backend/modules/nfproxy/firegex.py new file mode 100644 index 0000000..37055c3 --- /dev/null +++ b/backend/modules/nfproxy/firegex.py @@ -0,0 +1,121 @@ +from modules.nfproxy.nftables import FiregexTables +from utils import run_func +from modules.nfproxy.models import Service, PyFilter +import os +import asyncio +from utils import DEBUG +import traceback +from fastapi import HTTPException + +nft = FiregexTables() + +class FiregexInterceptor: + + def __init__(self): + self.srv:Service + self._stats_updater_cb:callable + self.filter_map_lock:asyncio.Lock + self.filter_map: dict[str, PyFilter] + self.pyfilters: set[PyFilter] + self.update_config_lock:asyncio.Lock + self.process:asyncio.subprocess.Process + self.update_task: asyncio.Task + self.ack_arrived = False + self.ack_status = None + self.ack_fail_what = "" + self.ack_lock = asyncio.Lock() + + async def _call_stats_updater_callback(self, filter: PyFilter): + if self._stats_updater_cb: + await run_func(self._stats_updater_cb(filter)) + + @classmethod + async def start(cls, srv: Service, stats_updater_cb:callable): + self = cls() + self._stats_updater_cb = stats_updater_cb + self.srv = srv + self.filter_map_lock = asyncio.Lock() + self.update_config_lock = asyncio.Lock() + queue_range = await self._start_binary() + self.update_task = asyncio.create_task(self.update_stats()) + nft.add(self.srv, queue_range) + if not self.ack_lock.locked(): + await self.ack_lock.acquire() + return self + + async def _start_binary(self): + proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../cpproxy") + self.process = await asyncio.create_subprocess_exec( + proxy_binary_path, + stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, + env={"NTHREADS": os.getenv("NTHREADS","1")}, + ) + line_fut = self.process.stdout.readuntil() + try: + line_fut = await asyncio.wait_for(line_fut, timeout=3) + except asyncio.TimeoutError: + self.process.kill() + raise Exception("Invalid binary output") + line = line_fut.decode() + if line.startswith("QUEUE "): + params = line.split() + return (int(params[1]), int(params[1])) + else: + self.process.kill() + raise Exception("Invalid binary output") + + async def update_stats(self): + try: + while True: + line = (await self.process.stdout.readuntil()).decode() + if DEBUG: + print(line) + if line.startswith("BLOCKED "): + filter_id = line.split()[1] + async with self.filter_map_lock: + if filter_id in self.filter_map: + self.filter_map[filter_id].blocked_packets+=1 + await self.filter_map[filter_id].update() + if line.startswith("EDITED "): + filter_id = line.split()[1] + async with self.filter_map_lock: + if filter_id in self.filter_map: + self.filter_map[filter_id].edited_packets+=1 + await self.filter_map[filter_id].update() + if line.startswith("ACK "): + self.ack_arrived = True + self.ack_status = line.split()[1].upper() == "OK" + if not self.ack_status: + self.ack_fail_what = " ".join(line.split()[2:]) + self.ack_lock.release() + except asyncio.CancelledError: + pass + except asyncio.IncompleteReadError: + pass + except Exception: + traceback.print_exc() + + async def stop(self): + self.update_task.cancel() + if self.process and self.process.returncode is None: + self.process.kill() + + async def _update_config(self, filters_codes): + async with self.update_config_lock: + # TODO write compiled code correctly + # self.process.stdin.write((" ".join(filters_codes)+"\n").encode()) + await self.process.stdin.drain() + try: + async with asyncio.timeout(3): + await self.ack_lock.acquire() + except TimeoutError: + pass + if not self.ack_arrived or not self.ack_status: + raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}") + + async def reload(self, filters:list[PyFilter]): + async with self.filter_map_lock: + self.filter_map = self.compile_filters(filters) + # TODO COMPILE CODE + #await self._update_config(filters_codes) TODO pass the compiled code + diff --git a/backend/modules/nfproxy/firewall.py b/backend/modules/nfproxy/firewall.py new file mode 100644 index 0000000..59002d9 --- /dev/null +++ b/backend/modules/nfproxy/firewall.py @@ -0,0 +1,119 @@ +import asyncio +from modules.nfproxy.firegex import FiregexInterceptor +from modules.nfproxy.nftables import FiregexTables, FiregexFilter +from modules.nfproxy.models import Service, PyFilter +from utils.sqlite import SQLite + +class STATUS: + STOP = "stop" + ACTIVE = "active" + +nft = FiregexTables() + +class ServiceManager: + def __init__(self, srv: Service, db): + self.srv = srv + self.db = db + self.status = STATUS.STOP + self.filters: dict[int, FiregexFilter] = {} + self.lock = asyncio.Lock() + self.interceptor = None + + async def _update_filters_from_db(self): + pyfilters = [ + PyFilter.from_dict(ele) for ele in + self.db.query("SELECT * FROM pyfilter WHERE service_id = ? AND active=1;", self.srv.id) + ] + #Filter check + old_filters = set(self.filters.keys()) + new_filters = set([f.id for f in pyfilters]) + #remove old filters + for f in old_filters: + if f not in new_filters: + del self.filters[f] + #add new filters + for f in new_filters: + if f not in old_filters: + self.filters[f] = [ele for ele in pyfilters if ele.id == f][0] + 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) + + async def next(self,to): + async with self.lock: + if (self.status, to) == (STATUS.ACTIVE, STATUS.STOP): + await self.stop() + self._set_status(to) + # PAUSE -> ACTIVE + elif (self.status, to) == (STATUS.STOP, STATUS.ACTIVE): + await self.restart() + + def _stats_updater(self,filter:PyFilter): + self.db.query("UPDATE pyfilter SET blocked_packets = ?, edited_packets = ? WHERE filter_id = ?;", filter.blocked_packets, filter.edited_packets, filter.id) + + def _set_status(self,status): + self.status = status + self.__update_status_db(status) + + async def start(self): + if not self.interceptor: + nft.delete(self.srv) + self.interceptor = await FiregexInterceptor.start(self.srv, self._stats_updater) + await self._update_filters_from_db() + self._set_status(STATUS.ACTIVE) + + async def stop(self): + nft.delete(self.srv) + if self.interceptor: + await self.interceptor.stop() + self.interceptor = None + + async def restart(self): + await self.stop() + await self.start() + + async def update_filters(self): + async with self.lock: + await self._update_filters_from_db() + +class FirewallManager: + def __init__(self, db:SQLite): + self.db = db + self.service_table: dict[str, ServiceManager] = {} + self.lock = asyncio.Lock() + + async def close(self): + for key in list(self.service_table.keys()): + await self.remove(key) + + async def remove(self,srv_id): + async with self.lock: + if srv_id in self.service_table: + await self.service_table[srv_id].next(STATUS.STOP) + del self.service_table[srv_id] + + async def init(self): + nft.init() + await self.reload() + + async def reload(self): + async with self.lock: + for srv in self.db.query('SELECT * FROM services;'): + srv = Service.from_dict(srv) + if srv.id in self.service_table: + continue + self.service_table[srv.id] = ServiceManager(srv, self.db) + await self.service_table[srv.id].next(srv.status) + + def get(self,srv_id) -> ServiceManager: + if srv_id in self.service_table: + return self.service_table[srv_id] + else: + raise ServiceNotFoundException() + +class ServiceNotFoundException(Exception): + pass + + diff --git a/backend/modules/nfproxy/models.py b/backend/modules/nfproxy/models.py new file mode 100644 index 0000000..ba048c4 --- /dev/null +++ b/backend/modules/nfproxy/models.py @@ -0,0 +1,26 @@ + +class Service: + def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, **other): + self.id = service_id + self.status = status + self.port = port + self.name = name + self.proto = proto + self.ip_int = ip_int + + @classmethod + def from_dict(cls, var: dict): + return cls(**var) + + +class PyFilter: + def __init__(self, filter_id:int, name: str, blocked_packets: int, edited_packets: int, active: bool, **other): + self.id = filter_id + self.name = name + self.blocked_packets = blocked_packets + self.edited_packets = edited_packets + self.active = active + + @classmethod + def from_dict(cls, var: dict): + return cls(**var) diff --git a/backend/modules/nfproxy/nftables.py b/backend/modules/nfproxy/nftables.py new file mode 100644 index 0000000..eafa129 --- /dev/null +++ b/backend/modules/nfproxy/nftables.py @@ -0,0 +1,109 @@ +from modules.nfproxy.models import Service +from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json + +class FiregexFilter: + def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int): + self.id = id + self.target = target + self.proto = proto + self.port = int(port) + self.ip_int = str(ip_int) + + def __eq__(self, o: object) -> bool: + if isinstance(o, FiregexFilter) or isinstance(o, Service): + return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) + return False + +class FiregexTables(NFTableManager): + input_chain = "nfproxy_input" + output_chain = "nfproxy_output" + + def __init__(self): + super().__init__([ + {"add":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":self.input_chain, + "type":"filter", + "hook":"prerouting", + "prio":-150, + "policy":"accept" + }}}, + {"add":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":self.output_chain, + "type":"filter", + "hook":"postrouting", + "prio":-150, + "policy":"accept" + }}} + ],[ + {"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}}, + {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}}, + {"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}}, + {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}}, + ]) + + def add(self, srv:Service, queue_range): + + for ele in self.get(): + if ele.__eq__(srv): + return + + 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)}}, + {"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}}, + {"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)}}, + {"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}}, + {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} + ] + }}} + ) + + + def get(self) -> list[FiregexFilter]: + res = [] + for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]): + ip_int = None + if isinstance(filter["expr"][0]["match"]["right"],str): + ip_int = str(ip_parse(filter["expr"][0]["match"]["right"])) + else: + ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}' + res.append(FiregexFilter( + target=filter["chain"], + id=int(filter["handle"]), + proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"], + port=filter["expr"][1]["match"]["right"], + ip_int=ip_int + )) + return res + + def delete(self, srv:Service): + for filter in self.get(): + if filter.__eq__(srv): + self.cmd({ "delete":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": filter.target, + "handle": filter.id + }}}) + \ No newline at end of file diff --git a/backend/modules/nfregex/firegex.py b/backend/modules/nfregex/firegex.py index 71afcf8..026b832 100644 --- a/backend/modules/nfregex/firegex.py +++ b/backend/modules/nfregex/firegex.py @@ -84,11 +84,15 @@ class FiregexInterceptor: return self async def _start_binary(self): - proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../cppqueue") + proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../cppregex") self.process = await asyncio.create_subprocess_exec( proxy_binary_path, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, - env={"MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block", "NTHREADS": os.getenv("NTHREADS","1")}, + env={ + "MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block", + "NTHREADS": os.getenv("NTHREADS","1"), + "FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0", + }, ) line_fut = self.process.stdout.readuntil() try: @@ -97,9 +101,9 @@ class FiregexInterceptor: self.process.kill() raise Exception("Invalid binary output") line = line_fut.decode() - if line.startswith("QUEUES "): + if line.startswith("QUEUE "): params = line.split() - return (int(params[1]), int(params[2])) + return (int(params[1]), int(params[1])) else: self.process.kill() raise Exception("Invalid binary output") diff --git a/backend/modules/nfregex/models.py b/backend/modules/nfregex/models.py index 0c36890..c06daa6 100644 --- a/backend/modules/nfregex/models.py +++ b/backend/modules/nfregex/models.py @@ -1,13 +1,14 @@ import base64 class Service: - def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, **other): + def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, fail_open: bool, **other): self.id = service_id self.status = status self.port = port self.name = name self.proto = proto self.ip_int = ip_int + self.fail_open = fail_open @classmethod def from_dict(cls, var: dict): diff --git a/backend/modules/nfregex/nftables.py b/backend/modules/nfregex/nftables.py index ce0088a..34ed844 100644 --- a/backend/modules/nfregex/nftables.py +++ b/backend/modules/nfregex/nftables.py @@ -48,10 +48,12 @@ class FiregexTables(NFTableManager): def add(self, srv:Service, queue_range): for ele in self.get(): - if ele.__eq__(srv): return + if ele.__eq__(srv): + return init, end = queue_range - if init > end: init, end = end, init + if init > end: + init, end = end, init self.cmd( { "insert":{ "rule": { "family": "inet", diff --git a/backend/requirements.txt b/backend/requirements.txt index 9678711..024f520 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,5 +4,5 @@ uvicorn[standard] passlib[bcrypt] psutil python-jose[cryptography] -fastapi-socketio +python-socketio #git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py diff --git a/backend/routers/firewall.py b/backend/routers/firewall.py index 8801db9..a16560c 100644 --- a/backend/routers/firewall.py +++ b/backend/routers/firewall.py @@ -24,7 +24,7 @@ db = SQLite('db/firewall-rules.db', { 'action': 'VARCHAR(10) NOT NULL CHECK (action IN ("accept", "drop", "reject"))', }, 'QUERY':[ - "CREATE UNIQUE INDEX IF NOT EXISTS unique_rules ON rules (proto, src, dst, port_src_from, port_src_to, port_dst_from, port_dst_to, mode);" + "CREATE UNIQUE INDEX IF NOT EXISTS unique_rules ON rules (proto, src, dst, port_src_from, port_src_to, port_dst_from, port_dst_to, mode, `table`);" ] }) @@ -71,7 +71,7 @@ async def get_settings(): """Get the firewall settings""" return firewall.settings -@app.post("/settings/set", response_model=StatusMessageModel) +@app.put("/settings", response_model=StatusMessageModel) async def set_settings(form: FirewallSettings): """Set the firewall settings""" firewall.settings = form @@ -86,13 +86,13 @@ async def get_rule_list(): "enabled": firewall.enabled } -@app.get('/enable', response_model=StatusMessageModel) +@app.post('/enable', response_model=StatusMessageModel) async def enable_firewall(): """Request enabling the firewall""" firewall.enabled = True return await apply_changes() -@app.get('/disable', response_model=StatusMessageModel) +@app.post('/disable', response_model=StatusMessageModel) async def disable_firewall(): """Request disabling the firewall""" firewall.enabled = False @@ -128,9 +128,9 @@ def parse_and_check_rule(rule:RuleModel): return rule -@app.post('/rules/set', response_model=StatusMessageModel) +@app.post('/rules', response_model=StatusMessageModel) async def add_new_service(form: RuleFormAdd): - """Add a new service""" + """Edit rule table""" rules = [parse_and_check_rule(ele) for ele in form.rules] try: db.queries(["DELETE FROM rules"]+ diff --git a/backend/routers/nfproxy.py b/backend/routers/nfproxy.py new file mode 100644 index 0000000..4cbb825 --- /dev/null +++ b/backend/routers/nfproxy.py @@ -0,0 +1,259 @@ +import secrets +import sqlite3 +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from modules.nfproxy.nftables import FiregexTables +from modules.nfproxy.firewall import STATUS, FirewallManager +from utils.sqlite import SQLite +from utils import ip_parse, refactor_name, socketio_emit, PortType +from utils.models import ResetRequest, StatusMessageModel + +class ServiceModel(BaseModel): + service_id: str + status: str + port: PortType + name: str + proto: str + ip_int: str + n_filters: int + edited_packets: int + blocked_packets: int + +class RenameForm(BaseModel): + name:str + +class PyFilterModel(BaseModel): + filter_id: int + name: str + blocked_packets: int + edited_packets: int + active: bool + +class ServiceAddForm(BaseModel): + name: str + port: PortType + proto: str + ip_int: str + +class ServiceAddResponse(BaseModel): + status:str + service_id: str|None = None + +#app = APIRouter() Not released in this version + +db = SQLite('db/nft-pyfilters.db', { + 'services': { + 'service_id': 'VARCHAR(100) PRIMARY KEY', + 'status': 'VARCHAR(100) NOT NULL', + 'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)', + 'name': 'VARCHAR(100) NOT NULL UNIQUE', + 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))', + 'ip_int': 'VARCHAR(100) NOT NULL', + }, + 'pyfilter': { + 'filter_id': 'INTEGER PRIMARY KEY', + 'name': 'VARCHAR(100) NOT NULL', + 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', + 'edited_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', + 'service_id': 'VARCHAR(100) NOT NULL', + 'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1)) DEFAULT 1', + 'FOREIGN KEY (service_id)':'REFERENCES services (service_id)', + }, + 'QUERY':[ + "CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (port, ip_int, proto);", + "CREATE UNIQUE INDEX IF NOT EXISTS unique_pyfilter_service ON pyfilter (name, service_id);" + ] +}) + +async def refresh_frontend(additional:list[str]=[]): + await socketio_emit(["nfproxy"]+additional) + +async def reset(params: ResetRequest): + if not params.delete: + db.backup() + await firewall.close() + FiregexTables().reset() + if params.delete: + db.delete() + db.init() + else: + db.restore() + try: + await firewall.init() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +async def startup(): + db.init() + try: + await firewall.init() + except Exception as e: + print("WARNING cannot start firewall:", e) + +async def shutdown(): + db.backup() + await firewall.close() + db.disconnect() + db.restore() + +def gen_service_id(): + while True: + res = secrets.token_hex(8) + if len(db.query('SELECT 1 FROM services WHERE service_id = ?;', res)) == 0: + break + return res + +firewall = FirewallManager(db) + +@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 service_id, + s.status status, + s.port port, + s.name name, + s.proto proto, + s.ip_int ip_int, + COUNT(f.filter_id) n_filters, + COALESCE(SUM(f.blocked_packets),0) blocked_packets, + COALESCE(SUM(f.edited_packets),0) edited_packets + FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id + GROUP BY s.service_id; + """) + +@app.get('/services/{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 service_id, + s.status status, + s.port port, + s.name name, + s.proto proto, + s.ip_int ip_int, + COUNT(f.filter_id) n_filters, + COALESCE(SUM(f.blocked_packets),0) blocked_packets, + COALESCE(SUM(f.edited_packets),0) edited_packets + FROM services s LEFT JOIN pyfilter f ON s.service_id = f.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.post('/services/{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.post('/services/{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.delete('/services/{service_id}', 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 pyfilter WHERE service_id = ?;', service_id) + await firewall.remove(service_id) + await refresh_frontend() + return {'status': 'ok'} + +@app.put('/services/{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="This name is already used") + await refresh_frontend() + return {'status': 'ok'} + +@app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel]) +async def get_service_pyfilter_list(service_id: str): + """Get the list of the pyfilters 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 + filter_id, name, blocked_packets, edited_packets, active + FROM pyfilter WHERE service_id = ?; + """, service_id) + +@app.get('/pyfilters/{filter_id}', response_model=PyFilterModel) +async def get_pyfilter_by_id(filter_id: int): + """Get pyfilter info using his id""" + res = db.query(""" + SELECT + filter_id, name, blocked_packets, edited_packets, active + FROM pyfilter WHERE filter_id = ?; + """, filter_id) + if len(res) == 0: + raise HTTPException(status_code=400, detail="This filter does not exists!") + return res[0] + +@app.delete('/pyfilters/{filter_id}', response_model=StatusMessageModel) +async def pyfilter_delete(filter_id: int): + """Delete a pyfilter using his id""" + res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) + if len(res) != 0: + db.query('DELETE FROM pyfilter WHERE filter_id = ?;', filter_id) + await firewall.get(res[0]["service_id"]).update_filters() + await refresh_frontend() + + return {'status': 'ok'} + +@app.post('/pyfilters/{filter_id}/enable', response_model=StatusMessageModel) +async def pyfilter_enable(filter_id: int): + """Request the enabling of a pyfilter""" + res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) + if len(res) != 0: + db.query('UPDATE pyfilter SET active=1 WHERE filter_id = ?;', filter_id) + await firewall.get(res[0]["service_id"]).update_filters() + await refresh_frontend() + return {'status': 'ok'} + +@app.post('/pyfilters/{filter_id}/disable', response_model=StatusMessageModel) +async def pyfilter_disable(filter_id: int): + """Request the deactivation of a pyfilter""" + res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) + if len(res) != 0: + db.query('UPDATE pyfilter SET active=0 WHERE filter_id = ?;', filter_id) + await firewall.get(res[0]["service_id"]).update_filters() + await refresh_frontend() + return {'status': 'ok'} + +@app.post('/services', response_model=ServiceAddResponse) +async def add_new_service(form: ServiceAddForm): + """Add a new service""" + try: + form.ip_int = ip_parse(form.ip_int) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid address") + if form.proto not in ["tcp", "http"]: + raise HTTPException(status_code=400, detail="Invalid protocol") + srv_id = None + try: + srv_id = gen_service_id() + db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?)", + srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int) + except sqlite3.IntegrityError: + raise HTTPException(status_code=400, detail="This type of service already exists") + await firewall.reload() + await refresh_frontend() + return {'status': 'ok', 'service_id': srv_id} + +#TODO check all the APIs and add +# 1. API to change the python filter file +# 2. a socketio mechanism to lock the previous feature \ No newline at end of file diff --git a/backend/routers/nfregex.py b/backend/routers/nfregex.py index f5de063..744b6f2 100644 --- a/backend/routers/nfregex.py +++ b/backend/routers/nfregex.py @@ -19,10 +19,17 @@ class ServiceModel(BaseModel): ip_int: str n_regex: int n_packets: int + fail_open: bool class RenameForm(BaseModel): name:str +class SettingsForm(BaseModel): + port: PortType|None = None + proto: str|None = None + ip_int: str|None = None + fail_open: bool|None = None + class RegexModel(BaseModel): regex:str mode:str @@ -44,6 +51,7 @@ class ServiceAddForm(BaseModel): port: PortType proto: str ip_int: str + fail_open: bool = False class ServiceAddResponse(BaseModel): status:str @@ -59,6 +67,7 @@ db = SQLite('db/nft-regex.db', { 'name': 'VARCHAR(100) NOT NULL UNIQUE', 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))', 'ip_int': 'VARCHAR(100) NOT NULL', + 'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1' }, 'regexes': { 'regex': 'TEXT NOT NULL', @@ -128,13 +137,14 @@ async def get_service_list(): s.name name, s.proto proto, s.ip_int ip_int, + s.fail_open fail_open, COUNT(r.regex_id) n_regex, COALESCE(SUM(r.blocked_packets),0) n_packets FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id GROUP BY s.service_id; """) -@app.get('/service/{service_id}', response_model=ServiceModel) +@app.get('/services/{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(""" @@ -145,6 +155,7 @@ async def get_service_by_id(service_id: str): s.name name, s.proto proto, s.ip_int ip_int, + s.fail_open fail_open, COUNT(r.regex_id) n_regex, COALESCE(SUM(r.blocked_packets),0) n_packets FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id @@ -154,21 +165,21 @@ async def get_service_by_id(service_id: str): raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] -@app.get('/service/{service_id}/stop', response_model=StatusMessageModel) +@app.post('/services/{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}/start', response_model=StatusMessageModel) +@app.post('/services/{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) +@app.delete('/services/{service_id}', 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) @@ -177,7 +188,7 @@ async def service_delete(service_id: str): await refresh_frontend() return {'status': 'ok'} -@app.post('/service/{service_id}/rename', response_model=StatusMessageModel) +@app.put('/services/{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) @@ -190,7 +201,46 @@ async def service_rename(service_id: str, form: RenameForm): await refresh_frontend() return {'status': 'ok'} -@app.get('/service/{service_id}/regexes', response_model=list[RegexModel]) +@app.put('/services/{service_id}/settings', response_model=StatusMessageModel) +async def service_settings(service_id: str, form: SettingsForm): + """Request to change the settings of a specific service (will cause a restart)""" + + if form.proto is not None and form.proto not in ["tcp", "udp"]: + raise HTTPException(status_code=400, detail="Invalid protocol") + + if form.port is not None and (form.port < 1 or form.port > 65535): + raise HTTPException(status_code=400, detail="Invalid port") + + if form.ip_int is not None: + try: + form.ip_int = ip_parse(form.ip_int) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid address") + + keys = [] + values = [] + + for key, value in form.model_dump(exclude_none=True).items(): + keys.append(key) + values.append(value) + + if len(keys) == 0: + raise HTTPException(status_code=400, detail="No settings to change provided") + + try: + db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id) + except sqlite3.IntegrityError: + raise HTTPException(status_code=400, detail="A service with these settings already exists") + + old_status = firewall.get(service_id).status + await firewall.remove(service_id) + await firewall.reload() + await firewall.get(service_id).next(old_status) + + await refresh_frontend() + return {'status': 'ok'} + +@app.get('/services/{service_id}/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): @@ -202,7 +252,7 @@ async def get_service_regexe_list(service_id: str): FROM regexes WHERE service_id = ?; """, service_id) -@app.get('/regex/{regex_id}', response_model=RegexModel) +@app.get('/regexes/{regex_id}', response_model=RegexModel) async def get_regex_by_id(regex_id: int): """Get regex info using his id""" res = db.query(""" @@ -215,7 +265,7 @@ async def get_regex_by_id(regex_id: int): raise HTTPException(status_code=400, detail="This regex does not exists!") return res[0] -@app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) +@app.delete('/regexes/{regex_id}', 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) @@ -226,7 +276,7 @@ async def regex_delete(regex_id: int): return {'status': 'ok'} -@app.get('/regex/{regex_id}/enable', response_model=StatusMessageModel) +@app.post('/regexes/{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) @@ -236,7 +286,7 @@ async def regex_enable(regex_id: int): await refresh_frontend() return {'status': 'ok'} -@app.get('/regex/{regex_id}/disable', response_model=StatusMessageModel) +@app.post('/regexes/{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) @@ -246,7 +296,7 @@ async def regex_disable(regex_id: int): await refresh_frontend() return {'status': 'ok'} -@app.post('/regexes/add', response_model=StatusMessageModel) +@app.post('/regexes', response_model=StatusMessageModel) async def add_new_regex(form: RegexAddForm): """Add a new regex""" try: @@ -263,7 +313,7 @@ async def add_new_regex(form: RegexAddForm): await refresh_frontend() return {'status': 'ok'} -@app.post('/services/add', response_model=ServiceAddResponse) +@app.post('/services', response_model=ServiceAddResponse) async def add_new_service(form: ServiceAddForm): """Add a new service""" try: @@ -275,8 +325,8 @@ async def add_new_service(form: ServiceAddForm): srv_id = None try: srv_id = gen_service_id() - db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?)", - srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int) + db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open) VALUES (?, ?, ?, ?, ?, ?, ?)", + srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open) except sqlite3.IntegrityError: raise HTTPException(status_code=400, detail="This type of service already exists") await firewall.reload() @@ -299,7 +349,8 @@ async def metrics(): FROM regexes r LEFT JOIN services s ON s.service_id = r.service_id; """) metrics = [] - sanitize = lambda s : s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + def sanitize(s): + return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') for stat in stats: props = f'service_name="{sanitize(stat["name"])}",regex="{sanitize(b64decode(stat["regex"]).decode())}",mode="{stat["mode"]}",is_case_sensitive="{stat["is_case_sensitive"]}"' metrics.append(f'firegex_blocked_packets{{{props}}} {stat["blocked_packets"]}') diff --git a/backend/routers/porthijack.py b/backend/routers/porthijack.py index 8fd3c54..7899ef4 100644 --- a/backend/routers/porthijack.py +++ b/backend/routers/porthijack.py @@ -92,7 +92,7 @@ async def get_service_list(): """Get the list of existent firegex services""" return db.query("SELECT service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services;") -@app.get('/service/{service_id}', response_model=ServiceModel) +@app.get('/services/{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 service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services WHERE service_id = ?;", service_id) @@ -100,21 +100,21 @@ async def get_service_by_id(service_id: str): raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] -@app.get('/service/{service_id}/stop', response_model=StatusMessageModel) +@app.post('/services/{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).disable() await refresh_frontend() return {'status': 'ok'} -@app.get('/service/{service_id}/start', response_model=StatusMessageModel) +@app.post('/services/{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).enable() await refresh_frontend() return {'status': 'ok'} -@app.get('/service/{service_id}/delete', response_model=StatusMessageModel) +@app.delete('/services/{service_id}', 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) @@ -122,7 +122,7 @@ async def service_delete(service_id: str): await refresh_frontend() return {'status': 'ok'} -@app.post('/service/{service_id}/rename', response_model=StatusMessageModel) +@app.put('/services/{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) @@ -139,7 +139,7 @@ class ChangeDestination(BaseModel): ip_dst: str proxy_port: PortType -@app.post('/service/{service_id}/change-destination', response_model=StatusMessageModel) +@app.put('/services/{service_id}/change-destination', response_model=StatusMessageModel) async def service_change_destination(service_id: str, form: ChangeDestination): """Request to change the proxy destination of the service""" @@ -162,7 +162,7 @@ async def service_change_destination(service_id: str, form: ChangeDestination): await refresh_frontend() return {'status': 'ok'} -@app.post('/services/add', response_model=ServiceAddResponse) +@app.post('/services', response_model=ServiceAddResponse) async def add_new_service(form: ServiceAddForm): """Add a new service""" try: diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py index 52e753d..1d9c23a 100644 --- a/backend/utils/__init__.py +++ b/backend/utils/__init__.py @@ -5,13 +5,13 @@ import socket import psutil import sys import nftables -from fastapi_socketio import SocketManager +from socketio import AsyncServer from fastapi import Path from typing import Annotated LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) -socketio:SocketManager = None +socketio:AsyncServer = None ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ROUTERS_DIR = os.path.join(ROOT_DIR,"routers") @@ -19,7 +19,7 @@ ON_DOCKER = "DOCKER" in sys.argv DEBUG = "DEBUG" in sys.argv FIREGEX_PORT = int(os.getenv("PORT","4444")) JWT_ALGORITHM: str = "HS256" -API_VERSION = "3.0.0" +API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" PortType = Annotated[int, Path(gt=0, lt=65536)] diff --git a/backend/utils/loader.py b/backend/utils/loader.py index 179c5d8..435c8c2 100644 --- a/backend/utils/loader.py +++ b/backend/utils/loader.py @@ -58,15 +58,18 @@ class RouterModule(): def get_router_modules(): res: list[RouterModule] = [] for route in list_routers(): - module = getattr(__import__(f"routers.{route}"), route, None) - if module: - res.append(RouterModule( - router=getattr(module, "app", None), - reset=getattr(module, "reset", None), - startup=getattr(module, "startup", None), - shutdown=getattr(module, "shutdown", None), - name=route - )) + try: + module = getattr(__import__(f"routers.{route}"), route, None) + if module: + res.append(RouterModule( + router=getattr(module, "app", None), + reset=getattr(module, "reset", None), + startup=getattr(module, "startup", None), + shutdown=getattr(module, "shutdown", None), + name=route + )) + except Exception as e: + print(f"Router {route} failed to load: {e}") return res def load_routers(app): @@ -74,6 +77,9 @@ def load_routers(app): for router in get_router_modules(): if router.router: app.include_router(router.router, prefix=f"/{router.name}", tags=[router.name]) + else: + print(f"Router {router.name} is not loaded") + continue if router.reset: resets.append(router.reset) if router.startup: diff --git a/frontend/bun.lock b/frontend/bun.lock index 01d62fd..526f399 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -141,17 +141,17 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], - "@mantine/core": ["@mantine/core@7.16.2", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.16.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-6dwFz+8HrOqFan7GezgpoWyZSCxedh10S8iILGVsc3GXiD4gzo+3VZndZKccktkYZ3GVC9E3cCS3SxbiyKSAVw=="], + "@mantine/core": ["@mantine/core@7.16.3", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-cxhIpfd2i0Zmk9TKdejYAoIvWouMGhzK3OOX+VRViZ5HEjnTQCGl2h3db56ThqB6NfVPCno6BPbt5lwekTtmuQ=="], - "@mantine/form": ["@mantine/form@7.16.2", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-JZkLbZ7xWAZndPrxObkf10gjHj57x8yvI/vobjDhfWN3zFPTSWmSSF6yBE1FpITseOs3oR03hlkqG6EclK6g+g=="], + "@mantine/form": ["@mantine/form@7.16.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-GqomUG2Ri5adxYsTU1S5IhKRPcqTG5JkPvMERns8PQAcUz/lvzsnk3wY1v4K5CEbCAdpimle4bSsZTM9g697vg=="], - "@mantine/hooks": ["@mantine/hooks@7.16.2", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-ZFHQhDi9T+r6VR5NEeE47gigPPIAHVIKDOCWsCsbCqHc3yz5l8kiO2RdfUmsTKV2KD/AiXnAw4b6pjQEP58GOg=="], + "@mantine/hooks": ["@mantine/hooks@7.16.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-B94FBWk5Sc81tAjV+B3dGh/gKzfqzpzVC/KHyBRWOOyJRqeeRbI/FAaJo4zwppyQo1POSl5ArdyjtDRrRIj2SQ=="], - "@mantine/modals": ["@mantine/modals@7.16.2", "", { "peerDependencies": { "@mantine/core": "7.16.2", "@mantine/hooks": "7.16.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-REwAV53Fcz021EE3zLyYdkdFlfG+b24y279Y+eA1jCCH9VMLivXL+gacrox4BcpzREsic9nGVInSNv3VJwPlAQ=="], + "@mantine/modals": ["@mantine/modals@7.16.3", "", { "peerDependencies": { "@mantine/core": "7.16.3", "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-BJuDzRugK6xLbuFTTo8NLJumVvVmSYsNVcEtmlXOWTE3NkDGktBXGKo8V1B0XfJ9/d/rZw7HCE0p4i76MtA+bQ=="], - "@mantine/notifications": ["@mantine/notifications@7.16.2", "", { "dependencies": { "@mantine/store": "7.16.2", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "7.16.2", "@mantine/hooks": "7.16.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-U342XWiiRI1NvOlLsI6PH/pSNe0rxNClJ2w5orvjOMXvaAfDe52mhnzRmtzRxYENp06++3b/G7MjPH+466rF9Q=="], + "@mantine/notifications": ["@mantine/notifications@7.16.3", "", { "dependencies": { "@mantine/store": "7.16.3", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "7.16.3", "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-wtEME9kSYfXWYmAmQUZ8c+rwNmhdWRBaW1mlPdQsPkzMqkv4q6yy0IpgwcnuHStSG9EHaQBXazmVxMZJdEAWBQ=="], - "@mantine/store": ["@mantine/store@7.16.2", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-9dEGLosrYSePlAwhfx3CxTLcWu2M98TtuYnelAiHEdNEkyafirvZxNt4paMoFXLKR1XPm5wdjDK7bdTaE0t7Og=="], + "@mantine/store": ["@mantine/store@7.16.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-6M2M5+0BrRtnVv+PUmr04tY1RjPqyapaHplo90uK1NMhP/1EIqrwTL9KoEtCNCJ5pog1AQtu0bj0QPbqUvxwLg=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], @@ -205,7 +205,7 @@ "@types/jest": ["@types/jest@27.5.2", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA=="], - "@types/node": ["@types/node@20.17.16", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw=="], + "@types/node": ["@types/node@20.17.17", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg=="], "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], diff --git a/frontend/package.json b/frontend/package.json index 8a0cba7..071420d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,14 +5,14 @@ "private": true, "dependencies": { "@hello-pangea/dnd": "^16.6.0", - "@mantine/core": "^7.16.2", - "@mantine/form": "^7.16.2", - "@mantine/hooks": "^7.16.2", - "@mantine/modals": "^7.16.2", - "@mantine/notifications": "^7.16.2", + "@mantine/core": "^7.16.3", + "@mantine/form": "^7.16.3", + "@mantine/hooks": "^7.16.3", + "@mantine/modals": "^7.16.3", + "@mantine/notifications": "^7.16.3", "@tanstack/react-query": "^4.36.1", "@types/jest": "^27.5.2", - "@types/node": "^20.17.16", + "@types/node": "^20.17.17", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "buffer": "^6.0.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fcfd39b..7fc33a5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,7 +14,7 @@ import { Firewall } from './pages/Firewall'; import { useQueryClient } from '@tanstack/react-query'; -const socket = IS_DEV?io("ws://"+DEV_IP_BACKEND, {transports: ["websocket", "polling"], path:"/sock" }):io({transports: ["websocket", "polling"], path:"/sock"}); +const socket = IS_DEV?io("ws://"+DEV_IP_BACKEND, {transports: ["websocket"], path:"/sock/socket.io" }):io({transports: ["websocket"], path:"/sock/socket.io"}); function App() { diff --git a/frontend/src/components/Firewall/utils.ts b/frontend/src/components/Firewall/utils.ts index fa2419b..c051df8 100644 --- a/frontend/src/components/Firewall/utils.ts +++ b/frontend/src/components/Firewall/utils.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query" import { ServerResponse } from "../../js/models" -import { getapi, postapi } from "../../js/utils" +import { getapi, postapi, putapi } from "../../js/utils" export enum Protocol { TCP = "tcp", @@ -79,15 +79,15 @@ export const firewall = { return await getapi("firewall/settings") as FirewallSettings; }, setsettings: async(data:FirewallSettings) => { - return await postapi("firewall/settings/set", data) as ServerResponse; + return await putapi("firewall/settings", data) as ServerResponse; }, enable: async() => { - return await getapi("firewall/enable") as ServerResponse; + return await postapi("firewall/enable") as ServerResponse; }, disable: async() => { - return await getapi("firewall/disable") as ServerResponse; + return await postapi("firewall/disable") as ServerResponse; }, ruleset: async (data:RuleAddForm) => { - return await postapi("firewall/rules/set", data) as ServerResponseListed; + return await postapi("firewall/rules", data) as ServerResponseListed; } } \ No newline at end of file diff --git a/frontend/src/components/NFRegex/AddEditService.tsx b/frontend/src/components/NFRegex/AddEditService.tsx new file mode 100644 index 0000000..1a696f5 --- /dev/null +++ b/frontend/src/components/NFRegex/AddEditService.tsx @@ -0,0 +1,139 @@ +import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useEffect, useState } from 'react'; +import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; +import { ImCross } from "react-icons/im" +import { nfregex, Service } from './utils'; +import PortAndInterface from '../PortAndInterface'; +import { IoMdInformationCircleOutline } from "react-icons/io"; +import { ServiceAddForm as ServiceAddFormOriginal } from './utils'; + +type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean} + +function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) { + + const initialValues = { + name: "", + port:edit?.port??8080, + ip_int:edit?.ip_int??"", + proto:edit?.proto??"tcp", + fail_open: edit?.fail_open??false, + autostart: true + } + + const form = useForm({ + initialValues: initialValues, + validate:{ + name: (value) => edit? null : value !== "" ? null : "Service name is required", + port: (value) => (value>0 && value<65536) ? null : "Invalid port", + proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", + ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", + } + }) + + useEffect(() => { + if (opened){ + form.setInitialValues(initialValues) + form.reset() + } + }, [opened]) + + const close = () =>{ + onClose() + form.reset() + setError(null) + } + + const [submitLoading, setSubmitLoading] = useState(false) + const [error, setError] = useState(null) + + const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ + setSubmitLoading(true) + if (edit){ + nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => { + if (!res){ + setSubmitLoading(false) + close(); + okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`) + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + }else{ + nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => { + if (res.status === "ok" && res.service_id){ + setSubmitLoading(false) + close(); + if (autostart) nfregex.servicestart(res.service_id) + okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) + }else{ + setSubmitLoading(false) + setError("Invalid request! [ "+res.status+" ]") + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + } + } + + + return +
+ {!edit?:null} + + + + + + + {!edit?:null} + + + Enable fail-open nfqueue + + + Firegex use internally nfqueue to handle packets
enabling this option will allow packets to pass through the firewall
in case the filtering is too slow or too many traffic is coming
+ }> + +
+
} + {...form.getInputProps('fail_open', { type: 'checkbox' })} + /> +
+ + + + + + + + + {error?<> + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} + + +
+ +} + +export default AddEditService; diff --git a/frontend/src/components/NFRegex/AddNewService.tsx b/frontend/src/components/NFRegex/AddNewService.tsx deleted file mode 100644 index f0dbc17..0000000 --- a/frontend/src/components/NFRegex/AddNewService.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useState } from 'react'; -import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; -import { ImCross } from "react-icons/im" -import { nfregex } from './utils'; -import PortAndInterface from '../PortAndInterface'; - -type ServiceAddForm = { - name:string, - port:number, - proto:string, - ip_int:string, - autostart: boolean, -} - -function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { - - const form = useForm({ - initialValues: { - name:"", - port:8080, - ip_int:"", - proto:"tcp", - autostart: true - }, - validate:{ - name: (value) => value !== "" ? null : "Service name is required", - port: (value) => (value>0 && value<65536) ? null : "Invalid port", - proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", - ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", - } - }) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = ({ name, port, autostart, proto, ip_int }:ServiceAddForm) =>{ - setSubmitLoading(true) - nfregex.servicesadd({name, port, proto, ip_int }).then( res => { - if (res.status === "ok" && res.service_id){ - setSubmitLoading(false) - close(); - if (autostart) nfregex.servicestart(res.service_id) - okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) - }else{ - setSubmitLoading(false) - setError("Invalid request! [ "+res.status+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - } - - - return -
- - - - - - - - - - - - - - - - - - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} - - -
- -} - -export default AddNewService; diff --git a/frontend/src/components/NFRegex/ServiceRow/RenameForm.tsx b/frontend/src/components/NFRegex/ServiceRow/RenameForm.tsx index 412be2c..a643bec 100644 --- a/frontend/src/components/NFRegex/ServiceRow/RenameForm.tsx +++ b/frontend/src/components/NFRegex/ServiceRow/RenameForm.tsx @@ -49,16 +49,16 @@ function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>v placeholder="Awesome Service Name!" {...form.getInputProps('name')} /> - + - - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} diff --git a/frontend/src/components/NFRegex/ServiceRow/index.tsx b/frontend/src/components/NFRegex/ServiceRow/index.tsx index 0ec16a2..53a5c1b 100644 --- a/frontend/src/components/NFRegex/ServiceRow/index.tsx +++ b/frontend/src/components/NFRegex/ServiceRow/index.tsx @@ -2,7 +2,7 @@ import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } fr import { useState } from 'react'; import { FaPlay, FaStop } from 'react-icons/fa'; import { nfregex, Service, serviceQueryKey } from '../utils'; -import { MdOutlineArrowForwardIos } from "react-icons/md" +import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md" import YesNoModal from '../../YesNoModal'; import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils'; import { BsTrashFill } from 'react-icons/bs'; @@ -10,8 +10,12 @@ import { BiRename } from 'react-icons/bi' import RenameForm from './RenameForm'; import { MenuDropDownWithButton } from '../../MainLayout'; import { useQueryClient } from '@tanstack/react-query'; +import { FaFilter } from "react-icons/fa"; +import { VscRegex } from "react-icons/vsc"; +import { IoSettingsSharp } from 'react-icons/io5'; +import AddEditService from '../AddEditService'; -function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { +export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { let status_color = "gray"; switch(service.status){ @@ -24,6 +28,7 @@ function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) const [tooltipStopOpened, setTooltipStopOpened] = useState(false); const [deleteModal, setDeleteModal] = useState(false) const [renameModal, setRenameModal] = useState(false) + const [editModal, setEditModal] = useState(false) const isMedium = isMediumScreen() const stopService = async () => { @@ -72,44 +77,43 @@ function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) return <> - - - - - + <Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}> + <Box> + <Box className="center-flex" style={{ justifyContent: "flex-start" }}> + <MdDoubleArrow size={30} style={{color: "white"}}/> + <Title className="firegex__nfregex__name" ml="xs"> {service.name} - - Status: {service.status} - - :{service.port} - - - {isMedium?null:} - + + {service.status} + + :{service.port} + + + {isMedium?null:} + - - - - - + - Connections Blocked: {service.n_packets} - - Regex: {service.n_regex} - {service.ip_int} on {service.proto} + + + {service.n_packets} + + {service.n_regex} + - {isMedium?:} + {isMedium?:} - Rename service + Edit service + } onClick={()=>setEditModal(true)}>Service Settings } onClick={()=>setRenameModal(true)}>Change service name Danger zone } onClick={()=>setDeleteModal(true)}>Delete Service - + void }) {isMedium?:} - {onClick? + {onClick? :null} - {isMedium?:null} - - - + + void }) opened={renameModal} service={service} /> + setEditModal(false)} + edit={service} + /> } - -export default ServiceRow; diff --git a/frontend/src/components/NFRegex/utils.ts b/frontend/src/components/NFRegex/utils.ts index cdf35f8..faa67c4 100644 --- a/frontend/src/components/NFRegex/utils.ts +++ b/frontend/src/components/NFRegex/utils.ts @@ -1,5 +1,5 @@ import { RegexFilter, ServerResponse } from "../../js/models" -import { getapi, postapi } from "../../js/utils" +import { deleteapi, getapi, postapi, putapi } from "../../js/utils" import { RegexAddForm } from "../../js/models" import { useQuery, useQueryClient } from "@tanstack/react-query" @@ -12,6 +12,7 @@ export type Service = { ip_int: string, n_packets:number, n_regex:number, + fail_open:boolean, } export type ServiceAddForm = { @@ -19,6 +20,14 @@ export type ServiceAddForm = { port:number, proto:string, ip_int:string, + fail_open: boolean, +} + +export type ServiceSettings = { + port?:number, + proto?:string, + ip_int?:string, + fail_open?: boolean, } export type ServiceAddResponse = { @@ -40,44 +49,48 @@ export const nfregex = { return await getapi("nfregex/services") as Service[]; }, serviceinfo: async (service_id:string) => { - return await getapi(`nfregex/service/${service_id}`) as Service; + return await getapi(`nfregex/services/${service_id}`) as Service; }, regexdelete: async (regex_id:number) => { - const { status } = await getapi(`nfregex/regex/${regex_id}/delete`) as ServerResponse; + const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse; return status === "ok"?undefined:status }, regexenable: async (regex_id:number) => { - const { status } = await getapi(`nfregex/regex/${regex_id}/enable`) as ServerResponse; + const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse; return status === "ok"?undefined:status }, regexdisable: async (regex_id:number) => { - const { status } = await getapi(`nfregex/regex/${regex_id}/disable`) as ServerResponse; + const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse; return status === "ok"?undefined:status }, servicestart: async (service_id:string) => { - const { status } = await getapi(`nfregex/service/${service_id}/start`) as ServerResponse; + const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse; return status === "ok"?undefined:status }, servicerename: async (service_id:string, name: string) => { - const { status } = await postapi(`nfregex/service/${service_id}/rename`,{ name }) as ServerResponse; + const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse; return status === "ok"?undefined:status }, servicestop: async (service_id:string) => { - const { status } = await getapi(`nfregex/service/${service_id}/stop`) as ServerResponse; + const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse; return status === "ok"?undefined:status }, servicesadd: async (data:ServiceAddForm) => { - return await postapi("nfregex/services/add",data) as ServiceAddResponse; + return await postapi("nfregex/services",data) as ServiceAddResponse; }, servicedelete: async (service_id:string) => { - const { status } = await getapi(`nfregex/service/${service_id}/delete`) as ServerResponse; + const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse; return status === "ok"?undefined:status }, regexesadd: async (data:RegexAddForm) => { - const { status } = await postapi("nfregex/regexes/add",data) as ServerResponse; + const { status } = await postapi("nfregex/regexes",data) as ServerResponse; return status === "ok"?undefined:status }, serviceregexes: async (service_id:string) => { - return await getapi(`nfregex/service/${service_id}/regexes`) as RegexFilter[]; - } + return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[]; + }, + settings: async (service_id:string, data:ServiceSettings) => { + const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse; + return status === "ok"?undefined:status + }, } \ No newline at end of file diff --git a/frontend/src/components/PortHijack/AddNewService.tsx b/frontend/src/components/PortHijack/AddNewService.tsx index b685672..44622be 100644 --- a/frontend/src/components/PortHijack/AddNewService.tsx +++ b/frontend/src/components/PortHijack/AddNewService.tsx @@ -94,16 +94,16 @@ function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) /> - + - - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} diff --git a/frontend/src/components/PortHijack/ServiceRow/ChangeDestination.tsx b/frontend/src/components/PortHijack/ServiceRow/ChangeDestination.tsx index e4f2bef..8d135e9 100644 --- a/frontend/src/components/PortHijack/ServiceRow/ChangeDestination.tsx +++ b/frontend/src/components/PortHijack/ServiceRow/ChangeDestination.tsx @@ -53,15 +53,16 @@ function ChangeDestination({ opened, onClose, service }:{ opened:boolean, onClos
- + - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} diff --git a/frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx b/frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx index 84f9fba..4d75c42 100644 --- a/frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx +++ b/frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx @@ -49,16 +49,16 @@ function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>v placeholder="Awesome Service Name!" {...form.getInputProps('name')} /> - + - - {error?<> - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - :null} + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} diff --git a/frontend/src/components/PortHijack/ServiceRow/index.tsx b/frontend/src/components/PortHijack/ServiceRow/index.tsx index 9428170..2f4a136 100644 --- a/frontend/src/components/PortHijack/ServiceRow/index.tsx +++ b/frontend/src/components/PortHijack/ServiceRow/index.tsx @@ -8,11 +8,11 @@ import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs'; import { BiRename } from 'react-icons/bi' import RenameForm from './RenameForm'; import ChangeDestination from './ChangeDestination'; -import PortInput from '../../PortInput'; import { useForm } from '@mantine/form'; import { MenuDropDownWithButton } from '../../MainLayout'; +import { MdDoubleArrow } from "react-icons/md"; -function ServiceRow({ service }:{ service:Service }) { +export default function ServiceRow({ service }:{ service:Service }) { let status_color = service.active ? "teal": "red" @@ -29,24 +29,6 @@ function ServiceRow({ service }:{ service:Service }) { validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" } }) - const onChangeProxyPort = ({proxy_port}:{proxy_port:number}) => { - if (proxy_port === service.proxy_port) return - if (proxy_port > 0 && proxy_port < 65536 && proxy_port !== service.public_port){ - porthijack.changedestination(service.service_id, service.ip_dst, proxy_port).then( res => { - if (res.status === "ok"){ - okNotify(`Service ${service.name} destination port has changed in ${ proxy_port }`, `Successfully changed destination port`) - }else{ - errorNotify(`Error while changing the destination port of ${service.name}`,`Error: ${res.status}`) - } - }).catch( err => { - errorNotify("Request for changing port failed!",`Error: [ ${err} ]`) - }) - }else{ - form.setFieldValue("proxy_port", service.proxy_port) - errorNotify(`Error while changing the destination port of ${service.name}`,`Insert a valid port number`) - } - } - const stopService = async () => { setButtonLoading(true) @@ -90,54 +72,36 @@ function ServiceRow({ service }:{ service:Service }) { return <> - - - - + <Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}> + <Box> + <Box className="center-flex" style={{ justifyContent: "flex-start" }}> + <MdDoubleArrow size={30} style={{color: "white"}}/> + <Title className="firegex__nfregex__name" ml="xs"> {service.name} - - Status: {service.active?"ENABLED":"DISABLED"} - - {service.proto} - - - {isMedium?null:} - + + {service.active?"ENABLED":"DISABLED"} + + {service.proto} + + + {isMedium?null:} + - - - - - - + - - FROM {service.ip_src} : {service.public_port} + + FROM {service.ip_src} :{service.public_port} - + - TO {service.ip_dst} : -
portInputRef.current?.blur())}> - {onChangeProxyPort({proxy_port:parseInt(e.target.value)})}} - ref={portInputRef} - {...form.getInputProps("proxy_port")} - /> - + TO {service.ip_dst} :{service.proxy_port}
- {isMedium?:} + {isMedium?:} Rename service @@ -166,14 +130,10 @@ function ServiceRow({ service }:{ service:Service }) { - - {isMedium?:null} - -
- + + - } - -export default ServiceRow; diff --git a/frontend/src/components/PortHijack/utils.ts b/frontend/src/components/PortHijack/utils.ts index 80875cc..2c00417 100644 --- a/frontend/src/components/PortHijack/utils.ts +++ b/frontend/src/components/PortHijack/utils.ts @@ -1,5 +1,5 @@ import { ServerResponse } from "../../js/models" -import { getapi, postapi } from "../../js/utils" +import { deleteapi, getapi, postapi, putapi } from "../../js/utils" import { useQuery } from "@tanstack/react-query" export type GeneralStats = { @@ -37,28 +37,28 @@ export const porthijack = { return await getapi("porthijack/services") as Service[]; }, serviceinfo: async (service_id:string) => { - return await getapi(`porthijack/service/${service_id}`) as Service; + return await getapi(`porthijack/services/${service_id}`) as Service; }, servicestart: async (service_id:string) => { - const { status } = await getapi(`porthijack/service/${service_id}/start`) as ServerResponse; + const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse; return status === "ok"?undefined:status }, servicerename: async (service_id:string, name: string) => { - const { status } = await postapi(`porthijack/service/${service_id}/rename`,{ name }) as ServerResponse; + const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse; return status === "ok"?undefined:status }, servicestop: async (service_id:string) => { - const { status } = await getapi(`porthijack/service/${service_id}/stop`) as ServerResponse; + const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse; return status === "ok"?undefined:status }, servicesadd: async (data:ServiceAddForm) => { - return await postapi("porthijack/services/add",data) as ServiceAddResponse; + return await postapi("porthijack/services",data) as ServiceAddResponse; }, servicedelete: async (service_id:string) => { - const { status } = await getapi(`porthijack/service/${service_id}/delete`) as ServerResponse; + const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse; return status === "ok"?undefined:status }, changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => { - return await postapi(`porthijack/service/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse; + return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse; } } \ No newline at end of file diff --git a/frontend/src/components/RegexView/index.tsx b/frontend/src/components/RegexView/index.tsx index 20b9d18..e5b0821 100644 --- a/frontend/src/components/RegexView/index.tsx +++ b/frontend/src/components/RegexView/index.tsx @@ -1,18 +1,19 @@ -import { Grid, Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; +import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; import { useState } from 'react'; import { RegexFilter } from '../../js/models'; -import { b64decode, errorNotify, getapiobject, okNotify } from '../../js/utils'; +import { b64decode, errorNotify, getapiobject, isMediumScreen, okNotify } from '../../js/utils'; import { BsTrashFill } from "react-icons/bs" import YesNoModal from '../YesNoModal'; import { FaPause, FaPlay } from 'react-icons/fa'; import { useClipboard } from '@mantine/hooks'; - +import { FaFilter } from "react-icons/fa"; +import { VscRegex } from "react-icons/vsc"; function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { const mode_string = regexInfo.mode === "C"? "C -> S": regexInfo.mode === "S"? "S -> C": - regexInfo.mode === "B"? "S <-> C": "🤔" + regexInfo.mode === "B"? "C <-> S": "🤔" let regex_expr = b64decode(regexInfo.regex); @@ -20,6 +21,7 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { const [deleteTooltipOpened, setDeleteTooltipOpened] = useState(false); const [statusTooltipOpened, setStatusTooltipOpened] = useState(false); const clipboard = useClipboard({ timeout: 500 }); + const isMedium = isMediumScreen(); const deleteRegex = () => { getapiobject().regexdelete(regexInfo.id).then(res => { @@ -42,57 +44,39 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { } return - - - Regex: - - + + { clipboard.copy(regex_expr) okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`) }}>{regex_expr} - - setStatusTooltipOpened(false)} onBlur={() => setStatusTooltipOpened(false)} - onMouseEnter={() => setStatusTooltipOpened(true)} onMouseLeave={() => setStatusTooltipOpened(false)} + onFocus={() => setStatusTooltipOpened(false)} onBlur={() => setStatusTooltipOpened(false)} + onMouseEnter={() => setStatusTooltipOpened(true)} onMouseLeave={() => setStatusTooltipOpened(false)} >{regexInfo.active?:} setDeleteModal(true)} size="xl" radius="md" variant="filled" - onFocus={() => setDeleteTooltipOpened(false)} onBlur={() => setDeleteTooltipOpened(false)} - onMouseEnter={() => setDeleteTooltipOpened(true)} onMouseLeave={() => setDeleteTooltipOpened(false)} + onFocus={() => setDeleteTooltipOpened(false)} onBlur={() => setDeleteTooltipOpened(false)} + onMouseEnter={() => setDeleteTooltipOpened(true)} onMouseLeave={() => setDeleteTooltipOpened(false)} > - - - - - - - Service: {regexInfo.service_id} - - {regexInfo.active?"ACTIVE":"DISABLED"} - - ID: {regexInfo.id} - - - - - - Case: {regexInfo.is_case_sensitive?"SENSIIVE":"INSENSITIVE"} - - Packets filtered: {regexInfo.n_packets} - - Mode: {mode_string} - - - + + + {regexInfo.n_packets} + + {regexInfo.active?"ACTIVE":"DISABLED"} + + {regexInfo.is_case_sensitive?"Strict":"Loose"} + + {mode_string} + + {description} - +