Merge pull request #13 from Pwnzer0tt1/dev-nfproxy

Releasing changes done for nfproxy influncing also nfregex and fixes -> 2.5.0 release
This commit is contained in:
Domingo Dirutigliano
2025-02-18 17:39:59 +01:00
committed by GitHub
72 changed files with 2818 additions and 1192 deletions

View File

@@ -19,7 +19,7 @@ Dockerfile
/frontend/build/ /frontend/build/
/frontend/build/** /frontend/build/**
/frontend/node_modules/ /frontend/node_modules/
/backend/modules/cppqueue /backend/modules/cppregex
/backend/modules/proxy /backend/modules/proxy
docker-compose.yml docker-compose.yml

View File

@@ -20,12 +20,6 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 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 - name: Set up QEMU
uses: docker/setup-qemu-action@master uses: docker/setup-qemu-action@master
with: with:
@@ -41,13 +35,20 @@ jobs:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 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 - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
@@ -59,5 +60,3 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

47
.github/workflows/pypi-publish.yml vendored Normal file
View File

@@ -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 }}

19
.gitignore vendored
View File

@@ -11,20 +11,25 @@
# testing # testing
/frontend/coverage /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/
/backend/db/** /backend/db/**
/frontend/build/ /frontend/build/
/frontend/build/** /frontend/build/**
/frontend/dist/ /frontend/dist/
/frontend/dist/** /frontend/dist/**
/backend/modules/cppqueue /backend/modules/cppregex
/backend/binsrc/cppqueue /backend/binsrc/cppregex
/backend/modules/cpproxy
/backend/binsrc/cpproxy
/backend/modules/proxy /backend/modules/proxy
docker-compose.yml /docker-compose.yml
firegex-compose.yml /firegex-compose.yml
firegex-compose-tmp-file.yml /firegex-compose-tmp-file.yml
firegex.py /firegex.py
/tests/benchmark.csv /tests/benchmark.csv
# misc # misc
**/.DS_Store **/.DS_Store

View File

@@ -14,11 +14,10 @@ RUN bun run build
#Building main conteiner #Building main conteiner
FROM --platform=$TARGETARCH debian:trixie-slim AS base FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest
RUN apt-get update -qq && apt-get upgrade -qq && \ RUN dnf -y update && dnf install -y python3.13-devel python3-pip @development-tools gcc-c++ \
apt-get install -qq python3-pip build-essential \ libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils nftables \
libnetfilter-queue-dev libnfnetlink-dev libmnl-dev libcap2-bin\ vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel
nftables libvectorscan-dev libtins-dev python3-nftables
RUN mkdir -p /execute/modules RUN mkdir -p /execute/modules
WORKDIR /execute 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 RUN pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt --no-warn-script-location
COPY ./backend/binsrc /execute/binsrc 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 ./backend/ /execute/
COPY --from=frontend /app/dist/ ./frontend/ COPY --from=frontend /app/dist/ ./frontend/

View File

@@ -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. 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 # Credits
- Copyright (c) 2022 Pwnzer0tt1 - Copyright (c) 2022-2025 Pwnzer0tt1
## Star History ## Star History

View File

