Compare commits

...

17 Commits

Author SHA1 Message Date
Ilya Starchak
c387d9b40b sd 2025-12-10 02:30:30 +03:00
Ilya Starchak
d8061985d6 asdas 2025-12-10 02:27:35 +03:00
Ilya Starchak
c237112077 sd 2025-12-10 02:17:54 +03:00
Your Name
811773e009 dsa 2025-12-08 02:08:30 +03:00
Your Name
f1ebada95d asd 2025-12-08 01:55:16 +03:00
Your Name
9af3023a37 dsa 2025-12-08 01:41:08 +03:00
Domingo Dirutigliano
16f96aa6f6 using brotli 1.2 from pypi, fixed tests to py 3.14, removed from experimental pyproxy 2025-11-11 23:30:48 +01:00
Domingo Dirutigliano
49c6c14fe5 upgrade to py3.14 and fedora 43 2025-10-13 15:38:38 +02:00
Domingo Dirutigliano
b352790f64 Merge pull request #31 from Minei3oat/restart-fixes
Fix caching problems on fresh restart
2025-10-03 08:17:48 +02:00
Minei3oat
f3024cc9a8 Invalidate cache on login 2025-10-03 00:58:55 +02:00
Minei3oat
753ed241b6 Clear password fields after submit 2025-10-03 00:58:34 +02:00
Domingo Dirutigliano
676e1dcb77 Merge pull request #30 from Minei3oat/socketio
Reconnect to websocket on access_token change
2025-10-02 23:13:24 +02:00
Minei3oat
0c5a681f5b Reconnect to websocket on access_token change 2025-10-02 23:10:26 +02:00
Domingo Dirutigliano
c726855b1c Merge pull request #29 from Pwnzer0tt1/unix_sock_bind
additional fixes to socket binding
2025-10-01 15:40:02 +02:00
Domingo Dirutigliano
bb4addf590 Merge pull request #28 from Minei3oat/socket
Allow binding to UNIX domain sockets
2025-10-01 15:39:40 +02:00
Domingo Dirutigliano
f554ac558a additional fixes to socket binding 2025-10-01 15:37:20 +02:00
Domingo Dirutigliano
0492f16cea using empty string instead of None to bind dualstack server 2025-09-29 16:39:27 +02:00
67 changed files with 7071 additions and 5364 deletions

View File

@@ -1,46 +1,46 @@
# This workflow will upload a Python Package using Twine when a release is created # # 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 # # 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. # # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by # # They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support # # separate terms of service, privacy policy, and support
# documentation. # # documentation.
name: Upload Python Package (fgex alias) # name: Upload Python Package (fgex alias)
on: # on:
release: # release:
types: # types:
- published # - published
permissions: # permissions:
contents: read # contents: read
jobs: # jobs:
deploy: # deploy:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- uses: actions/checkout@v4 # - uses: actions/checkout@v4
- name: Set up Python # - name: Set up Python
uses: actions/setup-python@v5 # uses: actions/setup-python@v5
with: # with:
python-version: '3.x' # python-version: '3.x'
- name: Install dependencies # - name: Install dependencies
run: | # run: |
python -m pip install --upgrade pip # python -m pip install --upgrade pip
pip install build # pip install build
- name: Extract tag name # - name: Extract tag name
id: tag # id: tag
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT # run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
- name: Update version in setup.py # - name: Update version in setup.py
run: >- # run: >-
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py; # sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
- name: Build package # - name: Build package
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../ # run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
- name: Publish package # - name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 # uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with: # with:
user: __token__ # user: __token__
password: ${{ secrets.PYPI_API_TOKEN_FGEX }} # password: ${{ secrets.PYPI_API_TOKEN_FGEX }}

View File

@@ -1,47 +1,47 @@
# This workflow will upload a Python Package using Twine when a release is created # # 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 # # 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. # # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by # # They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support # # separate terms of service, privacy policy, and support
# documentation. # # documentation.
name: Upload Python Package # name: Upload Python Package
on: # on:
release: # release:
types: # types:
- published # - published
permissions: # permissions:
contents: read # contents: read
jobs: # jobs:
deploy: # deploy:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- uses: actions/checkout@v4 # - uses: actions/checkout@v4
- name: Set up Python # - name: Set up Python
uses: actions/setup-python@v5 # uses: actions/setup-python@v5
with: # with:
python-version: '3.x' # python-version: '3.x'
- name: Install dependencies # - name: Install dependencies
run: | # run: |
python -m pip install --upgrade pip # python -m pip install --upgrade pip
pip install build # pip install build
- name: Extract tag name # - name: Extract tag name
id: tag # id: tag
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT # run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
- name: Update version in setup.py # - name: Update version in setup.py
run: >- # run: >-
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py; # sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py; # sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
- name: Build package # - name: Build package
run: cd fgex-lib && python -m build && mv ./dist ../ # run: cd fgex-lib && python -m build && mv ./dist ../
- name: Publish package # - name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 # uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with: # with:
user: __token__ # user: __token__
password: ${{ secrets.PYPI_API_TOKEN }} # password: ${{ secrets.PYPI_API_TOKEN }}

View File

@@ -12,24 +12,34 @@ RUN bun i
COPY ./frontend/ . COPY ./frontend/ .
RUN bun run build RUN bun run build
# Base fedora container # Base Ubuntu container
FROM --platform=$TARGETARCH quay.io/fedora/fedora:42 AS base FROM --platform=$TARGETARCH ubuntu:24.04 AS base
RUN dnf -y update && dnf install -y python3.13 libnetfilter_queue \ RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
libnfnetlink libmnl libcap-ng-utils nftables \ libnfnetlink0 libmnl0 libcap-ng-utils libcap2-bin nftables \
vectorscan libtins python3-nftables libpcap && dnf clean all libhyperscan5 python3-nftables libpcap0.8 && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /execute/modules RUN mkdir -p /execute/modules
WORKDIR /execute WORKDIR /execute
FROM --platform=$TARGETARCH base AS compiler FROM --platform=$TARGETARCH base AS compiler
RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \ RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel \ libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
vectorscan-devel libtins-devel libpcap-devel boost-devel libhyperscan-dev libpcap-dev libboost-dev pkg-config wget cmake && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Build libtins from source as it's not available in Ubuntu 24.04
RUN wget https://github.com/mfontanini/libtins/archive/v4.5.tar.gz && \
tar -xzf v4.5.tar.gz && cd libtins-4.5 && \
mkdir build && cd build && \
cmake ../ -DLIBTINS_ENABLE_CXX11=1 && \
make && make install && ldconfig && \
cd ../.. && rm -rf libtins-4.5 v4.5.tar.gz
COPY ./backend/binsrc /execute/binsrc COPY ./backend/binsrc /execute/binsrc
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.13 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3) RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
#Building main conteiner #Building main conteiner
FROM --platform=$TARGETARCH base AS final FROM --platform=$TARGETARCH base AS final
@@ -37,13 +47,17 @@ FROM --platform=$TARGETARCH base AS final
COPY ./backend/requirements.txt /execute/requirements.txt COPY ./backend/requirements.txt /execute/requirements.txt
COPY ./fgex-lib /execute/fgex-lib COPY ./fgex-lib /execute/fgex-lib
RUN dnf -y update && dnf install -y gcc-c++ python3.13-devel uv git &&\ RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
uv pip install --no-cache --system ./fgex-lib &&\ pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
uv pip install --no-cache --system -r /execute/requirements.txt &&\ pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
uv cache clean && dnf remove -y gcc-c++ python3.13-devel uv git && dnf clean all apt-get remove -y g++ python3-dev git && \
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
COPY ./backend/ /execute/ COPY ./backend/ /execute/
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/ COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
COPY --from=compiler /usr/local/lib/libtins* /usr/local/lib/
COPY --from=frontend /app/dist/ ./frontend/ COPY --from=frontend /app/dist/ ./frontend/
RUN ldconfig
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"] CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]

View File

@@ -68,6 +68,37 @@ All the configuration at the startup is customizable in [firegex.py](./run.py) o
- Create basic firewall rules to allow and deny specific traffic, like ufw or iptables but using firegex graphic interface (by using [nftable](https://netfilter.org/projects/nftables/)) - Create basic firewall rules to allow and deny specific traffic, like ufw or iptables but using firegex graphic interface (by using [nftable](https://netfilter.org/projects/nftables/))
- Port Hijacking allows you to redirect the traffic on a specific port to another port. Thanks to this you can start your own proxy, connecting to the real service using the loopback interface. Firegex will be resposable about the routing of the packets using internally [nftables](https://netfilter.org/projects/nftables/) - Port Hijacking allows you to redirect the traffic on a specific port to another port. Thanks to this you can start your own proxy, connecting to the real service using the loopback interface. Firegex will be resposable about the routing of the packets using internally [nftables](https://netfilter.org/projects/nftables/)
- EXPERIMENTAL: Netfilter Proxy uses [nfqueue](https://netfilter.org/projects/libnetfilter_queue/) to simulate a python proxy, you can write your own filter in python and use it to filter the traffic. There are built-in some data handler to parse protocols like HTTP, and before apply the filter you can test it with fgex command (you need to install firegex lib from pypi). - EXPERIMENTAL: Netfilter Proxy uses [nfqueue](https://netfilter.org/projects/libnetfilter_queue/) to simulate a python proxy, you can write your own filter in python and use it to filter the traffic. There are built-in some data handler to parse protocols like HTTP, and before apply the filter you can test it with fgex command (you need to install firegex lib from pypi).
- Traffic Viewer allows you to monitor live network traffic for all services in real-time
- Setup Import/Export allows you to backup and restore your entire Firegex configuration as a JSON file, making it easy to deploy identical configurations across multiple servers
## Configuration Management
Firegex supports importing and exporting configurations via JSON files. This is useful for:
- Backing up your configuration
- Deploying the same setup across multiple servers
- Version controlling your firewall rules
- Quick disaster recovery
### Using the Web Interface
Navigate to "Setup Import/Export" in the sidebar to:
- **Export**: Download your current configuration as JSON
- **Import from File**: Upload a setup.json file
- **Import from JSON**: Paste JSON directly into the interface
### Using the API
```bash
# Export configuration
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:4444/api/setup/export > setup.json
# Import configuration
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d @setup.json \
http://localhost:4444/api/setup/import
```
See [setup.example.json](setup.example.json) for the configuration file format.
## Documentation ## Documentation

View File

@@ -227,7 +227,7 @@ if __name__ == '__main__':
uvicorn.run( uvicorn.run(
"app:app", "app:app",
# None allows to bind also on ipv6, and is selected if FIREGEX_HOST is any # None allows to bind also on ipv6, and is selected if FIREGEX_HOST is any
host=None if FIREGEX_HOST == "any" else FIREGEX_HOST, host="" if FIREGEX_HOST == "any" else FIREGEX_HOST,
port=FIREGEX_PORT, port=FIREGEX_PORT,
uds=FIREGEX_SOCKET, uds=FIREGEX_SOCKET,
reload=DEBUG and not NORELOAD, reload=DEBUG and not NORELOAD,

View File

@@ -2,6 +2,13 @@
chown nobody -R /execute/ chown nobody -R /execute/
# Create socket directory if SOCKET_DIR is set
if [ -n "$SOCKET_DIR" ]; then
mkdir -p "$SOCKET_DIR"
chown nobody:nobody "$SOCKET_DIR"
chmod 755 "$SOCKET_DIR"
fi
echo "[*] Attempting to start with capabilities..." echo "[*] Attempting to start with capabilities..."
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \ if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \

View File

@@ -5,6 +5,7 @@ import asyncio
import traceback import traceback
from fastapi import HTTPException from fastapi import HTTPException
import time import time
import json
from utils import run_func from utils import run_func
from utils import DEBUG from utils import DEBUG
from utils import nicenessify from utils import nicenessify
@@ -35,11 +36,12 @@ class FiregexInterceptor:
self.last_time_exception = 0 self.last_time_exception = 0
self.outstrem_function = None self.outstrem_function = None
self.expection_function = None self.expection_function = None
self.traffic_function = None
self.outstrem_task: asyncio.Task self.outstrem_task: asyncio.Task
self.outstrem_buffer = "" self.outstrem_buffer = ""
@classmethod @classmethod
async def start(cls, srv: Service, outstream_func=None, exception_func=None): async def start(cls, srv: Service, outstream_func=None, exception_func=None, traffic_func=None):
self = cls() self = cls()
self.srv = srv self.srv = srv
self.filter_map_lock = asyncio.Lock() self.filter_map_lock = asyncio.Lock()
@@ -47,6 +49,7 @@ class FiregexInterceptor:
self.sock_conn_lock = asyncio.Lock() self.sock_conn_lock = asyncio.Lock()
self.outstrem_function = outstream_func self.outstrem_function = outstream_func
self.expection_function = exception_func self.expection_function = exception_func
self.traffic_function = traffic_func
if not self.sock_conn_lock.locked(): if not self.sock_conn_lock.locked():
await self.sock_conn_lock.acquire() await self.sock_conn_lock.acquire()
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock" self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
@@ -83,9 +86,21 @@ class FiregexInterceptor:
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n" self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
if self.outstrem_function: if self.outstrem_function:
await run_func(self.outstrem_function, self.srv.id, out_data) await run_func(self.outstrem_function, self.srv.id, out_data)
# Parse JSON traffic events (if binary emits them)
if self.traffic_function:
for line in out_data.splitlines():
if line.startswith("{"): # JSON event from binary
try:
event = json.loads(line)
if "ts" in event and "verdict" in event: # Basic validation
await run_func(self.traffic_function, self.srv.id, event)
except (json.JSONDecodeError, KeyError):
pass # Ignore malformed JSON, keep backward compat with raw logs
async def _start_binary(self): async def _start_binary(self):
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy")) proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
# Determine match mode based on protocol
match_mode = "stream" if self.srv.proto in ["tcp", "http"] else "block"
self.process = await asyncio.create_subprocess_exec( self.process = await asyncio.create_subprocess_exec(
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL, proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
@@ -93,7 +108,9 @@ class FiregexInterceptor:
env={ env={
"NTHREADS": os.getenv("NTHREADS","1"), "NTHREADS": os.getenv("NTHREADS","1"),
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0", "FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
"FIREGEX_NFPROXY_SOCK": self.sock_path "FIREGEX_NFPROXY_SOCK": self.sock_path,
"MATCH_MODE": match_mode,
"PROTOCOL": self.srv.proto
}, },
) )
nicenessify(-10, self.process.pid) nicenessify(-10, self.process.pid)

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
from collections import deque
from modules.nfproxy.firegex import FiregexInterceptor from modules.nfproxy.firegex import FiregexInterceptor
from modules.nfproxy.nftables import FiregexTables, FiregexFilter from modules.nfproxy.nftables import FiregexTables, FiregexFilter
from modules.nfproxy.models import Service, PyFilter from modules.nfproxy.models import Service, PyFilter
@@ -12,7 +13,7 @@ class STATUS:
nft = FiregexTables() nft = FiregexTables()
class ServiceManager: class ServiceManager:
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None): def __init__(self, srv: Service, db, outstream_func=None, exception_func=None, traffic_func=None):
self.srv = srv self.srv = srv
self.db = db self.db = db
self.status = STATUS.STOP self.status = STATUS.STOP
@@ -21,11 +22,17 @@ class ServiceManager:
self.interceptor = None self.interceptor = None
self.outstream_function = outstream_func self.outstream_function = outstream_func
self.last_exception_time = 0 self.last_exception_time = 0
self.traffic_events = deque(maxlen=500) # Ring buffer for traffic viewer
async def excep_internal_handler(srv, exc_time): async def excep_internal_handler(srv, exc_time):
self.last_exception_time = exc_time self.last_exception_time = exc_time
if exception_func: if exception_func:
await run_func(exception_func, srv, exc_time) await run_func(exception_func, srv, exc_time)
self.exception_function = excep_internal_handler self.exception_function = excep_internal_handler
async def traffic_internal_handler(srv, event):
self.traffic_events.append(event)
if traffic_func:
await run_func(traffic_func, srv, event)
self.traffic_function = traffic_internal_handler
async def _update_filters_from_db(self): async def _update_filters_from_db(self):
pyfilters = [ pyfilters = [
@@ -69,7 +76,7 @@ class ServiceManager:
async def start(self): async def start(self):
if not self.interceptor: if not self.interceptor:
nft.delete(self.srv) nft.delete(self.srv)
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function) self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
await self._update_filters_from_db() await self._update_filters_from_db()
self._set_status(STATUS.ACTIVE) self._set_status(STATUS.ACTIVE)
@@ -88,13 +95,23 @@ class ServiceManager:
async with self.lock: async with self.lock:
await self._update_filters_from_db() await self._update_filters_from_db()
def get_traffic_events(self, limit: int = 500):
"""Return recent traffic events from ring buffer"""
events_list = list(self.traffic_events)
return events_list[-limit:] if limit < len(events_list) else events_list
def clear_traffic_events(self):
"""Clear traffic event history"""
self.traffic_events.clear()
class FirewallManager: class FirewallManager:
def __init__(self, db:SQLite, outstream_func=None, exception_func=None): def __init__(self, db:SQLite, outstream_func=None, exception_func=None, traffic_func=None):
self.db = db self.db = db
self.service_table: dict[str, ServiceManager] = {} self.service_table: dict[str, ServiceManager] = {}
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self.outstream_function = outstream_func self.outstream_function = outstream_func
self.exception_function = exception_func self.exception_function = exception_func
self.traffic_function = traffic_func
async def close(self): async def close(self):
for key in list(self.service_table.keys()): for key in list(self.service_table.keys()):
@@ -116,7 +133,7 @@ class FirewallManager:
srv = Service.from_dict(srv) srv = Service.from_dict(srv)
if srv.id in self.service_table: if srv.id in self.service_table:
continue continue
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function) self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
await self.service_table[srv.id].next(srv.status) await self.service_table[srv.id].next(srv.status)
def get(self,srv_id) -> ServiceManager: def get(self,srv_id) -> ServiceManager:

View File

@@ -6,6 +6,8 @@ def convert_protocol_to_l4(proto:str):
return "tcp" return "tcp"
elif proto == "http": elif proto == "http":
return "tcp" return "tcp"
elif proto == "udp":
return "udp"
else: else:
raise Exception("Invalid protocol") raise Exception("Invalid protocol")

View File

@@ -4,5 +4,6 @@ uvicorn[standard]
psutil psutil
python-jose[cryptography] python-jose[cryptography]
python-socketio python-socketio
git+https://github.com/google/brotli.git@35d4992ac8eb1eca3b6c5f220e76cfc8b7e470aa brotli
zstandard
#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

@@ -65,7 +65,7 @@ db = SQLite('db/nft-pyfilters.db', {
'status': 'VARCHAR(100) NOT NULL', 'status': 'VARCHAR(100) NOT NULL',
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)', 'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
'name': 'VARCHAR(100) NOT NULL UNIQUE', 'name': 'VARCHAR(100) NOT NULL UNIQUE',
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))', 'proto': 'VARCHAR(4) NOT NULL CHECK (proto IN ("tcp", "http", "udp"))',
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))', 'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_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', 'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
@@ -113,6 +113,8 @@ async def startup():
utils.socketio.on("nfproxy-outstream-leave", leave_outstream) utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
utils.socketio.on("nfproxy-exception-join", join_exception) utils.socketio.on("nfproxy-exception-join", join_exception)
utils.socketio.on("nfproxy-exception-leave", leave_exception) utils.socketio.on("nfproxy-exception-leave", leave_exception)
utils.socketio.on("nfproxy-traffic-join", join_traffic)
utils.socketio.on("nfproxy-traffic-leave", leave_traffic)
async def shutdown(): async def shutdown():
db.backup() db.backup()
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
async def exception_func(service_id, timestamp): async def exception_func(service_id, timestamp):
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}") await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func) async def traffic_func(service_id, event):
await utils.socketio.emit(f"nfproxy-traffic-{service_id}", event, room=f"nfproxy-traffic-{service_id}")
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func, traffic_func=traffic_func)
@app.get('/services', response_model=list[ServiceModel]) @app.get('/services', response_model=list[ServiceModel])
async def get_service_list(): async def get_service_list():
@@ -300,7 +305,7 @@ async def add_new_service(form: ServiceAddForm):
form.ip_int = ip_parse(form.ip_int) form.ip_int = ip_parse(form.ip_int)
except ValueError: except ValueError:
raise HTTPException(status_code=400, detail="Invalid address") raise HTTPException(status_code=400, detail="Invalid address")
if form.proto not in ["tcp", "http"]: if form.proto not in ["tcp", "http", "udp"]:
raise HTTPException(status_code=400, detail="Invalid protocol") raise HTTPException(status_code=400, detail="Invalid protocol")
srv_id = None srv_id = None
try: try:
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
except FileNotFoundError: except FileNotFoundError:
return "" return ""
@app.get('/services/{service_id}/traffic')
async def get_traffic_events(service_id: str, limit: int = 500):
"""Get recent traffic events from the service ring buffer"""
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
raise HTTPException(status_code=400, detail="This service does not exists!")
try:
events = firewall.get(service_id).get_traffic_events(limit)
return {"events": events, "count": len(events)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post('/services/{service_id}/traffic/clear', response_model=StatusMessageModel)
async def clear_traffic_events(service_id: str):
"""Clear traffic event history for a service"""
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
raise HTTPException(status_code=400, detail="This service does not exists!")
try:
firewall.get(service_id).clear_traffic_events()
return {"status": "ok"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
#Socket io events #Socket io events
async def join_outstream(sid, data): async def join_outstream(sid, data):
"""Client joins a room.""" """Client joins a room."""
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
if srv: if srv:
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}") await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
async def join_traffic(sid, data):
"""Client joins traffic viewer room and gets initial event history."""
srv = data.get("service")
if srv:
room = f"nfproxy-traffic-{srv}"
await utils.socketio.enter_room(sid, room)
try:
events = firewall.get(srv).get_traffic_events(500)
await utils.socketio.emit("nfproxy-traffic-history", {"events": events}, room=sid)
except Exception:
pass # Service may not exist or not started
async def leave_traffic(sid, data):
"""Client leaves traffic viewer room."""
srv = data.get("service")
if srv:
await utils.socketio.leave_room(sid, f"nfproxy-traffic-{srv}")

223
backend/routers/setup.py Normal file
View File

@@ -0,0 +1,223 @@
from fastapi import APIRouter, HTTPException, UploadFile, File
from pydantic import BaseModel
import json
from typing import List, Optional
from utils.models import StatusMessageModel
from routers import nfproxy, nfregex, porthijack, firewall
class ServiceConfig(BaseModel):
name: str
port: int
proto: str
ip_int: str
fail_open: bool = True
class PortHijackServiceConfig(BaseModel):
name: str
public_port: int
proxy_port: int
proto: str
ip_src: str
ip_dst: str
class FirewallRuleConfig(BaseModel):
mode: str
src: str
dst: str
in_int: str
out_int: str
proto: str
sport: str
dport: str
class SetupConfig(BaseModel):
services: Optional[List[ServiceConfig]] = []
porthijack: Optional[List[PortHijackServiceConfig]] = []
firewall: Optional[List[FirewallRuleConfig]] = []
class SetupResponse(BaseModel):
status: str
services_created: int = 0
porthijack_created: int = 0
firewall_created: int = 0
errors: List[str] = []
app = APIRouter()
@app.post("/import", response_model=SetupResponse)
async def import_setup(config: SetupConfig):
"""
Import services and rules from a setup configuration.
Creates basic services without filters or regex rules.
"""
errors = []
services_count = 0
porthijack_count = 0
firewall_count = 0
# Import Services
if config.services:
for service_config in config.services:
try:
# Determine which module to use based on protocol
# HTTP -> NFProxy, TCP/UDP -> can use either (prefer NFProxy)
if service_config.proto in ["tcp", "http", "udp"]:
# Create NFProxy service
try:
add_form = nfproxy.ServiceAddForm(
name=service_config.name,
port=service_config.port,
proto=service_config.proto,
ip_int=service_config.ip_int,
fail_open=service_config.fail_open
)
result = await nfproxy.add_service(add_form)
if result.status == "ok":
services_count += 1
else:
errors.append(f"Service '{service_config.name}': Failed to create")
except Exception as e:
errors.append(f"Service '{service_config.name}': {str(e)}")
else:
errors.append(f"Service '{service_config.name}': Unsupported protocol '{service_config.proto}'")
except Exception as e:
errors.append(f"Service '{service_config.name}': {str(e)}")
# Import PortHijack services
if config.porthijack:
for service_config in config.porthijack:
try:
add_form = porthijack.ServiceAddForm(
name=service_config.name,
public_port=service_config.public_port,
proxy_port=service_config.proxy_port,
proto=service_config.proto,
ip_src=service_config.ip_src,
ip_dst=service_config.ip_dst
)
result = await porthijack.add_service(add_form)
if result.status == "ok":
porthijack_count += 1
else:
errors.append(f"PortHijack service '{service_config.name}': Failed to create")
except Exception as e:
errors.append(f"PortHijack service '{service_config.name}': {str(e)}")
# Import Firewall rules
if config.firewall:
for rule_config in config.firewall:
try:
rule_form = firewall.RuleFormAdd(
mode=rule_config.mode,
src=rule_config.src,
dst=rule_config.dst,
in_int=rule_config.in_int,
out_int=rule_config.out_int,
proto=rule_config.proto,
sport=rule_config.sport,
dport=rule_config.dport
)
await firewall.add_rule(rule_form)
firewall_count += 1
except Exception as e:
errors.append(f"Firewall rule: {str(e)}")
return SetupResponse(
status="ok" if len(errors) == 0 else "partial",
services_created=services_count,
porthijack_created=porthijack_count,
firewall_created=firewall_count,
errors=errors
)
@app.post("/import/file")
async def import_setup_file(file: UploadFile = File(...)):
"""
Import services from an uploaded JSON file.
"""
try:
content = await file.read()
config_dict = json.loads(content.decode('utf-8'))
config = SetupConfig(**config_dict)
return await import_setup(config)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(e)}")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error processing file: {str(e)}")
@app.get("/export")
async def export_setup():
"""
Export all current services and rules as a JSON configuration.
Exports only service definitions without filters or regexes.
"""
config = {
"services": [],
"porthijack": [],
"firewall": []
}
# Export NFProxy services
try:
nfproxy_services = await nfproxy.get_services()
for service in nfproxy_services:
config["services"].append({
"name": service.name,
"port": service.port,
"proto": service.proto,
"ip_int": service.ip_int,
"fail_open": service.fail_open
})
except:
pass
# Export NFRegex services
try:
nfregex_services = await nfregex.get_services()
for service in nfregex_services:
config["services"].append({
"name": service.name,
"port": service.port,
"proto": service.proto,
"ip_int": service.ip_int,
"fail_open": service.fail_open
})
except:
pass
# Export PortHijack services
try:
porthijack_services = await porthijack.get_services()
for service in porthijack_services:
config["porthijack"].append({
"name": service.name,
"public_port": service.public_port,
"proxy_port": service.proxy_port,
"proto": service.proto,
"ip_src": service.ip_src,
"ip_dst": service.ip_dst
})
except:
pass
# Export Firewall rules
try:
fw_rules = await firewall.get_rules()
for rule in fw_rules:
config["firewall"].append({
"mode": rule.mode,
"src": rule.src,
"dst": rule.dst,
"in_int": rule.in_int,
"out_int": rule.out_int,
"proto": rule.proto,
"sport": rule.sport,
"dport": rule.dport
})
except:
pass
return config