@@ -8,13 +8,13 @@ from fastapi import FastAPI, HTTPException, Depends, APIRouter
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt from jose import jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from fastapi_socketio import SocketManager
from utils.sqlite import SQLite from utils.sqlite import SQLite
from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager
from utils.loader import frontend_deploy, load_routers from utils.loader import frontend_deploy, load_routers
from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import socketio
# DB init # DB init
db = SQLite('db/firegex.db') db = SQLite('db/firegex.db')
@@ -42,7 +42,6 @@ app = FastAPI(
title="Firegex API", title="Firegex API",
version=API_VERSION, version=API_VERSION,
) )
utils.socketio = SocketManager(app, "/sock", socketio_path="")
if DEBUG: if DEBUG:
app.add_middleware( 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 APP_STATUS(): return "init" if db.get("password") is None else "run"
def JWT_SECRET(): return db.get("secret") def JWT_SECRET(): return db.get("secret")
@@ -190,10 +198,11 @@ if __name__ == '__main__':
os.chdir(os.path.dirname(os.path.realpath(__file__))) os.chdir(os.path.dirname(os.path.realpath(__file__)))
uvicorn.run( uvicorn.run(
"app:app", "app:app",
host=None, #"::" if DEBUG else None, host="::" if DEBUG else None,
port=FIREGEX_PORT, port=FIREGEX_PORT,
reload=False,#DEBUG, reload=DEBUG,
access_log=True, access_log=True,
workers=1, # Multiple workers will cause a crash due to the creation workers=1, # Firewall module can't be replicated in multiple workers
# of multiple processes with separated memory # Later the firewall module will be moved to a separate process
# The webserver will communicate using redis (redis is also needed for websockets)
) )

View File

@@ -1,527 +1,101 @@
#include <linux/netfilter/nfnetlink_queue.h> #include <vector>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <linux/netfilter/nfnetlink_conntrack.h>
#include <tins/tins.h>
#include <tins/tcp_ip/stream_follower.h>
#include <tins/tcp_ip/stream_identifier.h>
#include <libmnl/libmnl.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/types.h>
#include <stdexcept>
#include <thread> #include <thread>
#include <hs.h> #include <type_traits>
#include <iostream> #include "../utils.cpp"
#include "nfqueue.cpp"
using Tins::TCPIP::Stream; #ifndef NETFILTER_CLASS_CPP
using Tins::TCPIP::StreamFollower; #define NETFILTER_CLASS_CPP
using namespace std;
#ifndef NETFILTER_CLASSES_HPP namespace Firegex {
#define NETFILTER_CLASSES_HPP namespace NfQueue {
typedef Tins::TCPIP::StreamIdentifier stream_id;
typedef map<stream_id, hs_stream_t*> matching_map;
/* Considering to use unorder_map using this hash of stream_id template <typename Derived>
class ThreadNfQueue {
public:
ThreadNfQueue() = default;
virtual ~ThreadNfQueue() = default;
namespace std { std::thread thr;
template<> BlockingQueue<PktRequest<Derived>*> queue;
struct hash<stream_id> {
size_t operator()(const stream_id& sid) const
{
return std::hash<std::uint32_t>()(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);
}
};
}
*/ virtual void before_loop() {}
virtual void handle_next_packet(PktRequest<Derived>* pkt){}
void loop() {
static_cast<Derived*>(this)->before_loop();
PktRequest<Derived>* pkt;
for(;;) {
queue.take(pkt);
static_cast<Derived*>(this)->handle_next_packet(pkt);
delete pkt;
}
}
#ifdef DEBUG void run_thread_loop() {
ostream& operator<<(ostream& os, const Tins::TCPIP::StreamIdentifier::address_type &sid){ thr = std::thread([this]() { this->loop(); });
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;
}; };
struct stream_ctx { template <typename Worker, typename = is_base_of<ThreadNfQueue<Worker>, Worker>>
matching_map in_hs_streams; void __real_handler(PktRequest<std::vector<Worker>>* pkt) {
matching_map out_hs_streams; const size_t idx = hash_stream_id(pkt->sid) % pkt->ctx->size();
hs_scratch_t* in_scratch = nullptr;
hs_scratch_t* out_scratch = nullptr;
u_int16_t latest_config_ver = 0;
StreamFollower follower;
mnl_socket* nl;
tcp_stream_tmp tcp_match_util;
void clean_scratches(){ auto* converted_pkt = reinterpret_cast<PktRequest<Worker>*>(pkt);
if (out_scratch != nullptr){ converted_pkt->ctx = &((*pkt->ctx)[idx]);
hs_free_scratch(out_scratch);
out_scratch = nullptr; converted_pkt->ctx->queue.put(converted_pkt);
} }
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 <NetFilterQueueCallback callback_func>
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);
}
void run(){ template <typename Worker, typename = is_base_of<ThreadNfQueue<Worker>, Worker>>
/* class MultiThreadQueue {
* ENOBUFS is signalled to userspace when packets were lost static_assert(std::is_base_of_v<ThreadNfQueue<Worker>, Worker>,
* on kernel side. In most cases, userspace isn't interested "Worker must inherit from ThreadNfQueue<Worker>");
* 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));
for (;;) { private:
ret = recv_packet(); std::vector<Worker> workers;
if (ret == -1) { NfQueue<std::vector<Worker>, __real_handler<Worker>> * nfq;
throw runtime_error( "mnl_socket_recvfrom" ); uint16_t queue_num_;
}
ret = mnl_cb_run(buf, ret, 0, portid, queue_cb, &sctx);
if (ret < 0){
throw runtime_error( "mnl_cb_run" );
}
}
}
~NetfilterQueue() { public:
#ifdef DEBUG const size_t n_threads;
cerr << "[DEBUG] [NetfilterQueue.~NetfilterQueue] Destructor called" << endl; static constexpr int QUEUE_BASE_NUM = 1000;
#endif
send_config_cmd(NFQNL_CFG_CMD_UNBIND);
_clear();
}
private:
ssize_t send_config_cmd(nfqnl_msg_config_cmds cmd){ explicit MultiThreadQueue(size_t n_threads)
struct nlmsghdr *nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); : n_threads(n_threads), workers(n_threads)
nfq_nlmsg_cfg_put_cmd(nlh, AF_INET, cmd); {
return mnl_socket_sendto(sctx.nl, nlh, nlh->nlmsg_len); 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<std::vector<Worker>, __real_handler<Worker>>(qnum);
queue_num_ = qnum;
break;
}
catch(const std::invalid_argument&) {
if(qnum == std::numeric_limits<uint16_t>::max())
throw std::runtime_error("No available queue numbers");
}
}
}
ssize_t recv_packet(){ ~MultiThreadQueue() {
return mnl_socket_recvfrom(sctx.nl, buf, BUF_SIZE); delete nfq;
} }
void _clear(){ void start() {
if (buf != nullptr) { for(auto& worker : workers) {
free(buf); worker.run_thread_loop();
buf = nullptr; }
for (;;){
nfq->handle_next_packet(&workers);
} }
mnl_socket_close(sctx.nl); }
sctx.nl = nullptr;
sctx.clean();
}
template<typename T>
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<Tins::TCP>();
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<Tins::UDP>();
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 <NetFilterQueueCallback func> }} // namespace Firegex::NfQueue
class NFQueueSequence{ #endif // NETFILTER_CLASS_CPP
private:
vector<NetfilterQueue<func> *> nfq;
uint16_t _init;
uint16_t _end;
vector<thread> 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<NetfilterQueue<func>*>(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<seq_len;i++){
try{
nfq[i] = new NetfilterQueue<func>(_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<nfq.size();i++){
threads.push_back(thread(&NetfilterQueue<func>::run, nfq[i]));
}
}
void join(){
for (int i=0;i<nfq.size();i++){
threads[i].join();
}
threads.clear();
}
uint16_t init(){
return _init;
}
uint16_t end(){
return _end;
}
~NFQueueSequence(){
for (int i=0;i<nfq.size();i++){
delete nfq[i];
}
}
};
#endif // NETFILTER_CLASSES_HPP

View File

@@ -0,0 +1,364 @@
#ifndef NFQUEUE_CLASS_CPP
#define NFQUEUE_CLASS_CPP
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <linux/netfilter/nfnetlink_queue.h>
#include <tins/tcp_ip/stream_identifier.h>
#include <libmnl/libmnl.h>
#include <tins/tins.h>
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<typename T>
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<Tins::TCP>();
if (tcp == nullptr){
udp = ipv6->find_pdu<Tins::UDP>();
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<Tins::TCP>();
if (tcp == nullptr){
udp = ipv4->find_pdu<Tins::UDP>();
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 <typename D, void handle_func(PktRequest<D>*)>
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<D>(
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<const uint32_t*>(sid.min_address.data());
const uint32_t* max_addr = reinterpret_cast<const uint32_t*>(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<uint32_t>(sid.min_address_port) << 16) | sid.max_address_port;
uint32_t hash = addr_hash ^ ports;
hash *= 0x9e3779b9;
return hash;
}
}}
#endif // NFQUEUE_CLASS_CPP

View File

@@ -0,0 +1,72 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "proxytun/settings.cpp"
#include "proxytun/proxytun.cpp"
#include "classes/netfilter.cpp"
#include <syncstream>
#include <iostream>
#include <stdexcept>
#include <cstdlib>
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<uint8_t> 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<PyProxyQueue> 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();
}

View File

@@ -1,175 +0,0 @@
#include "classes/regex_rules.cpp"
#include "classes/netfilter.cpp"
#include "utils.hpp"
#include <iostream>
using namespace std;
shared_ptr<RegexRules> 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<string> 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<RegexRules> 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<filter_callback> 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();
}

View File

@@ -0,0 +1,78 @@
#include "regex/regex_rules.cpp"
#include "regex/regexfilter.cpp"
#include "classes/netfilter.cpp"
#include <syncstream>
#include <iostream>
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<string> 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<RegexNfQueue> 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();
}

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,18 @@
#ifndef REGEX_FILTER_CPP
#define REGEX_FILTER_CPP
#include <iostream> #include <iostream>
#include <cstring> #include <cstring>
#include <sstream> #include <sstream>
#include "../utils.hpp" #include "../utils.cpp"
#include <vector> #include <vector>
#include <hs.h> #include <hs.h>
#include <memory>
using namespace std; using namespace std;
#ifndef REGEX_FILTER_HPP namespace Firegex {
#define REGEX_FILTER_HPP namespace Regex {
enum FilterDirection{ CTOS, STOC }; enum FilterDirection{ CTOS, STOC };
@@ -170,5 +174,16 @@ class RegexRules{
} }
}; };
#endif // REGEX_FILTER_HPP shared_ptr<RegexRules> 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

View File

@@ -0,0 +1,229 @@
#ifndef REGEX_FILTER_CLASS_CPP
#define REGEX_FILTER_CLASS_CPP
#include <linux/netfilter/nfnetlink_queue.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <linux/netfilter/nfnetlink_conntrack.h>
#include <tins/tins.h>
#include <tins/tcp_ip/stream_follower.h>
#include <tins/tcp_ip/stream_identifier.h>
#include <libmnl/libmnl.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/types.h>
#include <stdexcept>
#include <thread>
#include <hs.h>
#include <syncstream>
#include <functional>
#include <iostream>
#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<RegexNfQueue> {
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<RegexNfQueue>* pkt;
} match_ctx;
bool filter_action(NfQueue::PktRequest<RegexNfQueue>* pkt){
shared_ptr<RegexRules> 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<RegexNfQueue>* pkt) override{
bool empty_payload = pkt->data_size == 0;
if (pkt->tcp){
match_ctx.matching_has_been_called = false;
match_ctx.pkt = pkt;
if (pkt->ipv4){
follower.process_packet(*pkt->ipv4);
}else{
follower.process_packet(*pkt->ipv6);
}
// Do an action only is an ordered packet has been received
if (match_ctx.matching_has_been_called){
//In this 2 cases we have to remove all data about the stream
if (!match_ctx.result || match_ctx.already_closed){
sctx.clean_stream_by_id(pkt->sid);
//If the packet has data, we have to remove it
if (!empty_payload){
Tins::PDU* data_layer = pkt->tcp->release_inner_pdu();
if (data_layer != nullptr){
delete data_layer;
}
}
//For the first matched data or only for data packets, we set FIN bit
//This only for client packets, because this will trigger server to close the connection
//Packets will be filtered anyway also if client don't send packets
if ((!match_ctx.result || !empty_payload) && pkt->is_input){
pkt->tcp->set_flag(Tins::TCP::FIN,1);
pkt->tcp->set_flag(Tins::TCP::ACK,1);
pkt->tcp->set_flag(Tins::TCP::SYN,0);
}
//Send the edited packet to the kernel
return pkt->mangle();
}
}
return pkt->accept();
}else{
if (!pkt->udp){
throw invalid_argument("Only TCP and UDP are supported");
}
if(empty_payload){
return pkt->accept();
}else if (filter_action(pkt)){
return pkt->accept();
}else{
return pkt->drop();
}
}
}
//If the stream has already been matched, drop all data, and try to close the connection
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

View File

@@ -0,0 +1,103 @@
#ifndef STREAM_CTX_CPP
#define STREAM_CTX_CPP
#include <iostream>
#include <hs.h>
#include <tins/tcp_ip/stream_identifier.h>
#include <functional>
#include <map>
#include "regexfilter.cpp"
using namespace std;
namespace Firegex {
namespace Regex {
typedef Tins::TCPIP::StreamIdentifier stream_id;
typedef map<stream_id, hs_stream_t*> 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

97
backend/binsrc/utils.cpp Normal file
View File

@@ -0,0 +1,97 @@
#include <string>
#include <unistd.h>
#include <queue>
#include <condition_variable>
#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<typename T>
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<typename T, int MAX = 1024> //same of kernel nfqueue max
class BlockingQueue
{
private:
std::mutex mut;
std::queue<T> 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<std::mutex> 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<std::mutex> 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

View File

@@ -1,23 +0,0 @@
#include <string>
#include <unistd.h>
#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

View File

@@ -4,5 +4,3 @@ chown nobody -R /execute/
exec capsh --caps="cap_net_admin+eip cap_setpcap,cap_setuid,cap_setgid+ep" \ 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" --keep=1 --user=nobody --addamb=cap_net_admin -- -c "python3 /execute/app.py DOCKER"

View File

@@ -130,6 +130,7 @@ class FirewallManager:
def allow_dhcp(self): def allow_dhcp(self):
return self.db.get("allow_dhcp", "1") == "1" return self.db.get("allow_dhcp", "1") == "1"
@drop_invalid.setter @allow_dhcp.setter
def allow_dhcp_set(self, value): def allow_dhcp(self, value):
self.db.set("allow_dhcp", "1" if value else "0") self.db.set("allow_dhcp", "1" if value else "0")

View File

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}}})

View File

@@ -84,11 +84,15 @@ class FiregexInterceptor:
return self return self
async def _start_binary(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( self.process = await asyncio.create_subprocess_exec(
proxy_binary_path, proxy_binary_path,
stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, 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() line_fut = self.process.stdout.readuntil()
try: try:
@@ -97,9 +101,9 @@ class FiregexInterceptor:
self.process.kill() self.process.kill()
raise Exception("Invalid binary output") raise Exception("Invalid binary output")
line = line_fut.decode() line = line_fut.decode()
if line.startswith("QUEUES "): if line.startswith("QUEUE "):
params = line.split() params = line.split()
return (int(params[1]), int(params[2])) return (int(params[1]), int(params[1]))
else: else:
self.process.kill() self.process.kill()
raise Exception("Invalid binary output") raise Exception("Invalid binary output")

View File

@@ -1,13 +1,14 @@
import base64 import base64
class Service: class Service:
def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, **other): def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, fail_open: bool, **other):
self.id = service_id self.id = service_id
self.status = status self.status = status
self.port = port self.port = port
self.name = name self.name = name
self.proto = proto self.proto = proto
self.ip_int = ip_int self.ip_int = ip_int
self.fail_open = fail_open
@classmethod @classmethod
def from_dict(cls, var: dict): def from_dict(cls, var: dict):

View File

@@ -48,10 +48,12 @@ class FiregexTables(NFTableManager):
def add(self, srv:Service, queue_range): def add(self, srv:Service, queue_range):
for ele in self.get(): for ele in self.get():
if ele.__eq__(srv): return if ele.__eq__(srv):
return
init, end = queue_range init, end = queue_range
if init > end: init, end = end, init if init > end:
init, end = end, init
self.cmd( self.cmd(
{ "insert":{ "rule": { { "insert":{ "rule": {
"family": "inet", "family": "inet",

View File

@@ -4,5 +4,5 @@ uvicorn[standard]
passlib[bcrypt] passlib[bcrypt]
psutil psutil
python-jose[cryptography] python-jose[cryptography]
fastapi-socketio python-socketio
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py #git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py

View File

@@ -24,7 +24,7 @@ db = SQLite('db/firewall-rules.db', {
'action': 'VARCHAR(10) NOT NULL CHECK (action IN ("accept", "drop", "reject"))', 'action': 'VARCHAR(10) NOT NULL CHECK (action IN ("accept", "drop", "reject"))',
}, },
'QUERY':[ '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""" """Get the firewall settings"""
return firewall.settings return firewall.settings
@app.post("/settings/set", response_model=StatusMessageModel) @app.put("/settings", response_model=StatusMessageModel)
async def set_settings(form: FirewallSettings): async def set_settings(form: FirewallSettings):
"""Set the firewall settings""" """Set the firewall settings"""
firewall.settings = form firewall.settings = form
@@ -86,13 +86,13 @@ async def get_rule_list():
"enabled": firewall.enabled "enabled": firewall.enabled
} }
@app.get('/enable', response_model=StatusMessageModel) @app.post('/enable', response_model=StatusMessageModel)
async def enable_firewall(): async def enable_firewall():
"""Request enabling the firewall""" """Request enabling the firewall"""
firewall.enabled = True firewall.enabled = True
return await apply_changes() return await apply_changes()
@app.get('/disable', response_model=StatusMessageModel) @app.post('/disable', response_model=StatusMessageModel)
async def disable_firewall(): async def disable_firewall():
"""Request disabling the firewall""" """Request disabling the firewall"""
firewall.enabled = False firewall.enabled = False
@@ -128,9 +128,9 @@ def parse_and_check_rule(rule:RuleModel):
return rule return rule
@app.post('/rules/set', response_model=StatusMessageModel) @app.post('/rules', response_model=StatusMessageModel)
async def add_new_service(form: RuleFormAdd): 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] rules = [parse_and_check_rule(ele) for ele in form.rules]
try: try:
db.queries(["DELETE FROM rules"]+ db.queries(["DELETE FROM rules"]+

259
backend/routers/nfproxy.py Normal file
View File

@@ -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

View File

@@ -19,10 +19,17 @@ class ServiceModel(BaseModel):
ip_int: str ip_int: str
n_regex: int n_regex: int
n_packets: int n_packets: int
fail_open: bool
class RenameForm(BaseModel): class RenameForm(BaseModel):
name:str 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): class RegexModel(BaseModel):
regex:str regex:str
mode:str mode:str
@@ -44,6 +51,7 @@ class ServiceAddForm(BaseModel):
port: PortType port: PortType
proto: str proto: str
ip_int: str ip_int: str
fail_open: bool = False
class ServiceAddResponse(BaseModel): class ServiceAddResponse(BaseModel):
status:str status:str
@@ -59,6 +67,7 @@ db = SQLite('db/nft-regex.db', {
'name': 'VARCHAR(100) NOT NULL UNIQUE', 'name': 'VARCHAR(100) NOT NULL UNIQUE',
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))', 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))',
'ip_int': 'VARCHAR(100) NOT NULL', 'ip_int': 'VARCHAR(100) NOT NULL',
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1'
}, },
'regexes': { 'regexes': {
'regex': 'TEXT NOT NULL', 'regex': 'TEXT NOT NULL',
@@ -128,13 +137,14 @@ async def get_service_list():
s.name name, s.name name,
s.proto proto, s.proto proto,
s.ip_int ip_int, s.ip_int ip_int,
s.fail_open fail_open,
COUNT(r.regex_id) n_regex, COUNT(r.regex_id) n_regex,
COALESCE(SUM(r.blocked_packets),0) n_packets COALESCE(SUM(r.blocked_packets),0) n_packets
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
GROUP BY s.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): async def get_service_by_id(service_id: str):
"""Get info about a specific service using his id""" """Get info about a specific service using his id"""
res = db.query(""" res = db.query("""
@@ -145,6 +155,7 @@ async def get_service_by_id(service_id: str):
s.name name, s.name name,
s.proto proto, s.proto proto,
s.ip_int ip_int, s.ip_int ip_int,
s.fail_open fail_open,
COUNT(r.regex_id) n_regex, COUNT(r.regex_id) n_regex,
COALESCE(SUM(r.blocked_packets),0) n_packets COALESCE(SUM(r.blocked_packets),0) n_packets
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id 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!") raise HTTPException(status_code=400, detail="This service does not exists!")
return res[0] 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): async def service_stop(service_id: str):
"""Request the stop of a specific service""" """Request the stop of a specific service"""
await firewall.get(service_id).next(STATUS.STOP) await firewall.get(service_id).next(STATUS.STOP)
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} 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): async def service_start(service_id: str):
"""Request the start of a specific service""" """Request the start of a specific service"""
await firewall.get(service_id).next(STATUS.ACTIVE) await firewall.get(service_id).next(STATUS.ACTIVE)
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} 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): async def service_delete(service_id: str):
"""Request the deletion of a specific service""" """Request the deletion of a specific service"""
db.query('DELETE FROM services WHERE service_id = ?;', service_id) db.query('DELETE FROM services WHERE service_id = ?;', service_id)
@@ -177,7 +188,7 @@ async def service_delete(service_id: str):
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} 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): async def service_rename(service_id: str, form: RenameForm):
"""Request to change the name of a specific service""" """Request to change the name of a specific service"""
form.name = refactor_name(form.name) form.name = refactor_name(form.name)
@@ -190,7 +201,46 @@ async def service_rename(service_id: str, form: RenameForm):
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} 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): async def get_service_regexe_list(service_id: str):
"""Get the list of the regexes of a service""" """Get the list of the regexes of a service"""
if not db.query("SELECT 1 FROM services s WHERE s.service_id = ?;", service_id): 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 = ?; FROM regexes WHERE service_id = ?;
""", 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): async def get_regex_by_id(regex_id: int):
"""Get regex info using his id""" """Get regex info using his id"""
res = db.query(""" 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!") raise HTTPException(status_code=400, detail="This regex does not exists!")
return res[0] 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): async def regex_delete(regex_id: int):
"""Delete a regex using his id""" """Delete a regex using his id"""
res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_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'} 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): async def regex_enable(regex_id: int):
"""Request the enabling of a regex""" """Request the enabling of a regex"""
res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) 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() await refresh_frontend()
return {'status': 'ok'} 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): async def regex_disable(regex_id: int):
"""Request the deactivation of a regex""" """Request the deactivation of a regex"""
res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) 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() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@app.post('/regexes/add', response_model=StatusMessageModel) @app.post('/regexes', response_model=StatusMessageModel)
async def add_new_regex(form: RegexAddForm): async def add_new_regex(form: RegexAddForm):
"""Add a new regex""" """Add a new regex"""
try: try:
@@ -263,7 +313,7 @@ async def add_new_regex(form: RegexAddForm):
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@app.post('/services/add', response_model=ServiceAddResponse) @app.post('/services', response_model=ServiceAddResponse)
async def add_new_service(form: ServiceAddForm): async def add_new_service(form: ServiceAddForm):
"""Add a new service""" """Add a new service"""
try: try:
@@ -275,8 +325,8 @@ async def add_new_service(form: ServiceAddForm):
srv_id = None srv_id = None
try: try:
srv_id = gen_service_id() srv_id = gen_service_id()
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?)", db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open) VALUES (?, ?, ?, ?, ?, ?, ?)",
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int) srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open)
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
raise HTTPException(status_code=400, detail="This type of service already exists") raise HTTPException(status_code=400, detail="This type of service already exists")
await firewall.reload() await firewall.reload()
@@ -299,7 +349,8 @@ async def metrics():
FROM regexes r LEFT JOIN services s ON s.service_id = r.service_id; FROM regexes r LEFT JOIN services s ON s.service_id = r.service_id;
""") """)
metrics = [] metrics = []
sanitize = lambda s : s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') def sanitize(s):
return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
for stat in stats: 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"]}"' 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"]}') metrics.append(f'firegex_blocked_packets{{{props}}} {stat["blocked_packets"]}')

View File

@@ -92,7 +92,7 @@ async def get_service_list():
"""Get the list of existent firegex services""" """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;") 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): async def get_service_by_id(service_id: str):
"""Get info about a specific service using his id""" """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) 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!") raise HTTPException(status_code=400, detail="This service does not exists!")
return res[0] 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): async def service_stop(service_id: str):
"""Request the stop of a specific service""" """Request the stop of a specific service"""
await firewall.get(service_id).disable() await firewall.get(service_id).disable()
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} 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): async def service_start(service_id: str):
"""Request the start of a specific service""" """Request the start of a specific service"""
await firewall.get(service_id).enable() await firewall.get(service_id).enable()
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} 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): async def service_delete(service_id: str):
"""Request the deletion of a specific service""" """Request the deletion of a specific service"""
db.query('DELETE FROM services WHERE service_id = ?;', service_id) db.query('DELETE FROM services WHERE service_id = ?;', service_id)
@@ -122,7 +122,7 @@ async def service_delete(service_id: str):
await refresh_frontend() await refresh_frontend()
return {'status': 'ok'} 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): async def service_rename(service_id: str, form: RenameForm):
"""Request to change the name of a specific service""" """Request to change the name of a specific service"""
form.name = refactor_name(form.name) form.name = refactor_name(form.name)
@@ -139,7 +139,7 @@ class ChangeDestination(BaseModel):
ip_dst: str ip_dst: str
proxy_port: PortType 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): async def service_change_destination(service_id: str, form: ChangeDestination):
"""Request to change the proxy destination of the service""" """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() await refresh_frontend()
return {'status': 'ok'} return {'status': 'ok'}
@app.post('/services/add', response_model=ServiceAddResponse) @app.post('/services', response_model=ServiceAddResponse)
async def add_new_service(form: ServiceAddForm): async def add_new_service(form: ServiceAddForm):
"""Add a new service""" """Add a new service"""
try: try:

View File

@@ -5,13 +5,13 @@ import socket
import psutil import psutil
import sys import sys
import nftables import nftables
from fastapi_socketio import SocketManager from socketio import AsyncServer
from fastapi import Path from fastapi import Path
from typing import Annotated from typing import Annotated
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
socketio:SocketManager = None socketio:AsyncServer = None
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers") ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
@@ -19,7 +19,7 @@ ON_DOCKER = "DOCKER" in sys.argv
DEBUG = "DEBUG" in sys.argv DEBUG = "DEBUG" in sys.argv
FIREGEX_PORT = int(os.getenv("PORT","4444")) FIREGEX_PORT = int(os.getenv("PORT","4444"))
JWT_ALGORITHM: str = "HS256" JWT_ALGORITHM: str = "HS256"
API_VERSION = "3.0.0" API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
PortType = Annotated[int, Path(gt=0, lt=65536)] PortType = Annotated[int, Path(gt=0, lt=65536)]

View File

@@ -58,15 +58,18 @@ class RouterModule():
def get_router_modules(): def get_router_modules():
res: list[RouterModule] = [] res: list[RouterModule] = []
for route in list_routers(): for route in list_routers():
module = getattr(__import__(f"routers.{route}"), route, None) try:
if module: module = getattr(__import__(f"routers.{route}"), route, None)
res.append(RouterModule( if module:
router=getattr(module, "app", None), res.append(RouterModule(
reset=getattr(module, "reset", None), router=getattr(module, "app", None),
startup=getattr(module, "startup", None), reset=getattr(module, "reset", None),
shutdown=getattr(module, "shutdown", None), startup=getattr(module, "startup", None),
name=route shutdown=getattr(module, "shutdown", None),
)) name=route
))
except Exception as e:
print(f"Router {route} failed to load: {e}")
return res return res
def load_routers(app): def load_routers(app):
@@ -74,6 +77,9 @@ def load_routers(app):
for router in get_router_modules(): for router in get_router_modules():
if router.router: if router.router:
app.include_router(router.router, prefix=f"/{router.name}", tags=[router.name]) 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: if router.reset:
resets.append(router.reset) resets.append(router.reset)
if router.startup: if router.startup:

View File

@@ -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=="], "@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=="], "@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/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=="], "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="],

View File

@@ -5,14 +5,14 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@mantine/core": "^7.16.2", "@mantine/core": "^7.16.3",
"@mantine/form": "^7.16.2", "@mantine/form": "^7.16.3",
"@mantine/hooks": "^7.16.2", "@mantine/hooks": "^7.16.3",
"@mantine/modals": "^7.16.2", "@mantine/modals": "^7.16.3",
"@mantine/notifications": "^7.16.2", "@mantine/notifications": "^7.16.3",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^20.17.16", "@types/node": "^20.17.17",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"buffer": "^6.0.3", "buffer": "^6.0.3",

View File

@@ -14,7 +14,7 @@ import { Firewall } from './pages/Firewall';
import { useQueryClient } from '@tanstack/react-query'; 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() { function App() {

View File

@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { ServerResponse } from "../../js/models" import { ServerResponse } from "../../js/models"
import { getapi, postapi } from "../../js/utils" import { getapi, postapi, putapi } from "../../js/utils"
export enum Protocol { export enum Protocol {
TCP = "tcp", TCP = "tcp",
@@ -79,15 +79,15 @@ export const firewall = {
return await getapi("firewall/settings") as FirewallSettings; return await getapi("firewall/settings") as FirewallSettings;
}, },
setsettings: async(data:FirewallSettings) => { setsettings: async(data:FirewallSettings) => {
return await postapi("firewall/settings/set", data) as ServerResponse; return await putapi("firewall/settings", data) as ServerResponse;
}, },
enable: async() => { enable: async() => {
return await getapi("firewall/enable") as ServerResponse; return await postapi("firewall/enable") as ServerResponse;
}, },
disable: async() => { disable: async() => {
return await getapi("firewall/disable") as ServerResponse; return await postapi("firewall/disable") as ServerResponse;
}, },
ruleset: async (data:RuleAddForm) => { ruleset: async (data:RuleAddForm) => {
return await postapi("firewall/rules/set", data) as ServerResponseListed; return await postapi("firewall/rules", data) as ServerResponseListed;
} }
} }

View File

@@ -0,0 +1,139 @@
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { 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<string|null>(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 <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
{!edit?<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>:null}
<Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" />
<Box className='center-flex'>
<Box>
{!edit?<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>:null}
<Space h="sm" />
<Switch
label={<Box className='center-flex'>
Enable fail-open nfqueue
<Space w="xs" />
<Tooltip label={<>
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
</>}>
<IoMdInformationCircleOutline size={15} />
</Tooltip>
</Box>}
{...form.getInputProps('fail_open', { type: 'checkbox' })}
/>
</Box>
<Box className="flex-spacer"></Box>
<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
{...form.getInputProps('proto')}
/>
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddEditService;

View File

@@ -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<string|null>(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 <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>
<Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" />
<Box className='center-flex'>
<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>
<Box className="flex-spacer"></Box>
<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
{...form.getInputProps('proto')}
/>
</Box>
<Group align="right" mt="md">
<Button loading={submitLoading} type="submit">Add Service</Button>
</Group>
<Space h="md" />
{error?<>
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" /></>:null}
</form>
</Modal>
}
export default AddNewService;

View File

@@ -49,16 +49,16 @@ function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>v
placeholder="Awesome Service Name!" placeholder="Awesome Service Name!"
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<Group align="right" mt="md"> <Group mt="md" justify="flex-end" mb="sm">
<Button loading={submitLoading} type="submit">Rename</Button> <Button loading={submitLoading} type="submit">Rename</Button>
</Group> </Group>
<Space h="md" />
{error?<> {error?<>
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}> <Space h="md" />
Error: {error} <Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
</Notification><Space h="md" /></>:null} Error: {error}
</Notification><Space h="md" />
</>:null}
</form> </form>
</Modal> </Modal>

View File

@@ -2,7 +2,7 @@ import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } fr
import { useState } from 'react'; import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa'; import { FaPlay, FaStop } from 'react-icons/fa';
import { nfregex, Service, serviceQueryKey } from '../utils'; import { nfregex, Service, serviceQueryKey } from '../utils';
import { MdOutlineArrowForwardIos } from "react-icons/md" import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
import YesNoModal from '../../YesNoModal'; import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils'; import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
import { BsTrashFill } from 'react-icons/bs'; import { BsTrashFill } from 'react-icons/bs';
@@ -10,8 +10,12 @@ import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm'; import RenameForm from './RenameForm';
import { MenuDropDownWithButton } from '../../MainLayout'; import { MenuDropDownWithButton } from '../../MainLayout';
import { useQueryClient } from '@tanstack/react-query'; 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"; let status_color = "gray";
switch(service.status){ switch(service.status){
@@ -24,6 +28,7 @@ function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void })
const [tooltipStopOpened, setTooltipStopOpened] = useState(false); const [tooltipStopOpened, setTooltipStopOpened] = useState(false);
const [deleteModal, setDeleteModal] = useState(false) const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false) const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const stopService = async () => { const stopService = async () => {
@@ -72,44 +77,43 @@ function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void })
return <> return <>
<Box className='firegex__nfregex__rowbox'> <Box className='firegex__nfregex__rowbox'>
<Grid className="firegex__nfregex__row" justify="flex-end" style={{width:"100%"}}> <Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Grid.Col span={{ md:4, xs: 12 }}> <Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<Box className={"center-flex-row"}> <MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name"> <Title className="firegex__nfregex__name" ml="xs">
{service.name} {service.name}
</Title> </Title>
<Box className="center-flex" style={{ gap: 6 }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">Status: <u>{service.status}</u></Badge>
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md">
:{service.port}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box> </Box>
</Grid.Col> <Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
:{service.port}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Grid.Col className={isMedium?"center-flex":"center-flex-row"} span={{ md:8, xs: 12 }}> <Box className={isMedium?"center-flex":"center-flex-row"}>
<Box visibleFrom='md' className='flex-spacer' />
<Space hiddenFrom='md' h="md" />
<Space hiddenFrom='md' w="xl" />
<Space hiddenFrom='md' w="md" />
<Box className="center-flex-row"> <Box className="center-flex-row">
<Badge color="yellow" radius="sm" size="md" variant="filled">Connections Blocked: {service.n_packets}</Badge>
<Space h="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled">Regex: {service.n_regex}</Badge>
<Space h="xs" />
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge> <Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
<Space h="xs" />
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
</Box>
</Box> </Box>
{isMedium?<Box className='flex-spacer' />:<Space h="xl" />} {isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex"> <Box className="center-flex">
<MenuDropDownWithButton> <MenuDropDownWithButton>
<Menu.Label><b>Rename service</b></Menu.Label> <Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item> <Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider /> <Divider />
<Menu.Label><b>Danger zone</b></Menu.Label> <Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item> <Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton> </MenuDropDownWithButton>
<Space w="md"/> <Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}> <Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}>
<ActionIcon color="red" loading={buttonLoading} <ActionIcon color="red" loading={buttonLoading}
@@ -129,14 +133,12 @@ function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void })
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
{isMedium?<Space w="xl" />:<Space w="md" />} {isMedium?<Space w="xl" />:<Space w="md" />}
{onClick?<Box style={{ backgroundColor: "var(--secondary_color)", borderRadius: "38%", width:"35px", height:"35px", display:"flex", justifyContent: "center", alignItems: "center", border:"#AAA 2px solid"}}> {onClick?<Box className='firegex__service_forward_btn'>
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} /> <MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
</Box>:null} </Box>:null}
{isMedium?<Space w="xl" />:null}
</Box> </Box>
</Box>
</Grid.Col> </Box>
</Grid>
</Box> </Box>
<YesNoModal <YesNoModal
title='Are you sure to delete this service?' title='Are you sure to delete this service?'
@@ -150,7 +152,10 @@ function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void })
opened={renameModal} opened={renameModal}
service={service} service={service}
/> />
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={service}
/>
</> </>
} }
export default ServiceRow;

View File

@@ -1,5 +1,5 @@
import { RegexFilter, ServerResponse } from "../../js/models" 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 { RegexAddForm } from "../../js/models"
import { useQuery, useQueryClient } from "@tanstack/react-query" import { useQuery, useQueryClient } from "@tanstack/react-query"
@@ -12,6 +12,7 @@ export type Service = {
ip_int: string, ip_int: string,
n_packets:number, n_packets:number,
n_regex:number, n_regex:number,
fail_open:boolean,
} }
export type ServiceAddForm = { export type ServiceAddForm = {
@@ -19,6 +20,14 @@ export type ServiceAddForm = {
port:number, port:number,
proto:string, proto:string,
ip_int:string, ip_int:string,
fail_open: boolean,
}
export type ServiceSettings = {
port?:number,
proto?:string,
ip_int?:string,
fail_open?: boolean,
} }
export type ServiceAddResponse = { export type ServiceAddResponse = {
@@ -40,44 +49,48 @@ export const nfregex = {
return await getapi("nfregex/services") as Service[]; return await getapi("nfregex/services") as Service[];
}, },
serviceinfo: async (service_id:string) => { 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) => { 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 return status === "ok"?undefined:status
}, },
regexenable: async (regex_id:number) => { 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 return status === "ok"?undefined:status
}, },
regexdisable: async (regex_id:number) => { 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 return status === "ok"?undefined:status
}, },
servicestart: async (service_id:string) => { 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 return status === "ok"?undefined:status
}, },
servicerename: async (service_id:string, name: string) => { 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 return status === "ok"?undefined:status
}, },
servicestop: async (service_id:string) => { 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 return status === "ok"?undefined:status
}, },
servicesadd: async (data:ServiceAddForm) => { 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) => { 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 return status === "ok"?undefined:status
}, },
regexesadd: async (data:RegexAddForm) => { 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 return status === "ok"?undefined:status
}, },
serviceregexes: async (service_id:string) => { 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
},
} }

View File

@@ -94,16 +94,16 @@ function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void })
/> />
</Box> </Box>
<Group align="right" mt="md"> <Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit">Add Service</Button> <Button loading={submitLoading} type="submit">Add Service</Button>
</Group> </Group>
<Space h="md" />
{error?<> {error?<>
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}> <Space h="md" />
Error: {error} <Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
</Notification><Space h="md" /></>:null} Error: {error}
</Notification><Space h="md" />
</>:null}
</form> </form>
</Modal> </Modal>

View File

@@ -53,15 +53,16 @@ function ChangeDestination({ opened, onClose, service }:{ opened:boolean, onClos
<form onSubmit={form.onSubmit(submitRequest)}> <form onSubmit={form.onSubmit(submitRequest)}>
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" /> <PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" />
<Group align="right" mt="md"> <Group justify='flex-end' mt="xl" mb="sm">
<Button loading={submitLoading} type="submit">Change</Button> <Button loading={submitLoading} type="submit">Change</Button>
</Group> </Group>
<Space h="md" />
{error?<> {error?<>
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}> <Space h="md" />
Error: {error} <Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
</Notification><Space h="md" /></>:null} Error: {error}
</Notification><Space h="md" />
</>:null}
</form> </form>
</Modal> </Modal>

View File

@@ -49,16 +49,16 @@ function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>v
placeholder="Awesome Service Name!" placeholder="Awesome Service Name!"
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<Group align="right" mt="md"> <Group mt="md" justify="flex-end" mb="sm">
<Button loading={submitLoading} type="submit">Rename</Button> <Button loading={submitLoading} type="submit">Rename</Button>
</Group> </Group>
<Space h="md" />
{error?<> {error?<>
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}> <Space h="md" />
Error: {error} <Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
</Notification><Space h="md" /></>:null} Error: {error}
</Notification><Space h="md" />
</>:null}
</form> </form>
</Modal> </Modal>

View File

@@ -8,11 +8,11 @@ import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi' import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm'; import RenameForm from './RenameForm';
import ChangeDestination from './ChangeDestination'; import ChangeDestination from './ChangeDestination';
import PortInput from '../../PortInput';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { MenuDropDownWithButton } from '../../MainLayout'; 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" 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" } 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 () => { const stopService = async () => {
setButtonLoading(true) setButtonLoading(true)
@@ -90,54 +72,36 @@ function ServiceRow({ service }:{ service:Service }) {
return <> return <>
<Box className='firegex__nfregex__rowbox'> <Box className='firegex__nfregex__rowbox'>
<Grid className="firegex__nfregex__row" justify="flex-end" style={{width:"100%"}}> <Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Grid.Col span={{ md:4, xs: 12 }}> <Box>
<Box className={"center-flex-row"}> <Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<Title className="firegex__nfregex__name"> <MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name} {service.name}
</Title> </Title>
<Box className="center-flex" style={{ gap: 6 }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">Status: <u>{service.active?"ENABLED":"DISABLED"}</u></Badge>
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="lg" variant="filled">
{service.proto}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box> </Box>
</Grid.Col> <Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
{service.proto}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Grid.Col className={isMedium?"center-flex":"center-flex-row"} span={{ md:8, xs: 12 }}> <Box className={isMedium?"center-flex":"center-flex-row"}>
<Box visibleFrom='md' className='flex-spacer' />
<Space hiddenFrom='md' h="md" />
<Space hiddenFrom='md' w="xl" />
<Space hiddenFrom='md' w="md" />
<Box className="center-flex-row"> <Box className="center-flex-row">
<Badge color="lime" radius="sm" size="md" variant="filled"> <Badge color="lime" radius="sm" size="lg" variant="filled">
FROM {service.ip_src} : {service.public_port} FROM {service.ip_src} :{service.public_port}
</Badge> </Badge>
<Space h="sm" /> <Space h="sm" />
<Badge color="blue" radius="sm" size="md" variant="filled"> <Badge color="blue" radius="sm" size="lg" variant="filled">
<Box className="center-flex"> <Box className="center-flex">
TO {service.ip_dst} : TO {service.ip_dst} :{service.proxy_port}
<form onSubmit={form.onSubmit((v)=>portInputRef.current?.blur())}>
<PortInput
defaultValue={service.proxy_port}
size="xs"
variant="unstyled"
style={{
width: (10+form.values.proxy_port.toString().length*6.2) +"px"
}}
className="firegex__porthijack__servicerow__portInput"
onBlur={(e)=>{onChangeProxyPort({proxy_port:parseInt(e.target.value)})}}
ref={portInputRef}
{...form.getInputProps("proxy_port")}
/>
</form>
</Box> </Box>
</Badge> </Badge>
</Box> </Box>
{isMedium?<Box className='flex-spacer' />:<Space h="xl" />} {isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex"> <Box className="center-flex">
<MenuDropDownWithButton> <MenuDropDownWithButton>
<Menu.Label><b>Rename service</b></Menu.Label> <Menu.Label><b>Rename service</b></Menu.Label>
@@ -166,14 +130,10 @@ function ServiceRow({ service }:{ service:Service }) {
<FaPlay size="20px" /> <FaPlay size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Box> </Box>
{isMedium?<Space w="xl" />:null} </Box>
</Box>
</Grid.Col>
</Grid>
</Box> </Box>
<YesNoModal <YesNoModal
title='Are you sure to delete this service?' title='Are you sure to delete this service?'
@@ -194,5 +154,3 @@ function ServiceRow({ service }:{ service:Service }) {
/> />
</> </>
} }
export default ServiceRow;

View File

@@ -1,5 +1,5 @@
import { ServerResponse } from "../../js/models" 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" import { useQuery } from "@tanstack/react-query"
export type GeneralStats = { export type GeneralStats = {
@@ -37,28 +37,28 @@ export const porthijack = {
return await getapi("porthijack/services") as Service[]; return await getapi("porthijack/services") as Service[];
}, },
serviceinfo: async (service_id:string) => { 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) => { 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 return status === "ok"?undefined:status
}, },
servicerename: async (service_id:string, name: string) => { 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 return status === "ok"?undefined:status
}, },
servicestop: async (service_id:string) => { 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 return status === "ok"?undefined:status
}, },
servicesadd: async (data:ServiceAddForm) => { 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) => { 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 return status === "ok"?undefined:status
}, },
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => { 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;
} }
} }

View File

@@ -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 { useState } from 'react';
import { RegexFilter } from '../../js/models'; 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 { BsTrashFill } from "react-icons/bs"
import YesNoModal from '../YesNoModal'; import YesNoModal from '../YesNoModal';
import { FaPause, FaPlay } from 'react-icons/fa'; import { FaPause, FaPlay } from 'react-icons/fa';
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import { FaFilter } from "react-icons/fa";
import { VscRegex } from "react-icons/vsc";
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
const mode_string = regexInfo.mode === "C"? "C -> S": const mode_string = regexInfo.mode === "C"? "C -> S":
regexInfo.mode === "S"? "S -> C": regexInfo.mode === "S"? "S -> C":
regexInfo.mode === "B"? "S <-> C": "🤔" regexInfo.mode === "B"? "C <-> S": "🤔"
let regex_expr = b64decode(regexInfo.regex); let regex_expr = b64decode(regexInfo.regex);
@@ -20,6 +21,7 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
const [deleteTooltipOpened, setDeleteTooltipOpened] = useState(false); const [deleteTooltipOpened, setDeleteTooltipOpened] = useState(false);
const [statusTooltipOpened, setStatusTooltipOpened] = useState(false); const [statusTooltipOpened, setStatusTooltipOpened] = useState(false);
const clipboard = useClipboard({ timeout: 500 }); const clipboard = useClipboard({ timeout: 500 });
const isMedium = isMediumScreen();
const deleteRegex = () => { const deleteRegex = () => {
getapiobject().regexdelete(regexInfo.id).then(res => { getapiobject().regexdelete(regexInfo.id).then(res => {
@@ -42,57 +44,39 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
} }
return <Box className="firegex__regexview__box"> return <Box className="firegex__regexview__box">
<Grid> <Box>
<Grid.Col span={2} className="center-flex"> <Box className='center-flex' style={{width: "100%"}}>
<Title order={4}>Regex:</Title>
</Grid.Col>
<Grid.Col span={8}>
<Box className="firegex__regexview__outer_regex_text"> <Box className="firegex__regexview__outer_regex_text">
<Text className="firegex__regexview__regex_text" onClick={()=>{ <Text className="firegex__regexview__regex_text" onClick={()=>{
clipboard.copy(regex_expr) clipboard.copy(regex_expr)
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`) okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
}}>{regex_expr}</Text> }}>{regex_expr}</Text>
</Box> </Box>
</Grid.Col>
<Grid.Col span={2} className='center-flex'>
<Space w="xs" /> <Space w="xs" />
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"} opened={statusTooltipOpened}> <Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"} opened={statusTooltipOpened}>
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled" <ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
onFocus={() => setStatusTooltipOpened(false)} onBlur={() => setStatusTooltipOpened(false)} onFocus={() => setStatusTooltipOpened(false)} onBlur={() => setStatusTooltipOpened(false)}
onMouseEnter={() => setStatusTooltipOpened(true)} onMouseLeave={() => setStatusTooltipOpened(false)} onMouseEnter={() => setStatusTooltipOpened(true)} onMouseLeave={() => setStatusTooltipOpened(false)}
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon> >{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Delete regex" zIndex={0} color="red" opened={deleteTooltipOpened} > <Tooltip label="Delete regex" zIndex={0} color="red" opened={deleteTooltipOpened} >
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled" <ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled"
onFocus={() => setDeleteTooltipOpened(false)} onBlur={() => setDeleteTooltipOpened(false)} onFocus={() => setDeleteTooltipOpened(false)} onBlur={() => setDeleteTooltipOpened(false)}
onMouseEnter={() => setDeleteTooltipOpened(true)} onMouseLeave={() => setDeleteTooltipOpened(false)} onMouseEnter={() => setDeleteTooltipOpened(true)} onMouseLeave={() => setDeleteTooltipOpened(false)}
><BsTrashFill size={22} /></ActionIcon> ><BsTrashFill size={22} /></ActionIcon>
</Tooltip> </Tooltip>
</Box>
</Grid.Col> <Box display="flex" mt="sm" ml="xs">
<Grid.Col className='center-flex' span={12}> <Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
<Box className='center-flex-row'> <Space w="xs" />
<Space h="md" /> <Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
<Box className='center-flex'> <Space w="xs" />
<Badge size="md" color="cyan" variant="filled">Service: {regexInfo.service_id}</Badge> <Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
<Space w="xs" /> <Space w="xs" />
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge> <Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
<Space w="xs" /> </Box>
<Badge size="md" color="gray" variant="filled">ID: {regexInfo.id}</Badge> </Box>
</Box>
</Box>
<Box className='flex-spacer' />
<Box className='center-flex-row'>
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">Case: {regexInfo.is_case_sensitive?"SENSIIVE":"INSENSITIVE"}</Badge>
<Space h="xs" />
<Badge size="md" color="yellow" variant="filled">Packets filtered: {regexInfo.n_packets}</Badge>
<Space h="xs" />
<Badge size="md" color="blue" variant="filled">Mode: {mode_string}</Badge>
</Box>
</Grid.Col>
</Grid>
<YesNoModal <YesNoModal
title='Are you sure to delete this regex?' title='Are you sure to delete this regex?'
description={`You are going to delete the regex '${regex_expr}'.`} description={`You are going to delete the regex '${regex_expr}'.`}

View File

@@ -5,7 +5,7 @@ function YesNoModal( { title, description, action, onClose, opened}:{ title:stri
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered> return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
{description} {description}
<Group align="right" mt="md"> <Group justify='flex-end' mt="md">
<Button onClick={()=>{ <Button onClick={()=>{
onClose() onClose()
action() action()

View File

@@ -32,14 +32,15 @@ body {
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 4px;
margin:3px; height: 4px;
margin:2px;
background: #333; background: #333;
cursor: pointer;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #757575; width: 2px;
border-radius: 8px; background: #777;
border-radius: 5px;
} }
.mantine-Modal-content { .mantine-Modal-content {
@@ -72,7 +73,7 @@ body {
} }
.firegex__regexview__box{ .firegex__regexview__box{
padding:30px; padding:10px;
margin:5px; margin:5px;
} }
@@ -80,6 +81,7 @@ body {
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
margin: 6px; margin: 6px;
width: 100%;
} }
.firegex__regexview__regex_text{ .firegex__regexview__regex_text{
@@ -131,25 +133,25 @@ body {
text-decoration: underline; text-decoration: underline;
} }
.firegex__nfregex__row{ .firegex__nfregex__row{
width: 95%; width: 95%;
padding: 20px; padding: 10px;
border-radius: 20px; padding-left: 35px;
padding-right: 35px;
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
} }
.firegex__nfregex__name{ .firegex__nfregex__name{
font-size: 1.8em; font-size: 1.6em;
font-weight: bolder; font-weight: bolder;
margin-right: 10px; text-transform: capitalize;
margin-bottom: 13px;
color:#FFF; color:#FFF;
max-width: 300px; max-width: 300px;
overflow: hidden; overflow: hidden;
text-align: center; margin-left: -20px;
margin-right: 20px;
} }
.firegex__nfregex__name:hover{ .firegex__nfregex__name:hover{
@@ -207,7 +209,47 @@ body {
.firegex__nfregex__rowbox{ .firegex__nfregex__rowbox{
width: 100%; width: 100%;
height: 150px;
align-items: center;
justify-content: center;
display: flex;
background-color: var(--fourth_color); background-color: var(--fourth_color);
border-radius: 20px; border-radius: 10px;
border: #444 3px solid; border: #444 3px solid;
}
@media (max-width: 599px) {
.firegex__nfregex__row{
margin: 0;
padding: 0;
}
.firegex__nfregex__row > *{
padding: 10px 0;
}
.firegex__nfregex__rowbox{
height: 100%;
width: 90%;
padding: 20px 20px;
margin: 0;
}
.firegex__nfregex__name{
padding: 0;
margin: 0;
}
}
.firegex__service_forward_btn{
background-color: var(--primary_color);
border-radius: 38%;
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #DDD;
transition: 0.3s;
}
.firegex__service_forward_btn:hover{
opacity: 0.8;
transition: 0.3s;
} }

View File

@@ -22,26 +22,6 @@ export const queryClient = new QueryClient({ defaultOptions: { queries: {
staleTime: Infinity staleTime: Infinity
} }}) } }})
export async function getapi(path:string):Promise<any>{
return await new Promise((resolve, reject) => {
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`,{
credentials: "same-origin",
headers: { "Authorization" : "Bearer " + window.localStorage.getItem("access_token")}
}).then(res => {
if(res.status === 401) window.location.reload()
if(!res.ok){
const errorDefault = res.statusText
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
}
res.json().then( res => resolve(res) ).catch( err => reject(err))
})
.catch(err => {
reject(err)
})
});
}
export function getErrorMessage(e: any) { export function getErrorMessage(e: any) {
let error = "Unknown error"; let error = "Unknown error";
if(typeof e == "string") return e if(typeof e == "string") return e
@@ -56,7 +36,6 @@ export function getErrorMessage(e: any) {
return error; return error;
} }
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") { export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
if (e.status){ if (e.status){
return e.status return e.status
@@ -74,17 +53,17 @@ export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown
} }
export async function postapi(path:string,data:any,is_form:boolean=false):Promise<any>{ export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, { fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
method: 'POST', method: method,
credentials: "same-origin", credentials: "same-origin",
cache: 'no-cache', cache: 'no-cache',
headers: { headers: {
'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json', ...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
"Authorization" : "Bearer " + window.localStorage.getItem("access_token") "Authorization" : "Bearer " + window.localStorage.getItem("access_token")
}, },
body: is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data) body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
}).then(res => { }).then(res => {
if(res.status === 401) window.location.reload() if(res.status === 401) window.location.reload()
if(res.status === 406) resolve({status:"Wrong Password"}) if(res.status === 406) resolve({status:"Wrong Password"})
@@ -100,6 +79,22 @@ export async function postapi(path:string,data:any,is_form:boolean=false):Promis
}); });
} }
export async function getapi(path:string):Promise<any>{
return await genericapi("GET",path)
}
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
return await genericapi("POST",path,data,is_form)
}
export async function deleteapi(path:string):Promise<any>{
return await genericapi("DELETE",path)
}
export async function putapi(path:string,data:any):Promise<any>{
return await genericapi("PUT",path,data)
}
export function getMainPath(){ export function getMainPath(){
const paths = window.location.pathname.split("/") const paths = window.location.pathname.split("/")
if (paths.length > 1) return paths[1] if (paths.length > 1) return paths[1]

View File

@@ -1,13 +1,27 @@
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react'; import { Navigate, useNavigate, useParams } from 'react-router-dom';
import { Navigate, useParams } from 'react-router-dom';
import RegexView from '../../components/RegexView'; import RegexView from '../../components/RegexView';
import ServiceRow from '../../components/NFRegex/ServiceRow';
import AddNewRegex from '../../components/AddNewRegex'; import AddNewRegex from '../../components/AddNewRegex';
import { BsPlusLg } from "react-icons/bs"; import { BsPlusLg } from "react-icons/bs";
import { nfregexServiceQuery, nfregexServiceRegexesQuery } from '../../components/NFRegex/utils'; import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
import { Badge, Divider, Menu } from '@mantine/core';
import { useState } from 'react';
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
import { MdDoubleArrow } from "react-icons/md"
import YesNoModal from '../../components/YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
import { MenuDropDownWithButton } from '../../components/MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaArrowLeft } from "react-icons/fa";
import { VscRegex } from 'react-icons/vsc';
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../../components/NFRegex/AddEditService';
function ServiceDetailsNFRegex() { export default function ServiceDetailsNFRegex() {
const {srv} = useParams() const {srv} = useParams()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -15,13 +29,140 @@ function ServiceDetailsNFRegex() {
const serviceInfo = services.data?.find(s => s.service_id == srv) const serviceInfo = services.data?.find(s => s.service_id == srv)
const [tooltipAddRegexOpened, setTooltipAddRegexOpened] = useState(false) const [tooltipAddRegexOpened, setTooltipAddRegexOpened] = useState(false)
const regexesList = nfregexServiceRegexesQuery(srv??"") const regexesList = nfregexServiceRegexesQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient()
const [tooltipStopOpened, setTooltipStopOpened] = useState(false);
const [tooltipBackOpened, setTooltipBackOpened] = useState(false);
const navigate = useNavigate()
const isMedium = isMediumScreen()
if (services.isLoading) return <LoadingOverlay visible={true} /> if (services.isLoading) return <LoadingOverlay visible={true} />
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace /> if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
let status_color = "gray";
switch(serviceInfo.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const startService = async () => {
setButtonLoading(true)
await nfregex.servicestart(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false)
}
const deleteService = () => {
nfregex.servicedelete(serviceInfo.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
const stopService = async () => {
setButtonLoading(true)
await nfregex.servicestop(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false);
}
return <> return <>
<LoadingOverlay visible={regexesList.isLoading} /> <LoadingOverlay visible={regexesList.isLoading} />
<ServiceRow service={serviceInfo} /> <Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box>
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
</Box>
</Title>
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex'>
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
{serviceInfo.status}
</Badge>
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
:{serviceInfo.port}
</Badge>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
</Box>
</Box>
{isMedium?null:<Space h="md" />}
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
</Box>
{isMedium?<Space w="xs" />:<Space h="xs" />}
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
</Box>
{isMedium?null:<Space h="xl" />}
<Box className='center-flex'>
<Tooltip label="Go back" zIndex={0} color="cyan" opened={tooltipBackOpened}>
<ActionIcon color="cyan"
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-back-id"
onFocus={() => setTooltipBackOpened(false)} onBlur={() => setTooltipBackOpened(false)}
onMouseEnter={() => setTooltipBackOpened(true)} onMouseLeave={() => setTooltipBackOpened(false)}>
<FaArrowLeft size="25px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red" opened={tooltipStopOpened}>
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={serviceInfo.status === "stop"}
aria-describedby="tooltip-stop-id"
onFocus={() => setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)}
onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}>
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="xl" />
{(!regexesList.data || regexesList.data.length == 0)?<> {(!regexesList.data || regexesList.data.length == 0)?<>
<Space h="xl" /> <Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title> <Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
@@ -41,7 +182,22 @@ function ServiceDetailsNFRegex() {
} }
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null} {srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
onClose={()=>setDeleteModal(false) }
action={deleteService}
opened={deleteModal}
/>
<RenameForm
onClose={()=>setRenameModal(false)}
opened={renameModal}
service={serviceInfo}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={serviceInfo}
/>
</> </>
} }
export default ServiceDetailsNFRegex;

View File

@@ -5,7 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import ServiceRow from '../../components/NFRegex/ServiceRow'; import ServiceRow from '../../components/NFRegex/ServiceRow';
import { nfregexServiceQuery } from '../../components/NFRegex/utils'; import { nfregexServiceQuery } from '../../components/NFRegex/utils';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddNewService from '../../components/NFRegex/AddNewService'; import AddEditService from '../../components/NFRegex/AddEditService';
import AddNewRegex from '../../components/AddNewRegex'; import AddNewRegex from '../../components/AddNewRegex';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb'; import { TbReload } from 'react-icons/tb';
@@ -81,13 +81,12 @@ function NFRegex({ children }: { children: any }) {
</Tooltip> </Tooltip>
</Box> </Box>
</>} </>}
<AddNewService opened={open} onClose={closeModal} />
</>} </>}
</Box> </Box>
{srv?children:null} {srv?children:null}
{srv? {srv?
<AddNewRegex opened={open} onClose={closeModal} service={srv} />: <AddNewRegex opened={open} onClose={closeModal} service={srv} />:
<AddNewService opened={open} onClose={closeModal} /> <AddEditService opened={open} onClose={closeModal} />
} }
</> </>
} }

1
proxy-client/MANIFEST.in Normal file
View File

@@ -0,0 +1 @@
include requirements.txt

3
proxy-client/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Firegex Python Library and CLI
It's a work in progress!

7
proxy-client/fgex Executable file
View File

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

View File

@@ -0,0 +1,5 @@
# Firegex python library
Alias of 'firegex' libaray
It's a work in progress!

View File

@@ -0,0 +1 @@
from firegex import *

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
from firegex.cli import run
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,25 @@
import setuptools
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setuptools.setup(
name="fgex",
version="0.0.0",
author="Pwnzer0tt1",
author_email="pwnzer0tt1@poliba.it",
py_modules=["fgex"],
install_requires=["fgex"],
include_package_data=True,
description="Firegex client",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pwnzer0tt1/firegex",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
],
python_requires='>=3.10',
)

View File

@@ -0,0 +1,7 @@
__version__ = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
#Exported functions
__all__ = [
]

View File

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

View File

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

31
proxy-client/setup.py Normal file
View File

@@ -0,0 +1,31 @@
import setuptools
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
with open('requirements.txt', 'r', encoding='utf-8') as f:
required = [ele.strip() for ele in f.read().splitlines() if not ele.strip().startswith("#") and ele.strip() != ""]
VERSION = "{{VERSION_PLACEHOLDER}}"
setuptools.setup(
name="firegex",
version= VERSION if "{" not in VERSION else "0.0.0", #uv pip install -U . --no-cache-dir for testing
author="Pwnzer0tt1",
author_email="pwnzer0tt1@poliba.it",
scripts=["fgex"],
py_modules=["fgex"],
install_requires=required,
include_package_data=True,
description="Firegex client",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pwnzer0tt1/firegex",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
],
python_requires='>=3.10',
)

View File

@@ -1,6 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import argparse, sys, platform, os, multiprocessing, subprocess, getpass import argparse
import sys
import platform
import os
import multiprocessing
import subprocess
import getpass
pref = "\033[" pref = "\033["
reset = f"{pref}0m" reset = f"{pref}0m"
@@ -36,7 +43,7 @@ def dict_to_yaml(data, indent_spaces:int=4, base_indent:int=0, additional_spaces
spaces = ' '*((indent_spaces*base_indent)+additional_spaces) spaces = ' '*((indent_spaces*base_indent)+additional_spaces)
if isinstance(data, dict): if isinstance(data, dict):
for key, value in data.items(): for key, value in data.items():
if not add_text_on_dict is None: if add_text_on_dict is not None:
spaces_len = len(spaces)-len(add_text_on_dict) spaces_len = len(spaces)-len(add_text_on_dict)
spaces = (' '*max(spaces_len, 0))+add_text_on_dict spaces = (' '*max(spaces_len, 0))+add_text_on_dict
add_text_on_dict = None add_text_on_dict = None
@@ -76,7 +83,7 @@ def composecmd(cmd, composefile=None):
puts("Docker compose not found! please install docker compose!", color=colors.red) puts("Docker compose not found! please install docker compose!", color=colors.red)
def check_already_running(): def check_already_running():
return "firegex" in cmd_check(f'docker ps --filter "name=^firegex$"', get_output=True) return "firegex" in cmd_check('docker ps --filter "name=^firegex$"', get_output=True)
def gen_args(args_to_parse: list[str]|None = None): def gen_args(args_to_parse: list[str]|None = None):
@@ -97,6 +104,7 @@ def gen_args(args_to_parse: list[str]|None = None):
parser_start.add_argument('--startup-psw','-P', required=False, action="store_true", help='Insert password in the startup screen of firegex', default=False) parser_start.add_argument('--startup-psw','-P', required=False, action="store_true", help='Insert password in the startup screen of firegex', default=False)
parser_start.add_argument('--port', "-p", type=int, required=False, help='Port where open the web service of the firewall', default=4444) parser_start.add_argument('--port', "-p", type=int, required=False, help='Port where open the web service of the firewall', default=4444)
parser_start.add_argument('--logs', required=False, action="store_true", help='Show firegex logs', default=False) parser_start.add_argument('--logs', required=False, action="store_true", help='Show firegex logs', default=False)
parser_start.add_argument('--version', '-v', required=False, type=str , help='Version of the firegex image to use', default="latest")
#Stop Command #Stop Command
parser_stop = subcommands.add_parser('stop', help='Stop the firewall') parser_stop = subcommands.add_parser('stop', help='Stop the firewall')
@@ -106,13 +114,13 @@ def gen_args(args_to_parse: list[str]|None = None):
parser_restart.add_argument('--logs', required=False, action="store_true", help='Show firegex logs', default=False) parser_restart.add_argument('--logs', required=False, action="store_true", help='Show firegex logs', default=False)
args = parser.parse_args(args=args_to_parse) args = parser.parse_args(args=args_to_parse)
if not "clear" in args: if "clear" not in args:
args.clear = False args.clear = False
if not "threads" in args or args.threads < 1: if "threads" not in args or args.threads < 1:
args.threads = multiprocessing.cpu_count() args.threads = multiprocessing.cpu_count()
if not "port" in args or args.port < 1: if "port" not in args or args.port < 1:
args.port = 4444 args.port = 4444
if args.command is None: if args.command is None:
@@ -126,7 +134,7 @@ def gen_args(args_to_parse: list[str]|None = None):
args = gen_args() args = gen_args()
def is_linux(): def is_linux():
return "linux" in sys.platform and not 'microsoft-standard' in platform.uname().release return "linux" in sys.platform and 'microsoft-standard' not in platform.uname().release
def write_compose(skip_password = True): def write_compose(skip_password = True):
psw_set = get_password() if not skip_password else None psw_set = get_password() if not skip_password else None
@@ -138,7 +146,7 @@ def write_compose(skip_password = True):
"firewall": { "firewall": {
"restart": "unless-stopped", "restart": "unless-stopped",
"container_name": "firegex", "container_name": "firegex",
"build" if g.build else "image": "." if g.build else "ghcr.io/pwnzer0tt1/firegex", "build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
"network_mode": "host", "network_mode": "host",
"environment": [ "environment": [
f"PORT={args.port}", f"PORT={args.port}",
@@ -183,7 +191,7 @@ def write_compose(skip_password = True):
"firewall": { "firewall": {
"restart": "unless-stopped", "restart": "unless-stopped",
"container_name": "firegex", "container_name": "firegex",
"build" if g.build else "image": "." if g.build else "ghcr.io/pwnzer0tt1/firegex", "build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
"ports": [ "ports": [
f"{args.port}:{args.port}" f"{args.port}:{args.port}"
], ],
@@ -229,10 +237,13 @@ def get_password():
def volume_exists(): def volume_exists():
return "firegex_firegex_data" in cmd_check(f'docker volume ls --filter "name=^firegex_firegex_data$"', get_output=True) return "firegex_firegex_data" in cmd_check('docker volume ls --filter "name=^firegex_firegex_data$"', get_output=True)
def nfqueue_exists(): def nfqueue_exists():
import socket, fcntl, os, time import socket
import fcntl
import os
import time
NETLINK_NETFILTER = 12 NETLINK_NETFILTER = 12
SOL_NETLINK = 270 SOL_NETLINK = 270
@@ -241,7 +252,7 @@ def nfqueue_exists():
nfsock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_NETFILTER) nfsock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_NETFILTER)
fcntl.fcntl(nfsock, fcntl.F_SETFL, os.O_RDONLY|os.O_NONBLOCK) fcntl.fcntl(nfsock, fcntl.F_SETFL, os.O_RDONLY|os.O_NONBLOCK)
nfsock.setsockopt(SOL_NETLINK, NETLINK_EXT_ACK, 1) nfsock.setsockopt(SOL_NETLINK, NETLINK_EXT_ACK, 1)
except Exception as e: except Exception:
return False return False
for rev in [3,2,1,0]: for rev in [3,2,1,0]:
@@ -252,10 +263,13 @@ def nfqueue_exists():
nfsock.send(payload) nfsock.send(payload)
data = nfsock.recv(1024) data = nfsock.recv(1024)
is_error = data[4] == 2 is_error = data[4] == 2
if not is_error: return True # The module exists and we have permission to use it if not is_error:
return True # The module exists and we have permission to use it
error_code = int.from_bytes(data[16:16+4], signed=True, byteorder='little') error_code = int.from_bytes(data[16:16+4], signed=True, byteorder='little')
if error_code == -1: return True # EPERM (the user is not root, but the module exists) if error_code == -1:
if error_code == -2: pass # ENOENT (the module does not exist) return True # EPERM (the user is not root, but the module exists)
if error_code == -2:
pass # ENOENT (the module does not exist)
else: else:
puts("Error while trying to check if the nfqueue module is loaded, this check will be skipped!", color=colors.yellow) puts("Error while trying to check if the nfqueue module is loaded, this check will be skipped!", color=colors.yellow)
return True return True
@@ -294,7 +308,7 @@ def main():
if check_already_running(): if check_already_running():
puts("Firegex is already running! use --help to see options useful to manage firegex execution", color=colors.yellow) puts("Firegex is already running! use --help to see options useful to manage firegex execution", color=colors.yellow)
else: else:
puts(f"Firegex", color=colors.yellow, end="") puts("Firegex", color=colors.yellow, end="")
puts(" will start on port ", end="") puts(" will start on port ", end="")
puts(f"{args.port}", color=colors.cyan) puts(f"{args.port}", color=colors.cyan)
write_compose(skip_password=False) write_compose(skip_password=False)

View File

@@ -15,7 +15,7 @@ puts(f"{args.address}", color=colors.yellow)
firegex = FiregexAPI(args.address) firegex = FiregexAPI(args.address)
#Connect to Firegex #Connect to Firegex
if firegex.status()["status"] =="init": if firegex.status()["status"] == "init":
if (firegex.set_password(args.password)): if (firegex.set_password(args.password)):
puts(f"Sucessfully set password to {args.password}", color=colors.green) puts(f"Sucessfully set password to {args.password}", color=colors.green)
else: else:

View File

@@ -117,4 +117,4 @@ else:
puts("Test Failed: Couldn't delete service ✗", color=colors.red) puts("Test Failed: Couldn't delete service ✗", color=colors.red)
exit(1) exit(1)
server.terminate() server.terminate()

View File

@@ -19,11 +19,17 @@ class BearerSession():
headers["Content-Type"] = "application/x-www-form-urlencoded" headers["Content-Type"] = "application/x-www-form-urlencoded"
return self.s.post(endpoint, json=json, data=data, headers=headers) return self.s.post(endpoint, json=json, data=data, headers=headers)
def delete(self, endpoint, json={}):
return self.s.delete(endpoint, json=json, headers=self.headers)
def put(self, endpoint, json={}):
return self.s.put(endpoint, json=json, headers=self.headers)
def get(self, endpoint, json={}): def get(self, endpoint, json={}):
return self.s.get(endpoint, json=json, headers=self.headers) return self.s.get(endpoint, json=json, headers=self.headers)
def set_token(self,token): def set_token(self,token):
self.headers = {"Authorization": f"Bearer {token}"} self.headers = {"Authorization": f"Bearer {token}"}
def unset_token(self): def unset_token(self):
self.headers = {} self.headers = {}
@@ -72,62 +78,57 @@ class FiregexAPI:
def reset(self, delete: bool): def reset(self, delete: bool):
self.s.post(f"{self.address}api/reset", json={"delete":delete}) self.s.post(f"{self.address}api/reset", json={"delete":delete})
#Netfilter regex
def nf_get_stats(self):
req = self.s.get(f"{self.address}api/nfregex/stats")
return req.json()
def nf_get_services(self): def nf_get_services(self):
req = self.s.get(f"{self.address}api/nfregex/services") req = self.s.get(f"{self.address}api/nfregex/services")
return req.json() return req.json()
def nf_get_service(self,service_id: str): def nf_get_service(self,service_id: str):
req = self.s.get(f"{self.address}api/nfregex/service/{service_id}") req = self.s.get(f"{self.address}api/nfregex/services/{service_id}")
return req.json() return req.json()
def nf_stop_service(self,service_id: str): def nf_stop_service(self,service_id: str):
req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/stop") req = self.s.post(f"{self.address}api/nfregex/services/{service_id}/stop")
return verify(req) return verify(req)
def nf_start_service(self,service_id: str): def nf_start_service(self,service_id: str):
req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/start") req = self.s.post(f"{self.address}api/nfregex/services/{service_id}/start")
return verify(req) return verify(req)
def nf_delete_service(self,service_id: str): def nf_delete_service(self,service_id: str):
req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/delete") req = self.s.delete(f"{self.address}api/nfregex/services/{service_id}")
return verify(req) return verify(req)
def nf_rename_service(self,service_id: str, newname: str): def nf_rename_service(self,service_id: str, newname: str):
req = self.s.post(f"{self.address}api/nfregex/service/{service_id}/rename" , json={"name":newname}) req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/rename" , json={"name":newname})
return verify(req) return verify(req)
def nf_get_service_regexes(self,service_id: str): def nf_get_service_regexes(self,service_id: str):
req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/regexes") req = self.s.get(f"{self.address}api/nfregex/services/{service_id}/regexes")
return req.json() return req.json()
def nf_get_regex(self,regex_id: str): def nf_get_regex(self,regex_id: str):
req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}") req = self.s.get(f"{self.address}api/nfregex/regexes/{regex_id}")
return req.json() return req.json()
def nf_delete_regex(self,regex_id: str): def nf_delete_regex(self,regex_id: str):
req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}/delete") req = self.s.delete(f"{self.address}api/nfregex/regexes/{regex_id}")
return verify(req) return verify(req)
def nf_enable_regex(self,regex_id: str): def nf_enable_regex(self,regex_id: str):
req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}/enable") req = self.s.post(f"{self.address}api/nfregex/regexes/{regex_id}/enable")
return verify(req) return verify(req)
def nf_disable_regex(self,regex_id: str): def nf_disable_regex(self,regex_id: str):
req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}/disable") req = self.s.post(f"{self.address}api/nfregex/regexes/{regex_id}/disable")
return verify(req) return verify(req)
def nf_add_regex(self, service_id: str, regex: str, mode: str, active: bool, is_case_sensitive: bool): def nf_add_regex(self, service_id: str, regex: str, mode: str, active: bool, is_case_sensitive: bool):
req = self.s.post(f"{self.address}api/nfregex/regexes/add", req = self.s.post(f"{self.address}api/nfregex/regexes",
json={"service_id": service_id, "regex": regex, "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive}) json={"service_id": service_id, "regex": regex, "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive})
return verify(req) return verify(req)
def nf_add_service(self, name: str, port: int, proto: str, ip_int: str): def nf_add_service(self, name: str, port: int, proto: str, ip_int: str):
req = self.s.post(f"{self.address}api/nfregex/services/add" , req = self.s.post(f"{self.address}api/nfregex/services" ,
json={"name":name,"port":port, "proto": proto, "ip_int": ip_int}) json={"name":name,"port":port, "proto": proto, "ip_int": ip_int})
return req.json()["service_id"] if verify(req) else False return req.json()["service_id"] if verify(req) else False
@@ -137,30 +138,30 @@ class FiregexAPI:
return req.json() return req.json()
def ph_get_service(self,service_id: str): def ph_get_service(self,service_id: str):
req = self.s.get(f"{self.address}api/porthijack/service/{service_id}") req = self.s.get(f"{self.address}api/porthijack/services/{service_id}")
return req.json() return req.json()
def ph_stop_service(self,service_id: str): def ph_stop_service(self,service_id: str):
req = self.s.get(f"{self.address}api/porthijack/service/{service_id}/stop") req = self.s.post(f"{self.address}api/porthijack/services/{service_id}/stop")
return verify(req) return verify(req)
def ph_start_service(self,service_id: str): def ph_start_service(self,service_id: str):
req = self.s.get(f"{self.address}api/porthijack/service/{service_id}/start") req = self.s.post(f"{self.address}api/porthijack/services/{service_id}/start")
return verify(req) return verify(req)
def ph_delete_service(self,service_id: str): def ph_delete_service(self,service_id: str):
req = self.s.get(f"{self.address}api/porthijack/service/{service_id}/delete") req = self.s.delete(f"{self.address}api/porthijack/services/{service_id}")
return verify(req) return verify(req)
def ph_rename_service(self,service_id: str,newname: str): def ph_rename_service(self,service_id: str,newname: str):
req = self.s.post(f"{self.address}api/porthijack/service/{service_id}/rename" , json={"name":newname}) req = self.s.put(f"{self.address}api/porthijack/services/{service_id}/rename" , json={"name":newname})
return verify(req) return verify(req)
def ph_change_destination(self,service_id: str, ip_dst:string , proxy_port: int): def ph_change_destination(self,service_id: str, ip_dst:string , proxy_port: int):
req = self.s.post(f"{self.address}api/porthijack/service/{service_id}/change-destination", json={"ip_dst": ip_dst, "proxy_port": proxy_port}) req = self.s.put(f"{self.address}api/porthijack/services/{service_id}/change-destination", json={"ip_dst": ip_dst, "proxy_port": proxy_port})
return verify(req) return verify(req)
def ph_add_service(self, name: str, public_port: int, proxy_port: int, proto: str, ip_src: str, ip_dst: str): def ph_add_service(self, name: str, public_port: int, proxy_port: int, proto: str, ip_src: str, ip_dst: str):
req = self.s.post(f"{self.address}api/porthijack/services/add" , req = self.s.post(f"{self.address}api/porthijack/services" ,
json={"name":name, "public_port": public_port, "proxy_port":proxy_port, "proto": proto, "ip_src": ip_src, "ip_dst": ip_dst}) json={"name":name, "public_port": public_port, "proxy_port":proxy_port, "proto": proto, "ip_src": ip_src, "ip_dst": ip_dst})
return req.json()["service_id"] if verify(req) else False return req.json()["service_id"] if verify(req) else False