116
docs/TRAFFIC_VIEWER.md Normal file
View File

@@ -0,0 +1,116 @@
# Traffic Viewer - JSON Event Format
The traffic viewer is now fully integrated and supports **TCP, HTTP, and UDP** protocols. To enable structured event display, the NFProxy C++ binary (`backend/binsrc/nfproxy.cpp`) should emit JSON lines to stdout with the following format:
## JSON Event Schema
```json
{
"ts": 1701964234567,
"direction": "in",
"src_ip": "192.168.1.100",
"src_port": 54321,
"dst_ip": "10.0.0.5",
"dst_port": 443,
"proto": "tcp",
"size": 1420,
"verdict": "accept",
"filter": "filter_sanitize",
"sample_hex": "474554202f20485454502f312e310d0a486f73743a206578616d706c652e636f6d..."
}
```
## Fields
- `ts` (required): Unix timestamp in milliseconds
- `direction`: `"in"` (client→server) or `"out"` (server→client)
- `src_ip`, `dst_ip`: Source and destination IP addresses
- `src_port`, `dst_port`: Source and destination ports
- `proto`: Protocol name (e.g., `"tcp"`, `"udp"`)
- `size`: Packet/payload size in bytes
- `verdict` (required): `"accept"`, `"drop"`, `"reject"`, or `"edited"`
- `filter`: Name of the Python filter that processed this packet
- `sample_hex`: Hex-encoded sample of payload (first 64-128 bytes recommended)
## Implementation Notes
1. **Backward Compatibility**: The parser in `firegex.py::_stream_handler` only processes lines starting with `{`. Non-JSON output (logs, ACK messages) continues to work as before.
2. **Performance**: Emit JSON only when needed. Consider an env flag:
```cpp
bool emit_traffic_json = getenv("FIREGEX_TRAFFIC_JSON") != nullptr;
if (emit_traffic_json) {
std::cout << json_event << std::endl;
}
```
3. **Sample Code** (C++ with nlohmann/json or similar):
```cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;
void emit_traffic_event(const PacketInfo& pkt, const char* verdict, const char* filter_name) {
json event = {
{"ts", current_timestamp_ms()},
{"direction", pkt.is_inbound ? "in" : "out"},
{"src_ip", pkt.src_addr},
{"src_port", pkt.src_port},
{"dst_ip", pkt.dst_addr},
{"dst_port", pkt.dst_port},
{"proto", pkt.protocol},
{"size", pkt.payload_len},
{"verdict", verdict},
{"filter", filter_name},
{"sample_hex", hex_encode(pkt.payload, std::min(64, pkt.payload_len))}
};
std::cout << event.dump() << std::endl;
}
```
## Testing Without Binary Changes
The viewer works immediately—it will display "No traffic events yet" until the binary is updated. You can manually test the Socket.IO flow by emitting mock events from Python:
```python
# In backend shell or script
import asyncio
import json
from utils import socketio
async def emit_test_event():
event = {
"ts": int(time.time() * 1000),
"direction": "in",
"src_ip": "192.168.1.50",
"src_port": 12345,
"dst_ip": "10.0.0.1",
"dst_port": 80,
"proto": "tcp",
"size": 512,
"verdict": "accept",
"filter": "test_filter"
}
await socketio.emit("nfproxy-traffic-YOUR_SERVICE_ID", event, room="nfproxy-traffic-YOUR_SERVICE_ID")
```
## Current Features
✅ **Backend**:
- Ring buffer stores last 500 events per service
- REST endpoint: `GET /api/nfproxy/services/{id}/traffic?limit=500`
- REST endpoint: `POST /api/nfproxy/services/{id}/traffic/clear`
- Socket.IO channels: `nfproxy-traffic-{service_id}` for live events, `nfproxy-traffic-history` on join
✅ **Frontend**:
- Live table view at `/nfproxy/{service_id}/traffic`
- Client-side text filter (searches IP, verdict, filter name, proto)
- Click row to view full event details + hex payload
- Auto-scroll, clear history button
- Accessible via new button (double-arrow icon) in ServiceDetails page
## Next Steps
1. Update `backend/binsrc/nfproxy.cpp` to emit JSON events as shown above
2. Rebuild the C++ binary
3. Start a service and generate traffic—viewer will populate in real-time
4. Optionally add more filters (by verdict, time range) or export to PCAP

View File

@@ -10,7 +10,7 @@ from firegex.nfproxy.internals.exceptions import (
from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
from dataclasses import dataclass, field from dataclasses import dataclass, field
from collections import deque from collections import deque
from zstd import ZSTD_uncompress import zstandard as zstd
import gzip import gzip
import io import io
import zlib import zlib
@@ -200,7 +200,7 @@ class InternalCallbackHandler:
break break
elif enc == "zstd": elif enc == "zstd":
try: try:
decoding_body = ZSTD_uncompress(decoding_body) decoding_body = zstd.decompress(decoding_body)
except Exception as e: except Exception as e:
print(f"Error decompressing zstd: {e}: skipping", flush=True) print(f"Error decompressing zstd: {e}: skipping", flush=True)
decode_success = False decode_success = False

View File

@@ -2,7 +2,6 @@ typer
pydantic>=2 pydantic>=2
typing-extensions>=4.7.1 typing-extensions>=4.7.1
pycryptodome pycryptodome
zstd
watchfiles watchfiles
fgex fgex
websockets websockets

View File

@@ -13,7 +13,10 @@ import { Firewall } from './pages/Firewall';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import NFProxy from './pages/NFProxy'; import NFProxy from './pages/NFProxy';
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails'; import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
import { useAuth } from './js/store'; import TrafficViewer from './pages/NFProxy/TrafficViewer';
import TrafficViewerMain from './pages/TrafficViewer';
import SetupPage from './pages/Setup';
import { useAuthStore } from './js/store';
function App() { function App() {
@@ -23,7 +26,7 @@ function App() {
const [error, setError] = useState<string|null>() const [error, setError] = useState<string|null>()
const [loadinBtn, setLoadingBtn] = useState(false); const [loadinBtn, setLoadingBtn] = useState(false);
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { isAuthenticated, access_token } = useAuth() const { access_token } = useAuthStore()
useEffect(()=>{ useEffect(()=>{
socketio.auth = { token: access_token || "" } socketio.auth = { token: access_token || "" }
@@ -43,7 +46,7 @@ function App() {
socketio.off("connect_error") socketio.off("connect_error")
socketio.disconnect() socketio.disconnect()
} }
},[isAuthenticated]) },[access_token])
const getStatus = () =>{ const getStatus = () =>{
getstatus().then( res =>{ getstatus().then( res =>{
@@ -92,6 +95,7 @@ function App() {
} }
}).catch( err => setError(err.toString())) }).catch( err => setError(err.toString()))
setLoadingBtn(false) setLoadingBtn(false)
form.reset()
} }
@@ -119,12 +123,14 @@ function App() {
setLoadingBtn(true) setLoadingBtn(true)
await login(values).then(res => { await login(values).then(res => {
if(!res){ if(!res){
queryClient.invalidateQueries()
setSystemStatus({...systemStatus, loggined:true}) setSystemStatus({...systemStatus, loggined:true})
}else{ }else{
setError("Login failed") setError("Login failed")
} }
}).catch( err => setError(err.toString())) }).catch( err => setError(err.toString()))
setLoadingBtn(false) setLoadingBtn(false)
form.reset()
} }
@@ -169,9 +175,12 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
</Route> </Route>
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} > <Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
<Route path=":srv" element={<ServiceDetailsNFProxy />} /> <Route path=":srv" element={<ServiceDetailsNFProxy />} />
<Route path=":srv/traffic" element={<TrafficViewer />} />
</Route> </Route>
<Route path="traffic" element={<TrafficViewerMain />} />
<Route path="firewall" element={<Firewall />} /> <Route path="firewall" element={<Firewall />} />
<Route path="porthijack" element={<PortHijack />} /> <Route path="porthijack" element={<PortHijack />} />
<Route path="setup" element={<SetupPage />} />
<Route path="*" element={<HomeRedirector />} /> <Route path="*" element={<HomeRedirector />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -26,7 +26,7 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
validate:{ validate:{
name: (value) => edit? null : value !== "" ? null : "Service name is required", name: (value) => edit? null : value !== "" ? null : "Service name is required",
port: (value) => (value>0 && value<65536) ? null : "Invalid port", port: (value) => (value>0 && value<65536) ? null : "Invalid port",
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol", proto: (value) => ["tcp","http","udp"].includes(value) ? null : "Invalid protocol",
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
} }
}) })
@@ -115,6 +115,7 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
data={[ data={[
{ label: 'TCP', value: 'tcp' }, { label: 'TCP', value: 'tcp' },
{ label: 'HTTP', value: 'http' }, { label: 'HTTP', value: 'http' },
{ label: 'UDP', value: 'udp' },
]} ]}
{...form.getInputProps('proto')} {...form.getInputProps('proto')}
/>} />}

View File

@@ -72,7 +72,7 @@ export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h
│ * port INTEGER The port of the target to proxy [default: None] [required] │ │ * port INTEGER The port of the target to proxy [default: None] [required] │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --proto [tcp|http] The protocol to proxy [default: tcp] │ --proto [tcp|http|udp] The protocol to proxy [default: tcp] │
│ --from-address TEXT The address of the local server [default: None] │ │ --from-address TEXT The address of the local server [default: None] │
│ --from-port INTEGER The port of the local server [default: 7474] │ │ --from-port INTEGER The port of the local server [default: 7474] │
│ -6 Use IPv6 for the connection │ │ -6 Use IPv6 for the connection │

View File

@@ -94,6 +94,13 @@ export const nfproxy = {
setpyfilterscode: async (service_id:string, code:string) => { setpyfilterscode: async (service_id:string, code:string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse; const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
},
gettraffic: async (service_id:string, limit:number = 500) => {
return await getapi(`nfproxy/services/${service_id}/traffic?limit=${limit}`) as { events: any[], count: number };
},
cleartraffic: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
return status === "ok"?undefined:status
} }
} }

View File

@@ -7,16 +7,17 @@ import { PiWallLight } from "react-icons/pi";
import { useNavbarStore } from "../../js/store"; import { useNavbarStore } from "../../js/store";
import { getMainPath } from "../../js/utils"; import { getMainPath } from "../../js/utils";
import { BsRegex } from "react-icons/bs"; import { BsRegex } from "react-icons/bs";
import { MdVisibility, MdSettings } from "react-icons/md";
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }: function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
{ navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) { { navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
const navigator = useNavigate() const navigator = useNavigate()
return <UnstyledButton return <UnstyledButton
className={`firegex__navbar__unstyled_button${navigate==getMainPath()?" selected":""}${disabled?" disabled":""}`} className={`firegex__navbar__unstyled_button${navigate == getMainPath() ? " selected" : ""}${disabled ? " disabled" : ""}`}
onClick={()=>{ onClick={() => {
if(navigate){navigator(`/${navigate}`);closeNav()} if (navigate) { navigator(`/${navigate}`); closeNav() }
if (onClick) onClick() if (onClick) onClick()
}} disabled={disabled}> }} disabled={disabled}>
<Group> <Group>
<ThemeIcon color={color} variant="light"> <ThemeIcon color={color} variant="light">
{icon} {icon}
@@ -35,16 +36,18 @@ export default function NavBar() {
<Title order={4}>Options </Title> <Title order={4}>Options </Title>
</Box> </Box>
<Divider my="xs" /> <Divider my="xs" />
<Box style={{flexGrow: 1}} component={ScrollArea} px="xs" mt="xs"> <Box style={{ flexGrow: 1 }} component={ScrollArea} px="xs" mt="xs">
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="grape" icon={<BsRegex size={19} />} /> <NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="grape" icon={<BsRegex size={19} />} />
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} /> <NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} /> <NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
<Box px="xs" mt="lg"> <NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
<NavBarButton navigate="setup" closeNav={closeNav} name="Setup Import/Export" color="teal" icon={<MdSettings size={19} />} />
{/* <Box px="xs" mt="lg">
<Title order={5}>Experimental Features 🧪</Title> <Title order={5}>Experimental Features 🧪</Title>
</Box> </Box>
<Text></Text> <Text></Text>
<Divider my="xs" /> <Divider my="xs" /> */}
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
</Box> </Box>
</AppShell.Navbar> </AppShell.Navbar>

View File

@@ -42,21 +42,6 @@ export const useAuthStore = create<AuthState>()(
) )
); );
// Hook personalizzati per un uso più facile nei componenti
export const useAuth = () => {
const { access_token, setAccessToken, clearAccessToken, getAccessToken } = useAuthStore();
const isAuthenticated = !!access_token;
return {
access_token,
isAuthenticated,
setAccessToken,
clearAccessToken,
getAccessToken,
};
};
interface SessionState { interface SessionState {
home_section: string | null; home_section: string | null;
setHomeSection: (section: string | null) => void; setHomeSection: (section: string | null) => void;

View File

@@ -151,6 +151,12 @@ export default function ServiceDetailsNFProxy() {
<FiFileText size="20px" /> <FiFileText size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs"/>
<Tooltip label="Traffic viewer" zIndex={0} color="grape">
<ActionIcon color="grape" size="lg" radius="md" onClick={()=>navigate(`/nfproxy/${srv}/traffic`)} variant="filled">
<MdDoubleArrow size="20px" />
</ActionIcon>
</Tooltip>
</Box> </Box>
</Box> </Box>
{isMedium?null:<Space h="md" />} {isMedium?null:<Space h="md" />}

View File

@@ -0,0 +1,306 @@
import { ActionIcon, Badge, Box, Code, Divider, Grid, Group, LoadingOverlay, Modal, ScrollArea, Select, Space, Table, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router';
import { useEffect, useState } from 'react';
import { nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { errorNotify, isMediumScreen, socketio } from '../../js/utils';
import { FaArrowLeft, FaFilter, FaTrash } from 'react-icons/fa';
import { MdDoubleArrow } from "react-icons/md";
import { useListState } from '@mantine/hooks';
type TrafficEvent = {
ts: number;
direction?: string;
src_ip?: string;
src_port?: number;
dst_ip?: string;
dst_port?: number;
proto?: string;
size?: number;
verdict?: string;
filter?: string;
sample_hex?: string;
};
export default function TrafficViewer() {
const { srv } = useParams();
const services = nfproxyServiceQuery();
const serviceInfo = services.data?.find((s: any) => s.service_id === srv);
const navigate = useNavigate();
const isMedium = isMediumScreen();
const [events, eventsHandlers] = useListState<TrafficEvent>([]);
const [loading, setLoading] = useState(true);
const [filterText, setFilterText] = useState('');
const [filterDirection, setFilterDirection] = useState<string | null>(null);
const [filterProto, setFilterProto] = useState<string | null>(null);
const [filterVerdict, setFilterVerdict] = useState<string | null>(null);
const [selectedEvent, setSelectedEvent] = useState<TrafficEvent | null>(null);
const [modalOpened, setModalOpened] = useState(false);
useEffect(() => {
if (!srv) return;
// Fetch historical events
const fetchHistory = async () => {
try {
const response = await nfproxy.gettraffic(srv, 500);
if (response.events) {
eventsHandlers.setState(response.events);
}
} catch (err) {
console.error('Failed to fetch traffic history:', err);
} finally {
setLoading(false);
}
};
fetchHistory();
// Join Socket.IO room
socketio.emit("nfproxy-traffic-join", { service: srv });
// Listen for historical events on initial join
socketio.on("nfproxy-traffic-history", (data: { events: TrafficEvent[] }) => {
if (data.events && data.events.length > 0) {
eventsHandlers.setState(data.events);
}
});
// Listen for live events
socketio.on(`nfproxy-traffic-${srv}`, (event: TrafficEvent) => {
eventsHandlers.append(event);
});
return () => {
socketio.emit("nfproxy-traffic-leave", { service: srv });
socketio.off(`nfproxy-traffic-${srv}`);
socketio.off("nfproxy-traffic-history");
};
}, [srv]);
if (services.isLoading) return <LoadingOverlay visible={true} />;
if (!srv || !serviceInfo) return <Navigate to="/" replace />;
const clearEvents = async () => {
try {
await nfproxy.cleartraffic(srv);
eventsHandlers.setState([]);
} catch (err) {
errorNotify("Failed to clear traffic events", String(err));
}
};
const filteredEvents = events.filter((e: TrafficEvent) => {
// Text filter
if (filterText) {
const search = filterText.toLowerCase();
const matchesText = (
e.src_ip?.toLowerCase().includes(search) ||
e.dst_ip?.toLowerCase().includes(search) ||
e.verdict?.toLowerCase().includes(search) ||
e.filter?.toLowerCase().includes(search) ||
e.proto?.toLowerCase().includes(search) ||
e.src_port?.toString().includes(search) ||
e.dst_port?.toString().includes(search)
);
if (!matchesText) return false;
}
// Direction filter
if (filterDirection && e.direction !== filterDirection) {
return false;
}
// Protocol filter
if (filterProto && e.proto !== filterProto) {
return false;
}
// Verdict filter
if (filterVerdict && e.verdict !== filterVerdict) {
return false;
}
return true;
});
const formatTimestamp = (ts: number) => {
const date = new Date(ts);
return date.toLocaleTimeString() + '.' + date.getMilliseconds().toString().padStart(3, '0');
};
const getVerdictColor = (verdict?: string) => {
switch (verdict?.toLowerCase()) {
case 'accept': return 'teal';
case 'drop': return 'red';
case 'reject': return 'orange';
case 'edited': return 'yellow';
default: return 'gray';
}
};
const openDetails = (event: TrafficEvent) => {
setSelectedEvent(event);
setModalOpened(true);
};
return <>
<LoadingOverlay visible={loading} />
<Box className={isMedium ? 'center-flex' : 'center-flex-row'} style={{ justifyContent: "space-between" }} px="md" mt="lg">
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />Traffic Viewer - {serviceInfo.name}
</Box>
</Title>
<Box className='center-flex'>
<Badge color="cyan" radius="md" size="xl" variant="filled" mr="sm">
{filteredEvents.length} events
</Badge>
<Tooltip label="Clear events" color="red">
<ActionIcon color="red" size="lg" radius="md" onClick={clearEvents} variant="filled">
<FaTrash size="18px" />
</ActionIcon>
</Tooltip>
<Space w="md" />
<Tooltip label="Go back" color="cyan">
<ActionIcon color="cyan" onClick={() => navigate(-1)} size="lg" radius="md" variant="filled">
<FaArrowLeft size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="md" />
<Box px="md">
<Grid mb="md">
<Grid.Col span={{ base: 12, md: 6 }}>
<TextInput
placeholder="Search by IP, port, verdict, filter name, or protocol..."
value={filterText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
leftSection={<FaFilter />}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Select
placeholder="Direction"
clearable
value={filterDirection}
onChange={setFilterDirection}
data={[
{ value: 'in', label: 'Incoming' },
{ value: 'out', label: 'Outgoing' }
]}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Select
placeholder="Protocol"
clearable
value={filterProto}
onChange={setFilterProto}
data={[
{ value: 'tcp', label: 'TCP' },
{ value: 'udp', label: 'UDP' },
{ value: 'http', label: 'HTTP' }
]}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Select
placeholder="Verdict"
clearable
value={filterVerdict}
onChange={setFilterVerdict}
data={[
{ value: 'accept', label: 'Accept' },
{ value: 'drop', label: 'Drop' },
{ value: 'reject', label: 'Reject' },
{ value: 'edited', label: 'Edited' }
]}
/>
</Grid.Col>
</Grid>
<ScrollArea style={{ height: 'calc(100vh - 280px)' }}>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Time</Table.Th>
<Table.Th>Direction</Table.Th>
<Table.Th>Source</Table.Th>
<Table.Th>Destination</Table.Th>
<Table.Th>Protocol</Table.Th>
<Table.Th>Size</Table.Th>
<Table.Th>Filter</Table.Th>
<Table.Th>Verdict</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filteredEvents.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={8} style={{ textAlign: 'center', padding: '2rem' }}>
<Text c="dimmed">
{filterText ? 'No events match your filter' : 'No traffic events yet. Waiting for traffic...'}
</Text>
</Table.Td>
</Table.Tr>
) : (
filteredEvents.slice(-500).reverse().map((event: TrafficEvent, idx: number) => (
<Table.Tr key={idx} onClick={() => openDetails(event)} style={{ cursor: 'pointer' }}>
<Table.Td><Code>{formatTimestamp(event.ts)}</Code></Table.Td>
<Table.Td>
<Badge size="sm" variant="dot" color={event.direction === 'in' ? 'blue' : 'grape'}>
{event.direction || 'unknown'}
</Badge>
</Table.Td>
<Table.Td>{event.src_ip || '-'}:{event.src_port || '-'}</Table.Td>
<Table.Td>{event.dst_ip || '-'}:{event.dst_port || '-'}</Table.Td>
<Table.Td><Badge size="sm" color="violet">{event.proto || 'unknown'}</Badge></Table.Td>
<Table.Td>{event.size ? `${event.size} B` : '-'}</Table.Td>
<Table.Td><Code>{event.filter || '-'}</Code></Table.Td>
<Table.Td>
<Badge color={getVerdictColor(event.verdict)} size="sm">
{event.verdict || 'unknown'}
</Badge>
</Table.Td>
</Table.Tr>
))
)}
</Table.Tbody>
</Table>
</ScrollArea>
</Box>
{/* Payload details modal */}
<Modal
opened={modalOpened}
onClose={() => setModalOpened(false)}
title="Event Details"
size="xl"
>
{selectedEvent && (
<Box>
<Grid>
<Grid.Col span={6}><strong>Timestamp:</strong> {formatTimestamp(selectedEvent.ts)}</Grid.Col>
<Grid.Col span={6}><strong>Direction:</strong> {selectedEvent.direction || 'unknown'}</Grid.Col>
<Grid.Col span={6}><strong>Source:</strong> {selectedEvent.src_ip}:{selectedEvent.src_port}</Grid.Col>
<Grid.Col span={6}><strong>Destination:</strong> {selectedEvent.dst_ip}:{selectedEvent.dst_port}</Grid.Col>
<Grid.Col span={6}><strong>Protocol:</strong> {selectedEvent.proto || 'unknown'}</Grid.Col>
<Grid.Col span={6}><strong>Size:</strong> {selectedEvent.size ? `${selectedEvent.size} B` : '-'}</Grid.Col>
<Grid.Col span={6}><strong>Filter:</strong> {selectedEvent.filter || '-'}</Grid.Col>
<Grid.Col span={6}><strong>Verdict:</strong> {selectedEvent.verdict || 'unknown'}</Grid.Col>
</Grid>
{selectedEvent.sample_hex && (
<>
<Divider my="md" label="Payload Sample (Hex)" />
<ScrollArea style={{ maxHeight: '300px' }}>
<Code block>{selectedEvent.sample_hex}</Code>
</ScrollArea>
</>
)}
</Box>
)}
</Modal>
</>;
}

View File

@@ -0,0 +1,278 @@
import { Box, Button, Code, Divider, FileButton, Group, List, Paper, Space, Stack, Text, Textarea, ThemeIcon, Title } from '@mantine/core';
import { useState } from 'react';
import { FaCheck, FaDownload, FaExclamationTriangle, FaTimes, FaUpload } from 'react-icons/fa';
import { MdSettings } from 'react-icons/md';
import { getapi, isMediumScreen, postapi } from '../../js/utils';
import { errorNotify, okNotify } from '../../js/utils';
export default function SetupPage() {
const [file, setFile] = useState<File | null>(null);
const [importing, setImporting] = useState(false);
const [exporting, setExporting] = useState(false);
const [importResult, setImportResult] = useState<any>(null);
const [configJson, setConfigJson] = useState('');
const isMedium = isMediumScreen();
const handleExport = async () => {
setExporting(true);
try {
const response = await getapi('/setup/export');
const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `firegex-setup-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
okNotify('Configuration exported successfully', '');
} catch (err) {
errorNotify('Failed to export configuration', String(err));
} finally {
setExporting(false);
}
};
const handleImportFile = async () => {
if (!file) {
errorNotify('Please select a file first', '');
return;
}
setImporting(true);
setImportResult(null);
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/setup/import/file', {
method: 'POST',
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Import failed');
}
const result = await response.json();
setImportResult(result);
if (result.status === 'ok') {
okNotify('Configuration imported successfully', '');
} else {
errorNotify('Configuration imported with errors', 'Check the results below');
}
} catch (err) {
errorNotify('Failed to import configuration', String(err));
} finally {
setImporting(false);
}
};
const handleImportJson = async () => {
if (!configJson.trim()) {
errorNotify('Please enter a JSON configuration', '');
return;
}
setImporting(true);
setImportResult(null);
try {
const config = JSON.parse(configJson);
const result = await postapi('/setup/import', config);
setImportResult(result);
if (result.status === 'ok') {
okNotify('Configuration imported successfully', '');
} else {
errorNotify('Configuration imported with errors', 'Check the results below');
}
} catch (err) {
if (err instanceof SyntaxError) {
errorNotify('Invalid JSON format', String(err));
} else {
errorNotify('Failed to import configuration', String(err));
}
} finally {
setImporting(false);
}
};
return (
<Box px="md" mt="lg">
<Title order={1} className="center-flex">
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
<MdSettings size={24} />
</ThemeIcon>
<Space w="sm" />
Setup Import/Export
</Title>
<Text c="dimmed" mt="sm">
Import or export your Firegex configuration including all services and rules
</Text>
<Divider my="lg" />
<Stack gap="xl">
{/* Export Section */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Export Configuration</Title>
<Text c="dimmed" mb="md">
Download all current services and rules as a JSON file
</Text>
<Button
leftSection={<FaDownload />}
onClick={handleExport}
loading={exporting}
color="teal"
size="md"
>
Export to JSON
</Button>
</Paper>
{/* Import from File Section */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Import from File</Title>
<Text c="dimmed" mb="md">
Upload a setup.json file to create services and rules
</Text>
<Group>
<FileButton onChange={setFile} accept="application/json">
{(props) => (
<Button {...props} variant="outline" color="cyan">
{file ? file.name : 'Select JSON File'}
</Button>
)}
</FileButton>
<Button
leftSection={<FaUpload />}
onClick={handleImportFile}
loading={importing}
disabled={!file}
color="blue"
size="md"
>
Import from File
</Button>
</Group>
</Paper>
{/* Import from JSON Section */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Import from JSON</Title>
<Text c="dimmed" mb="md">
Paste a JSON configuration directly
</Text>
<Textarea
placeholder='{"services": [], "porthijack": [], "firewall": []}'
value={configJson}
onChange={(e) => setConfigJson(e.currentTarget.value)}
minRows={10}
maxRows={20}
mb="md"
styles={{ input: { fontFamily: 'monospace', fontSize: '12px' } }}
/>
<Button
leftSection={<FaUpload />}
onClick={handleImportJson}
loading={importing}
disabled={!configJson.trim()}
color="blue"
size="md"
>
Import from JSON
</Button>
</Paper>
{/* Import Results */}
{importResult && (
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">
<Group>
{importResult.status === 'ok' ? (
<ThemeIcon color="teal" size="lg" radius="xl">
<FaCheck />
</ThemeIcon>
) : (
<ThemeIcon color="yellow" size="lg" radius="xl">
<FaExclamationTriangle />
</ThemeIcon>
)}
Import Results
</Group>
</Title>
<Stack gap="md">
<Group>
<Text fw={500}>Services:</Text>
<Text c={importResult.services_created > 0 ? "teal" : "dimmed"}>
{importResult.services_created} created
</Text>
</Group>
<Group>
<Text fw={500}>PortHijack Services:</Text>
<Text c={importResult.porthijack_created > 0 ? "teal" : "dimmed"}>
{importResult.porthijack_created} created
</Text>
</Group>
<Group>
<Text fw={500}>Firewall Rules:</Text>
<Text c={importResult.firewall_created > 0 ? "teal" : "dimmed"}>
{importResult.firewall_created} created
</Text>
</Group>
{importResult.errors && importResult.errors.length > 0 && (
<>
<Divider />
<Text fw={500} c="red">Errors:</Text>
<List
spacing="xs"
size="sm"
icon={
<ThemeIcon color="red" size={20} radius="xl">
<FaTimes size={12} />
</ThemeIcon>
}
>
{importResult.errors.map((error: string, idx: number) => (
<List.Item key={idx}>
<Code>{error}</Code>
</List.Item>
))}
</List>
</>
)}
</Stack>
</Paper>
)}
{/* Example Configuration */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Example Configuration</Title>
<Text c="dimmed" mb="md">
Here's an example of the JSON structure:
</Text>
<Code block>{`{
"services": [
{
"name": "Example HTTP Service",
"port": 8080,
"proto": "http",
"ip_int": "0.0.0.0",
"fail_open": true
}
],
"porthijack": [],
"firewall": []
}`}</Code>
</Paper>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,307 @@
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Select, Space, Text, TextInput, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useNavigate } from 'react-router';
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
import { isMediumScreen } from '../../js/utils';
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
import { TbPlugConnected } from 'react-icons/tb';
import { FaFilter, FaServer } from 'react-icons/fa';
import { BsRegex } from 'react-icons/bs';
import { useState } from 'react';
type UnifiedService = {
service_id: string;
name: string;
status: string;
port: number;
proto: string;
ip_int: string;
type: 'nfproxy' | 'nfregex';
stats: {
edited_packets?: number;
blocked_packets?: number;
n_packets?: number;
};
};
export default function TrafficViewer() {
const nfproxyServices = nfproxyServiceQuery();
const nfregexServices = nfregexServiceQuery();
const navigate = useNavigate();
const isMedium = isMediumScreen();
const [filterText, setFilterText] = useState('');
const [filterType, setFilterType] = useState<string | null>(null);
const [filterProto, setFilterProto] = useState<string | null>(null);
const [filterStatus, setFilterStatus] = useState<string | null>(null);
if (nfproxyServices.isLoading || nfregexServices.isLoading) {
return <LoadingOverlay visible={true} />;
}
// Combine services from both modules
const allServices: UnifiedService[] = [
...(nfproxyServices.data?.map(s => ({
service_id: s.service_id,
name: s.name,
status: s.status,
port: s.port,
proto: s.proto,
ip_int: s.ip_int,
type: 'nfproxy' as const,
stats: {
edited_packets: s.edited_packets,
blocked_packets: s.blocked_packets
}
})) || []),
...(nfregexServices.data?.map(s => ({
service_id: s.service_id,
name: s.name,
status: s.status,
port: s.port,
proto: s.proto,
ip_int: s.ip_int,
type: 'nfregex' as const,
stats: {
n_packets: s.n_packets
}
})) || [])
];
// Apply filters
const filteredServices = allServices.filter(service => {
// Text filter
if (filterText) {
const search = filterText.toLowerCase();
const matchesText = (
service.name.toLowerCase().includes(search) ||
service.service_id.toLowerCase().includes(search) ||
service.port.toString().includes(search) ||
service.ip_int.toLowerCase().includes(search)
);
if (!matchesText) return false;
}
// Type filter
if (filterType && service.type !== filterType) {
return false;
}
// Protocol filter
if (filterProto && service.proto !== filterProto) {
return false;
}
// Status filter
if (filterStatus) {
if (filterStatus === 'active' && service.status !== 'active') return false;
if (filterStatus === 'stopped' && service.status === 'active') return false;
}
return true;
});
const activeServices = filteredServices.filter(s => s.status === 'active');
const stoppedServices = filteredServices.filter(s => s.status !== 'active');
return <>
<Box px="md" mt="lg">
<Title order={1} className="center-flex">
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
<MdVisibility size={24} />
</ThemeIcon>
<Space w="sm" />
Traffic Viewer
</Title>
<Text c="dimmed" mt="sm">
Monitor live network traffic for NFProxy and NFRegex services
</Text>
</Box>
<Divider my="lg" />
<Box px="md" mb="lg">
<Group grow>
<TextInput
placeholder="Search by name, ID, port, or IP..."
value={filterText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
leftSection={<FaFilter />}
/>
<Select
placeholder="Service Type"
clearable
value={filterType}
onChange={setFilterType}
data={[
{ value: 'nfproxy', label: 'Netfilter Proxy' },
{ value: 'nfregex', label: 'Netfilter Regex' }
]}
/>
<Select
placeholder="Protocol"
clearable
value={filterProto}
onChange={setFilterProto}
data={[
{ value: 'tcp', label: 'TCP' },
{ value: 'udp', label: 'UDP' },
{ value: 'http', label: 'HTTP' }
]}
/>
<Select
placeholder="Status"
clearable
value={filterStatus}
onChange={setFilterStatus}
data={[
{ value: 'active', label: 'Active' },
{ value: 'stopped', label: 'Stopped' }
]}
/>
</Group>
</Box>
{allServices.length === 0 ? (
<Box px="md">
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
No services found
</Title>
<Space h="xs" />
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
Create services in Netfilter Proxy or Netfilter Regex to start monitoring traffic
</Text>
</Box>
) : filteredServices.length === 0 ? (
<Box px="md">
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
No services match your filters
</Title>
<Space h="xs" />
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
Try adjusting your filter criteria
</Text>
</Box>
) : (
<Box px="md">
{activeServices.length > 0 && (
<>
<Title order={3} mb="md">
<Badge color="teal" size="lg" mr="xs">Active</Badge>
Running Services
</Title>
{activeServices.map(service => (
<Card key={`${service.type}-${service.service_id}`} shadow="sm" padding="lg" radius="md" withBorder mb="md">
<Group justify="space-between">
<Box>
<Group>
<ThemeIcon
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
variant="light"
size="lg"
>
{service.type === 'nfproxy' ? (
<TbPlugConnected size={20} />
) : (
<BsRegex size={20} />
)}
</ThemeIcon>
<div>
<Group gap="xs">
<Text fw={700} size="lg">{service.name}</Text>
<Badge
size="xs"
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
variant="dot"
>
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
</Badge>
</Group>
<Group gap="xs" mt={4}>
<Badge color="cyan" size="sm">:{service.port}</Badge>
<Badge color="violet" size="sm">{service.proto}</Badge>
<Badge color="gray" size="sm">{service.ip_int}</Badge>
</Group>
</div>
</Group>
</Box>
<Box>
<Group>
<Box style={{ textAlign: 'right' }}>
{service.type === 'nfproxy' ? (
<>
<Badge color="orange" size="sm" mb={4}>
{service.stats.edited_packets || 0} edited
</Badge>
<br />
<Badge color="yellow" size="sm">
{service.stats.blocked_packets || 0} blocked
</Badge>
</>
) : (
<Badge color="yellow" size="sm">
{service.stats.n_packets || 0} blocked
</Badge>
)}
</Box>
<Tooltip label="View traffic">
<ActionIcon
color="cyan"
size="xl"
radius="md"
variant="filled"
onClick={() => navigate(`/${service.type}/${service.service_id}/traffic`)}
>
<MdDoubleArrow size="24px" />
</ActionIcon>
</Tooltip>
</Group>
</Box>
</Group>
</Card>
))}
<Space h="xl" />
</>
)}
{stoppedServices.length > 0 && (
<>
<Title order={3} mb="md">
<Badge color="red" size="lg" mr="xs">Stopped</Badge>
Inactive Services
</Title>
{stoppedServices.map(service => (
<Card key={service.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md" opacity={0.6}>
<Group justify="space-between">
<Box>
<Group>
<ThemeIcon color={service.type === 'nfproxy' ? 'lime' : 'grape'} variant="light" size="lg">
{service.type === 'nfproxy' ? <TbPlugConnected size={18} /> : <BsRegex size={18} />}
</ThemeIcon>
<div>
<Group gap="xs">
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
<Badge color="gray" size="sm">
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
</Badge>
</Group>
<Group gap="xs" mt={4}>
<Badge color="gray" size="sm">:{service.port}</Badge>
<Badge color="gray" size="sm">{service.proto}</Badge>
</Group>
</div>
</Group>
</Box>
<Box>
<Text size="sm" c="dimmed">
Start service to view traffic
</Text>
</Box>
</Group>
</Card>
))}
</>
)}
</Box>
)}
</>;
}

28
run.py
View File

@@ -255,8 +255,6 @@ def get_web_interface_url():
if args.socket_dir: if args.socket_dir:
return os.path.join(args.socket_dir, "firegex.sock") return os.path.join(args.socket_dir, "firegex.sock")
# Per altre piattaforme, usiamo l'host configurato se non è 0.0.0.0
# altrimenti usiamo localhost per evitare confusione
display_host = "localhost" if args.host == "0.0.0.0" else args.host display_host = "localhost" if args.host == "0.0.0.0" else args.host
return f"http://{display_host}:{args.port}" return f"http://{display_host}:{args.port}"
@@ -270,14 +268,14 @@ 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 f"ghcr.io/pwnzer0tt1/firegex:{args.version}", "build" if g.build else "image": "." if g.build else f"ghcr.io/ilyastar9999/firegex:{args.version}",
"network_mode": "host", "network_mode": "host",
"environment": [ "environment": [
f"PORT={args.port}", f"PORT={args.port}",
f"HOST={args.host}", f"HOST={args.host}",
f"NTHREADS={args.threads}", f"NTHREADS={args.threads}",
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []), *([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []),
*([f"SOCKET_DIR=/run/firegex"] if args.socket_dir else []) *(["SOCKET_DIR=/run/firegex"] if args.socket_dir else [])
], ],
"volumes": [ "volumes": [
"firegex_data:/execute/db", "firegex_data:/execute/db",
@@ -325,7 +323,7 @@ def write_compose(skip_password = True):
"container_name": "firegex", "container_name": "firegex",
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}", "build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
"ports": [ "ports": [
f"{args.host}:{args.port}:{args.port}" f"{'' if args.host == 'any' else args.host+':'}{args.port}:{args.port}"
], ],
"environment": [ "environment": [
f"PORT={args.port}", f"PORT={args.port}",
@@ -600,6 +598,10 @@ def cleanup_standalone_mounts():
f"{g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding" f"{g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
] ]
# Add socket directory mount point if configured
if args.socket_dir:
mount_points.append(f"{g.rootfs_path}/run/firegex")
# Create umount commands (with || true to ignore errors) # Create umount commands (with || true to ignore errors)
umount_commands = [f"umount -l {mount_point} || true" for mount_point in mount_points] umount_commands = [f"umount -l {mount_point} || true" for mount_point in mount_points]
@@ -754,6 +756,18 @@ def setup_standalone_mounts():
f"mount --bind /proc/sys/net/ipv6/conf/all/forwarding {g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding" f"mount --bind /proc/sys/net/ipv6/conf/all/forwarding {g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
]) ])
# Add socket directory bind mount if configured
if args.socket_dir:
# Create socket directory on host if it doesn't exist
# Create mount point in rootfs and bind mount the socket directory
privileged_commands.extend([
f"mkdir -p {args.socket_dir}",
f"chmod 755 {args.socket_dir}",
f"mkdir -p {g.rootfs_path}/run/firegex",
f"chmod 755 {g.rootfs_path}/run/firegex",
f"mount --bind {args.socket_dir} {g.rootfs_path}/run/firegex"
])
# Run all privileged commands in one batch # Run all privileged commands in one batch
if not run_privileged_commands(privileged_commands, "setup bind mounts"): if not run_privileged_commands(privileged_commands, "setup bind mounts"):
puts("Failed to set up bind mounts", color=colors.red) puts("Failed to set up bind mounts", color=colors.red)
@@ -784,9 +798,9 @@ def run_standalone():
if psw_set: if psw_set:
env_vars.append(f"PSW_HASH_SET={hash_psw(psw_set)}") env_vars.append(f"PSW_HASH_SET={hash_psw(psw_set)}")
# Add socket dir if set # Add socket dir if set (use path inside chroot)
if args.socket_dir: if args.socket_dir:
env_vars.append(f"SOCKET_DIR={args.socket_dir}") env_vars.append("SOCKET_DIR=/run/firegex")
# Prepare environment string for chroot # Prepare environment string for chroot
env_string = " ".join([f"{var}" for var in env_vars]) env_string = " ".join([f"{var}" for var in env_vars])

20
setup.example.json Normal file
View File

@@ -0,0 +1,20 @@
{
"services": [
{
"name": "Example HTTP Service",
"port": 8080,
"proto": "http",
"ip_int": "0.0.0.0",
"fail_open": true
},
{
"name": "Example TCP Service",
"port": 443,
"proto": "tcp",
"ip_int": "0.0.0.0",
"fail_open": false
}
],
"porthijack": [],
"firewall": []
}

View File

@@ -4,92 +4,126 @@ from utils.firegexapi import FiregexAPI
import argparse import argparse
import secrets import secrets
parser = argparse.ArgumentParser() if __name__ == "__main__":
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") parser = argparse.ArgumentParser()
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password') parser.add_argument(
args = parser.parse_args() "--address",
sep() "-a",
puts("Testing will start on ", color=colors.cyan, end="") type=str,
puts(f"{args.address}", color=colors.yellow) required=False,
help="Address of firegex backend",
default="http://127.0.0.1:4444/",
)
parser.add_argument(
"--password", "-p", type=str, required=True, help="Firegex password"
)
args = parser.parse_args()
sep()
puts("Testing will start on ", color=colors.cyan, end="")
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:
puts(
"Test Failed: Unknown response or password already put ✗",
color=colors.red,
)
exit(1)
else: else:
puts("Test Failed: Unknown response or password already put ✗", color=colors.red) if firegex.login(args.password):
exit(1) puts("Sucessfully logged in ✔", color=colors.green)
else: else:
if (firegex.login(args.password)): puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
puts("Sucessfully logged in ✔", color=colors.green) exit(1)
if firegex.status()["loggined"]:
puts("Correctly received status ✔", color=colors.green)
else: else:
puts("Test Failed: Unknown response or wrong passowrd ", color=colors.red) puts("Test Failed: Unknown response or not logged in", color=colors.red)
exit(1) exit(1)
if(firegex.status()["loggined"]): # Prepare second instance
puts("Correctly received status ✔", color=colors.green) firegex2 = FiregexAPI(args.address)
else: if firegex2.login(args.password):
puts("Test Failed: Unknown response or not logged in✗", color=colors.red) puts("Sucessfully logged in on second instance ✔", color=colors.green)
exit(1) else:
puts(
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
color=colors.red,
)
exit(1)
#Prepare second instance if firegex2.status()["loggined"]:
firegex2 = FiregexAPI(args.address) puts("Correctly received status on second instance✔", color=colors.green)
if (firegex2.login(args.password)): else:
puts("Sucessfully logged in on second instance ✔", color=colors.green) puts(
else: "Test Failed: Unknown response or not logged in on second instance✗",
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red) color=colors.red,
exit(1) )
exit(1)
if(firegex2.status()["loggined"]): # Change password
puts("Correctly received status on second instance✔", color=colors.green) new_password = secrets.token_hex(10)
else: if firegex.change_password(new_password, expire=True):
puts("Test Failed: Unknown response or not logged in on second instance✗", color=colors.red) puts(f"Sucessfully changed password to {new_password}", color=colors.green)
exit(1) else:
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
exit(1)
#Change password # Check if we are still logged in
new_password = secrets.token_hex(10) if firegex.status()["loggined"]:
if (firegex.change_password(new_password,expire=True)): puts("Correctly received status after password change ✔", color=colors.green)
puts(f"Sucessfully changed password to {new_password}", color=colors.green) else:
else: puts(
puts("Test Failed: Coundl't change the password ✗", color=colors.red) "Test Failed: Unknown response or not logged after password change ✗",
exit(1) color=colors.red,
)
exit(1)
#Check if we are still logged in # Check if second session expired and relog
if(firegex.status()["loggined"]):
puts("Correctly received status after password change ✔", color=colors.green)
else:
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red)
exit(1)
#Check if second session expired and relog if not firegex2.status()["loggined"]:
puts("Second instance was expired currectly ✔", color=colors.green)
else:
puts(
"Test Failed: Still logged in on second instance, expire expected ✗",
color=colors.red,
)
exit(1)
if firegex2.login(new_password):
puts("Sucessfully logged in on second instance ✔", color=colors.green)
else:
puts(
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
color=colors.red,
)
exit(1)
if(not firegex2.status()["loggined"]): # Change it back
puts("Second instance was expired currectly ✔", color=colors.green) if firegex.change_password(args.password, expire=False):
else: puts("Sucessfully restored the password ✔", color=colors.green)
puts("Test Failed: Still logged in on second instance, expire expected ✗", color=colors.red) else:
exit(1) puts("Test Failed: Coundl't change the password ✗", color=colors.red)
if (firegex2.login(new_password)): exit(1)
puts("Sucessfully logged in on second instance ✔", color=colors.green)
else:
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red)
exit(1)
#Change it back # Check if we are still logged in
if (firegex.change_password(args.password,expire=False)): if firegex2.status()["loggined"]:
puts("Sucessfully restored the password ✔", color=colors.green) puts("Correctly received status after password change ", color=colors.green)
else: else:
puts("Test Failed: Coundl't change the password ✗", color=colors.red) puts(
exit(1) "Test Failed: Unknown response or not logged after password change ✗",
color=colors.red,
)
exit(1)
#Check if we are still logged in puts("List of available interfaces:", color=colors.yellow)
if(firegex2.status()["loggined"]): for interface in firegex.get_interfaces():
puts("Correctly received status after password change ✔", color=colors.green) puts(
else: "name: {}, address: {}".format(interface["name"], interface["addr"]),
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red) color=colors.yellow,
exit(1) )
puts("List of available interfaces:", color=colors.yellow)
for interface in firegex.get_interfaces():
puts("name: {}, address: {}".format(interface["name"], interface["addr"]), color=colors.yellow)

File diff suppressed because it is too large Load Diff

View File

@@ -8,247 +8,366 @@ import secrets
import base64 import base64
import time import time
parser = argparse.ArgumentParser() if __name__ == "__main__":
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") parser = argparse.ArgumentParser()
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password') parser.add_argument(
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service") "--address",
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337) "-a",
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False) type=str,
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp") required=False,
help="Address of firegex backend",
default="http://127.0.0.1:4444/",
)
parser.add_argument(
"--password", "-p", type=str, required=True, help="Firegex password"
)
parser.add_argument(
"--service_name",
"-n",
type=str,
required=False,
help="Name of the test service",
default="Test Service",
)
parser.add_argument(
"--port",
"-P",
type=int,
required=False,
help="Port of the test service",
default=1337,
)
parser.add_argument(
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
)
parser.add_argument(
"--proto",
"-m",
type=str,
required=False,
choices=["tcp", "udp"],
help="Select the protocol",
default="tcp",
)
args = parser.parse_args() args = parser.parse_args()
sep() sep()
puts("Testing will start on ", color=colors.cyan, end="") puts("Testing will start on ", color=colors.cyan, end="")
puts(f"{args.address}", color=colors.yellow) puts(f"{args.address}", color=colors.yellow)
firegex = FiregexAPI(args.address) firegex = FiregexAPI(args.address)
#Login # Login
if (firegex.login(args.password)): if firegex.login(args.password):
puts("Sucessfully logged in ✔", color=colors.green) puts("Sucessfully logged in ✔", color=colors.green)
else:
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
exit(1)
#Create server
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port,ipv6=args.ipv6)
def exit_test(code):
if service_id:
server.stop()
if(firegex.nfregex_delete_service(service_id)):
puts("Sucessfully deleted service ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
exit_test(1)
exit(code)
srvs = firegex.nfregex_get_services()
for ele in srvs:
if ele['name'] == args.service_name:
firegex.nfregex_delete_service(ele['service_id'])
service_id = firegex.nfregex_add_service(args.service_name, args.port, args.proto , "::1" if args.ipv6 else "127.0.0.1" )
if service_id:
puts(f"Sucessfully created service {service_id}", color=colors.green)
else:
puts("Test Failed: Failed to create service ✗", color=colors.red)
exit(1)
if(firegex.nfregex_start_service(service_id)):
puts("Sucessfully started service ✔", color=colors.green)
else:
puts("Test Failed: Failed to start service ✗", color=colors.red)
exit_test(1)
server.start()
time.sleep(0.5)
try:
if server.sendCheckData(secrets.token_bytes(432)):
puts("Successfully tested first proxy with no regex ✔", color=colors.green)
else: else:
puts("Test Failed: Data was corrupted ", color=colors.red) puts("Test Failed: Unknown response or wrong passowrd ", color=colors.red)
exit(1)
# Create server
server = (TcpServer if args.proto == "tcp" else UdpServer)(
args.port, ipv6=args.ipv6
)
def exit_test(code):
if service_id:
server.stop()
if firegex.nfregex_delete_service(service_id):
puts("Sucessfully deleted service ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
exit_test(1)
exit(code)
srvs = firegex.nfregex_get_services()
for ele in srvs:
if ele["name"] == args.service_name:
firegex.nfregex_delete_service(ele["service_id"])
service_id = firegex.nfregex_add_service(
args.service_name, args.port, args.proto, "::1" if args.ipv6 else "127.0.0.1"
)
if service_id:
puts(f"Sucessfully created service {service_id}", color=colors.green)
else:
puts("Test Failed: Failed to create service ✗", color=colors.red)
exit(1)
if firegex.nfregex_start_service(service_id):
puts("Sucessfully started service ✔", color=colors.green)
else:
puts("Test Failed: Failed to start service ✗", color=colors.red)
exit_test(1) exit_test(1)
except Exception:
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
exit_test(1)
#Add new regex
secret = bytes(secrets.token_hex(16).encode())
if firegex.nfregex_add_regex(service_id,secret,"B",active=True,is_case_sensitive=True): server.start()
puts(f"Sucessfully added regex {str(secret)}", color=colors.green) time.sleep(0.5)
else: try:
puts(f"Test Failed: Couldn't add the regex {str(secret)}", color=colors.red) if server.sendCheckData(secrets.token_bytes(432)):
exit_test(1) puts("Successfully tested first proxy with no regex ✔", color=colors.green)
else:
puts("Test Failed: Data was corrupted ", color=colors.red)
exit_test(1)
except Exception:
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
exit_test(1)
# Add new regex
secret = bytes(secrets.token_hex(16).encode())
if firegex.nfregex_add_regex(
service_id, secret, "B", active=True, is_case_sensitive=True
):
puts(f"Sucessfully added regex {str(secret)}", color=colors.green)
else:
puts(f"Test Failed: Couldn't add the regex {str(secret)}", color=colors.red)
exit_test(1)
#Check if regex is present in the service # Check if regex is present in the service
n_blocked = 0 n_blocked = 0
def getMetric(metric_name, regex): def getMetric(metric_name, regex):
for metric in firegex.nfregex_get_metrics().split("\n"): for metric in firegex.nfregex_get_metrics().split("\n"):
if metric.startswith(metric_name + "{") and f'regex="{regex}"' in metric: if metric.startswith(metric_name + "{") and f'regex="{regex}"' in metric:
return int(metric.split(" ")[-1]) return int(metric.split(" ")[-1])
def checkRegex(regex, should_work=True, upper=False, deleted=False): def checkRegex(regex, should_work=True, upper=False, deleted=False):
if should_work: if should_work:
global n_blocked
for r in firegex.nfregex_get_service_regexes(service_id):
if r["regex"] == secret:
# Test the regex
s = regex.upper() if upper else regex
if not server.sendCheckData(
secrets.token_bytes(40) + s + secrets.token_bytes(40)
):
puts(
"The malicious request was successfully blocked ✔",
color=colors.green,
)
n_blocked += 1
time.sleep(1)
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
puts(
"The packet was reported as blocked in the API ✔",
color=colors.green,
)
else:
puts(
"Test Failed: The packet wasn't reported as blocked in the API ✗",
color=colors.red,
)
exit_test(1)
if (
getMetric("firegex_blocked_packets", secret.decode())
== n_blocked
):
puts(
"The packet was reported as blocked in the metrics ✔",
color=colors.green,
)
else:
puts(
"Test Failed: The packet wasn't reported as blocked in the metrics ✗",
color=colors.red,
)
exit_test(1)
if getMetric("firegex_active", secret.decode()) == 1:
puts(
"The regex was reported as active in the metrics ✔",
color=colors.green,
)
else:
puts(
"Test Failed: The regex wasn't reported as active in the metrics ✗",
color=colors.red,
)
exit_test(1)
else:
puts(
"Test Failed: The request wasn't blocked ✗",
color=colors.red,
)
exit_test(1)
return
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
exit_test(1)
else:
if server.sendCheckData(
secrets.token_bytes(40)
+ base64.b64decode(regex)
+ secrets.token_bytes(40)
):
puts("The request wasn't blocked ✔", color=colors.green)
else:
puts(
"Test Failed: The request was blocked when it shouldn't have",
color=colors.red,
)
exit_test(1)
if not deleted:
if getMetric("firegex_active", secret.decode()) == 0:
puts(
"The regex was reported as inactive in the metrics ✔",
color=colors.green,
)
else:
puts(
"Test Failed: The regex wasn't reported as inactive in the metrics ✗",
color=colors.red,
)
exit_test(1)
def clear_regexes():
global n_blocked global n_blocked
n_blocked = 0
for r in firegex.nfregex_get_service_regexes(service_id): for r in firegex.nfregex_get_service_regexes(service_id):
if r["regex"] == secret: if r["regex"] == secret:
#Test the regex if firegex.nfregex_delete_regex(r["id"]):
s = regex.upper() if upper else regex puts(
if not server.sendCheckData(secrets.token_bytes(40) + s + secrets.token_bytes(40)): f"Sucessfully deleted regex with id {r['id']}",
puts("The malicious request was successfully blocked ✔", color=colors.green) color=colors.green,
n_blocked += 1 )
time.sleep(1)
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
puts("The packet was reported as blocked in the API ✔", color=colors.green)
else:
puts("Test Failed: The packet wasn't reported as blocked in the API ✗", color=colors.red)
exit_test(1)
if getMetric("firegex_blocked_packets", secret.decode()) == n_blocked:
puts("The packet was reported as blocked in the metrics ✔", color=colors.green)
else:
puts("Test Failed: The packet wasn't reported as blocked in the metrics ✗", color=colors.red)
exit_test(1)
if getMetric("firegex_active", secret.decode()) == 1:
puts("The regex was reported as active in the metrics ✔", color=colors.green)
else:
puts("Test Failed: The regex wasn't reported as active in the metrics ✗", color=colors.red)
exit_test(1)
else: else:
puts("Test Failed: The request wasn't blocked", color=colors.red) puts("Test Failed: Coulnd't delete the regex", color=colors.red)
exit_test(1) exit_test(1)
return break
puts("Test Failed: The regex wasn't found ✗", color=colors.red) if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
exit_test(1) puts("No regex metrics after deletion ✔", color=colors.green)
else:
if server.sendCheckData(secrets.token_bytes(40) + base64.b64decode(regex) + secrets.token_bytes(40)):
puts("The request wasn't blocked ✔", color=colors.green)
else: else:
puts("Test Failed: The request was blocked when it shouldn't have", color=colors.red) puts(
"Test Failed: Metrics found after deleting the regex ✗",
color=colors.red,
)
exit_test(1) exit_test(1)
if not deleted:
if getMetric("firegex_active", secret.decode()) == 0:
puts("The regex was reported as inactive in the metrics ✔", color=colors.green)
else:
puts("Test Failed: The regex wasn't reported as inactive in the metrics ✗", color=colors.red)
exit_test(1)
def clear_regexes(): checkRegex(secret)
global n_blocked
n_blocked = 0 # Pause the proxy
if firegex.nfregex_stop_service(service_id):
puts(f"Sucessfully paused service with id {service_id}", color=colors.green)
else:
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
exit_test(1)
# Check if it's actually paused
checkRegex(secret, should_work=False)
# Start firewall
if firegex.nfregex_start_service(service_id):
puts(f"Sucessfully started service with id {service_id}", color=colors.green)
else:
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
exit_test(1)
checkRegex(secret)
# Disable regex
for r in firegex.nfregex_get_service_regexes(service_id): for r in firegex.nfregex_get_service_regexes(service_id):
if r["regex"] == secret: if r["regex"] == secret:
if(firegex.nfregex_delete_regex(r["id"])): if firegex.nfregex_disable_regex(r["id"]):
puts(f"Sucessfully deleted regex with id {r['id']}", color=colors.green) puts(
f"Sucessfully disabled regex with id {r['id']}",
color=colors.green,
)
else: else:
puts("Test Failed: Coulnd't delete the regex ✗", color=colors.red) puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red)
exit_test(1) exit_test(1)
break break
if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
puts("No regex metrics after deletion ✔", color=colors.green) # Check if it's actually disabled
checkRegex(secret, should_work=False)
# Enable regex
for r in firegex.nfregex_get_service_regexes(service_id):
if r["regex"] == secret:
if firegex.nfregex_enable_regex(r["id"]):
puts(
f"Sucessfully enabled regex with id {r['id']}", color=colors.green
)
else:
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
exit_test(1)
break
checkRegex(secret)
# Delete regex
clear_regexes()
# Check if it's actually deleted
checkRegex(secret, should_work=False, deleted=True)
# Add case insensitive regex
if firegex.nfregex_add_regex(
service_id, secret, "B", active=True, is_case_sensitive=False
):
puts(
f"Sucessfully added case insensitive regex {str(secret)}",
color=colors.green,
)
else: else:
puts("Test Failed: Metrics found after deleting the regex ✗", color=colors.red) puts(
f"Test Failed: Coulnd't add the case insensitive regex {str(secret)}",
color=colors.red,
)
exit_test(1) exit_test(1)
checkRegex(secret) checkRegex(secret, upper=True)
checkRegex(secret)
#Pause the proxy clear_regexes()
if(firegex.nfregex_stop_service(service_id)):
puts(f"Sucessfully paused service with id {service_id}", color=colors.green)
else:
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
exit_test(1)
#Check if it's actually paused # Rename service
checkRegex(secret,should_work=False) if firegex.nfregex_rename_service(service_id, f"{args.service_name}2"):
puts(
#Start firewall f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
if(firegex.nfregex_start_service(service_id)): )
puts(f"Sucessfully started service with id {service_id}", color=colors.green)
else:
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
exit_test(1)
checkRegex(secret)
#Disable regex
for r in firegex.nfregex_get_service_regexes(service_id):
if r["regex"] == secret:
if(firegex.nfregex_disable_regex(r["id"])):
puts(f"Sucessfully disabled regex with id {r['id']}", color=colors.green)
else:
puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red)
exit_test(1)
break
#Check if it's actually disabled
checkRegex(secret,should_work=False)
#Enable regex
for r in firegex.nfregex_get_service_regexes(service_id):
if r["regex"] == secret:
if(firegex.nfregex_enable_regex(r["id"])):
puts(f"Sucessfully enabled regex with id {r['id']}", color=colors.green)
else:
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
exit_test(1)
break
checkRegex(secret)
#Delete regex
clear_regexes()
#Check if it's actually deleted
checkRegex(secret,should_work=False,deleted=True)
#Add case insensitive regex
if(firegex.nfregex_add_regex(service_id,secret,"B",active=True, is_case_sensitive=False)):
puts(f"Sucessfully added case insensitive regex {str(secret)}", color=colors.green)
else:
puts(f"Test Failed: Coulnd't add the case insensitive regex {str(secret)}", color=colors.red)
exit_test(1)
checkRegex(secret, upper=True)
checkRegex(secret)
clear_regexes()
#Rename service
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}2")):
puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
exit_test(1)
#Check if service was renamed correctly
service = firegex.nfregex_get_service(service_id)
if service["name"] == f"{args.service_name}2":
puts("Checked that service was renamed correctly ✔", color=colors.green)
else:
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
exit_test(1)
#Rename back service
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}")):
puts(f"Sucessfully renamed service to {args.service_name}", color=colors.green)
else:
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
exit_test(1)
#Change settings
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
if(firegex.nfregex_settings_service(service_id, 1338, opposite_proto, "::dead:beef" if args.ipv6 else "123.123.123.123", True)):
srv_updated = firegex.nfregex_get_service(service_id)
if srv_updated["port"] == 1338 and srv_updated["proto"] == opposite_proto and ("::dead:beef" if args.ipv6 else "123.123.123.123") in srv_updated["ip_int"] and srv_updated["fail_open"]:
puts("Sucessfully changed service settings ✔", color=colors.green)
else: else:
puts("Test Failed: Service settings weren't updated correctly", color=colors.red) puts("Test Failed: Coulnd't rename service", color=colors.red)
exit_test(1) exit_test(1)
else:
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
exit_test(1)
exit_test(0) # Check if service was renamed correctly
service = firegex.nfregex_get_service(service_id)
if service["name"] == f"{args.service_name}2":
puts("Checked that service was renamed correctly ✔", color=colors.green)
else:
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
exit_test(1)
# Rename back service
if firegex.nfregex_rename_service(service_id, f"{args.service_name}"):
puts(
f"Sucessfully renamed service to {args.service_name}", color=colors.green
)
else:
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
exit_test(1)
# Change settings
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
if firegex.nfregex_settings_service(
service_id,
1338,
opposite_proto,
"::dead:beef" if args.ipv6 else "123.123.123.123",
True,
):
srv_updated = firegex.nfregex_get_service(service_id)
if (
srv_updated["port"] == 1338
and srv_updated["proto"] == opposite_proto
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
in srv_updated["ip_int"]
and srv_updated["fail_open"]
):
puts("Sucessfully changed service settings ✔", color=colors.green)
else:
puts(
"Test Failed: Service settings weren't updated correctly ✗",
color=colors.red,
)
exit_test(1)
else:
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
exit_test(1)
exit_test(0)

View File

@@ -7,131 +7,180 @@ import argparse
import secrets import secrets
import time import time
parser = argparse.ArgumentParser() if __name__ == "__main__":
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/") parser = argparse.ArgumentParser()
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password') parser.add_argument(
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service") "--address",
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337) "-a",
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False) type=str,
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp") required=False,
help="Address of firegex backend",
default="http://127.0.0.1:4444/",
)
parser.add_argument(
"--password", "-p", type=str, required=True, help="Firegex password"
)
parser.add_argument(
"--service_name",
"-n",
type=str,
required=False,
help="Name of the test service",
default="Test Service",
)
parser.add_argument(
"--port",
"-P",
type=int,
required=False,
help="Port of the test service",
default=1337,
)
parser.add_argument(
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
)
parser.add_argument(
"--proto",
"-m",
type=str,
required=False,
choices=["tcp", "udp"],
help="Select the protocol",
default="tcp",
)
args = parser.parse_args() args = parser.parse_args()
sep() sep()
puts("Testing will start on ", color=colors.cyan, end="") puts("Testing will start on ", color=colors.cyan, end="")
puts(f"{args.address}", color=colors.yellow) puts(f"{args.address}", color=colors.yellow)
firegex = FiregexAPI(args.address) firegex = FiregexAPI(args.address)
#Login # Login
if (firegex.login(args.password)): if firegex.login(args.password):
puts("Sucessfully logged in ✔", color=colors.green) puts("Sucessfully logged in ✔", color=colors.green)
else:
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
exit(1)
#Create server
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+1,ipv6=args.ipv6,proxy_port=args.port)
def exit_test(code):
if service_id:
server.stop()
if(firegex.ph_delete_service(service_id)):
puts("Sucessfully deleted service ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
exit_test(1)
exit(code)
srvs = firegex.ph_get_services()
for ele in srvs:
if ele['name'] == args.service_name:
firegex.ph_delete_service(ele['service_id'])
#Create and start serivce
service_id = firegex.ph_add_service(args.service_name, args.port, args.port+1, args.proto , "::1" if args.ipv6 else "127.0.0.1", "::1" if args.ipv6 else "127.0.0.1")
if service_id:
puts(f"Sucessfully created service {service_id}", color=colors.green)
else:
puts("Test Failed: Failed to create service ✗", color=colors.red)
exit(1)
if(firegex.ph_start_service(service_id)):
puts("Sucessfully started service ✔", color=colors.green)
else:
puts("Test Failed: Failed to start service ✗", color=colors.red)
exit_test(1)
server.start()
time.sleep(0.5)
#Check if it started
def checkData(should_work):
res = None
try:
res = server.sendCheckData(secrets.token_bytes(432))
except (ConnectionRefusedError, TimeoutError):
res = None
if res:
if should_work:
puts("Successfully received data ✔", color=colors.green)
else:
puts("Test Failed: Connection wasn't blocked ✗", color=colors.red)
exit_test(1)
else: else:
if should_work: puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
puts("Test Failed: Data wans't received ✗", color=colors.red) exit(1)
exit_test(1)
# Create server
server = (TcpServer if args.proto == "tcp" else UdpServer)(
args.port + 1, ipv6=args.ipv6, proxy_port=args.port
)
def exit_test(code):
if service_id:
server.stop()
if firegex.ph_delete_service(service_id):
puts("Sucessfully deleted service ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
exit_test(1)
exit(code)
srvs = firegex.ph_get_services()
for ele in srvs:
if ele["name"] == args.service_name:
firegex.ph_delete_service(ele["service_id"])
# Create and start serivce
service_id = firegex.ph_add_service(
args.service_name,
args.port,
args.port + 1,
args.proto,
"::1" if args.ipv6 else "127.0.0.1",
"::1" if args.ipv6 else "127.0.0.1",
)
if service_id:
puts(f"Sucessfully created service {service_id}", color=colors.green)
else:
puts("Test Failed: Failed to create service ✗", color=colors.red)
exit(1)
if firegex.ph_start_service(service_id):
puts("Sucessfully started service ✔", color=colors.green)
else:
puts("Test Failed: Failed to start service ✗", color=colors.red)
exit_test(1)
server.start()
time.sleep(0.5)
# Check if it started
def checkData(should_work):
res = None
try:
res = server.sendCheckData(secrets.token_bytes(432))
except (ConnectionRefusedError, TimeoutError):
res = None
if res:
if should_work:
puts("Successfully received data ✔", color=colors.green)
else:
puts("Test Failed: Connection wasn't blocked ✗", color=colors.red)
exit_test(1)
else: else:
puts("Successfully blocked connection ✔", color=colors.green) if should_work:
puts("Test Failed: Data wans't received ✗", color=colors.red)
exit_test(1)
else:
puts("Successfully blocked connection ✔", color=colors.green)
checkData(True) checkData(True)
#Pause the proxy # Pause the proxy
if(firegex.ph_stop_service(service_id)): if firegex.ph_stop_service(service_id):
puts(f"Sucessfully paused service with id {service_id}", color=colors.green) puts(f"Sucessfully paused service with id {service_id}", color=colors.green)
else: else:
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red) puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
exit_test(1)
checkData(False)
# Start firewall
if firegex.ph_start_service(service_id):
puts(f"Sucessfully started service with id {service_id}", color=colors.green)
else:
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
exit_test(1)
checkData(True)
# Change port
if firegex.ph_change_destination(
service_id, "::1" if args.ipv6 else "127.0.0.1", args.port + 2
):
puts("Sucessfully changed port ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
exit_test(1)
checkData(False)
server.stop()
server = (TcpServer if args.proto == "tcp" else UdpServer)(
args.port + 2, ipv6=args.ipv6, proxy_port=args.port
)
server.start()
time.sleep(0.5)
checkData(True)
# Rename service
if firegex.ph_rename_service(service_id, f"{args.service_name}2"):
puts(
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
)
else:
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
exit_test(1)
# Check if service was renamed correctly
for services in firegex.ph_get_services():
if services["name"] == f"{args.service_name}2":
puts("Checked that service was renamed correctly ✔", color=colors.green)
exit_test(0)
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
exit_test(1) exit_test(1)
checkData(False)
#Start firewall
if(firegex.ph_start_service(service_id)):
puts(f"Sucessfully started service with id {service_id}", color=colors.green)
else:
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
exit_test(1)
checkData(True)
#Change port
if(firegex.ph_change_destination(service_id, "::1" if args.ipv6 else "127.0.0.1", args.port+2)):
puts("Sucessfully changed port ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
exit_test(1)
checkData(False)
server.stop()
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+2,ipv6=args.ipv6,proxy_port=args.port)
server.start()
time.sleep(0.5)
checkData(True)
#Rename service
if(firegex.ph_rename_service(service_id,f"{args.service_name}2")):
puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green)
else:
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
exit_test(1)
#Check if service was renamed correctly
for services in firegex.ph_get_services():
if services["name"] == f"{args.service_name}2":
puts("Checked that service was renamed correctly ✔", color=colors.green)
exit_test(0)
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
exit_test(1)

View File

@@ -8,7 +8,7 @@ ERROR=0
pip3 install -r requirements.txt pip3 install -r requirements.txt
until curl --output /dev/null --silent --fail http://localhost:4444/api/status; do until curl --output /dev/null --silent --fail http://127.0.0.1:4444/api/status; do
printf '.' printf '.'
sleep 1 sleep 1
done done

View File

@@ -1,9 +1,46 @@
import queue
from multiprocessing import Process, Queue from multiprocessing import Process, Queue
import socket import socket
import traceback import traceback
def _start_tcp_server(port, server_queue: Queue, ipv6, verbose):
sock = socket.socket(
socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM
)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
sock.listen(8)
while True:
connection, address = sock.accept()
while True:
try:
buf = connection.recv(4096)
if buf == b"":
break
reply = buf # Default to echo
try:
# See if there is a custom reply, but don't block
custom_reply = server_queue.get(block=False)
reply = custom_reply
except queue.Empty:
pass # No custom reply, just echo
if verbose:
print("SERVER: ", reply)
connection.sendall(reply)
except (ConnectionResetError, BrokenPipeError):
break # Client closed connection
except Exception:
if verbose:
traceback.print_exc()
break # Exit on other errors
connection.close()
class TcpServer: class TcpServer:
def __init__(self,port,ipv6,proxy_port=None, verbose=False): def __init__(self, port, ipv6, proxy_port=None, verbose=False):
self.proxy_port = proxy_port self.proxy_port = proxy_port
self.ipv6 = ipv6 self.ipv6 = ipv6
self.port = port self.port = port
@@ -12,30 +49,10 @@ class TcpServer:
self._regen_process() self._regen_process()
def _regen_process(self): def _regen_process(self):
def _startServer(port, server_queue:Queue): self.server = Process(
sock = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM) target=_start_tcp_server,
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) args=[self.port, self._server_data_queue, self.ipv6, self.verbose],
sock.bind(('::1' if self.ipv6 else '127.0.0.1', port)) )
sock.listen(8)
while True:
connection,address = sock.accept()
while True:
try:
buf = connection.recv(4096)
if buf == b'':
break
try:
buf = server_queue.get(block=False)
except Exception:
pass
if self.verbose:
print("SERVER: ", buf)
connection.sendall(buf)
except Exception:
if self.verbose:
traceback.print_exc()
connection.close()
self.server = Process(target=_startServer,args=[self.port, self._server_data_queue])
def start(self): def start(self):
self.server.start() self.server.start()
@@ -46,9 +63,16 @@ class TcpServer:
self._regen_process() self._regen_process()
def connect_client(self): def connect_client(self):
self.client_sock = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM) self.client_sock = socket.socket(
socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM
)
self.client_sock.settimeout(1) self.client_sock.settimeout(1)
self.client_sock.connect(('::1' if self.ipv6 else '127.0.0.1', self.proxy_port if self.proxy_port else self.port)) self.client_sock.connect(
(
"::1" if self.ipv6 else "127.0.0.1",
self.proxy_port if self.proxy_port else self.port,
)
)
def close_client(self): def close_client(self):
if self.client_sock: if self.client_sock:

View File

@@ -1,35 +1,94 @@
from multiprocessing import Process from multiprocessing import Process, Queue
import socket import socket
import queue
import traceback
def _start_udp_server(port, server_queue: Queue, ipv6, verbose):
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
while True:
try:
bytesAddressPair = sock.recvfrom(4096)
message = bytesAddressPair[0]
address = bytesAddressPair[1]
reply = message # Default to echo
try:
# See if there is a custom reply, but don't block
custom_reply = server_queue.get(block=False)
reply = custom_reply
except queue.Empty:
pass # No custom reply, just echo
if verbose:
print(f"SERVER: sending {reply} to {address}")
sock.sendto(reply, address)
except Exception:
if verbose:
traceback.print_exc()
class UdpServer: class UdpServer:
def __init__(self,port,ipv6, proxy_port = None): def __init__(self, port, ipv6, proxy_port=None, verbose=False):
def _startServer(port):
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('::1' if ipv6 else '127.0.0.1', port))
while True:
bytesAddressPair = sock.recvfrom(432)
message = bytesAddressPair[0]
address = bytesAddressPair[1]
sock.sendto(message, address)
self.ipv6 = ipv6
self.port = port self.port = port
self.ipv6 = ipv6
self.proxy_port = proxy_port self.proxy_port = proxy_port
self.server = Process(target=_startServer,args=[port]) self.verbose = verbose
self._server_data_queue = Queue()
self._regen_process()
def _regen_process(self):
self.server = Process(
target=_start_udp_server,
args=[self.port, self._server_data_queue, self.ipv6, self.verbose],
)
def start(self): def start(self):
self.server.start() self.server.start()
def stop(self): def stop(self):
self.server.terminate() self.server.terminate()
self.server.join()
self._regen_process()
def sendCheckData(self,data): def connect_client(self):
s = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_DGRAM) self.client_sock = socket.socket(
s.settimeout(2) socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_DGRAM
s.sendto(data, ('::1' if self.ipv6 else '127.0.0.1', self.proxy_port if self.proxy_port else self.port)) )
self.client_sock.settimeout(1)
self.client_sock.connect(
(
"::1" if self.ipv6 else "127.0.0.1",
self.proxy_port if self.proxy_port else self.port,
)
)
def close_client(self):
if self.client_sock:
self.client_sock.close()
def send_packet(self, packet, server_reply=None):
if self.verbose:
print("CLIENT: ", packet)
if server_reply:
self._server_data_queue.put(server_reply)
self.client_sock.sendall(packet)
def recv_packet(self):
try: try:
received_data = s.recvfrom(432) return self.client_sock.recv(4096)
except Exception: except (TimeoutError, ConnectionResetError):
if self.verbose:
traceback.print_exc()
return False return False
return received_data[0] == data
def sendCheckData(self, data, get_data=False):
self.connect_client()
self.send_packet(data)
received_data = self.recv_packet()
self.close_client()
if get_data:
return received_data
return received_data == data