Compare commits
17 Commits
88f4f54b55
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c387d9b40b | ||
|
|
d8061985d6 | ||
|
|
c237112077 | ||
|
|
811773e009 | ||
|
|
f1ebada95d | ||
|
|
9af3023a37 | ||
|
|
16f96aa6f6 | ||
|
|
49c6c14fe5 | ||
|
|
b352790f64 | ||
|
|
f3024cc9a8 | ||
|
|
753ed241b6 | ||
|
|
676e1dcb77 | ||
|
|
0c5a681f5b | ||
|
|
c726855b1c | ||
|
|
bb4addf590 | ||
|
|
f554ac558a | ||
|
|
0492f16cea |
78
.github/workflows/pypi-publish-fgex.yml
vendored
78
.github/workflows/pypi-publish-fgex.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
80
.github/workflows/pypi-publish.yml
vendored
80
.github/workflows/pypi-publish.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
40
Dockerfile
40
Dockerfile
@@ -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"]
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
223
backend/routers/setup.py
Normal 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
116
docs/TRAFFIC_VIEWER.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')}
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@@ -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 │
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 }) {
|
||||||
@@ -39,12 +40,14 @@ export default function NavBar() {
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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" />}
|
||||||
|
|||||||
306
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
306
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal 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>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
278
frontend/src/pages/Setup/index.tsx
Normal file
278
frontend/src/pages/Setup/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
307
frontend/src/pages/TrafficViewer/index.tsx
Normal file
307
frontend/src/pages/TrafficViewer/index.tsx
Normal 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
28
run.py
@@ -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
20
setup.example.json
Normal 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": []
|
||||||
|
}
|
||||||
@@ -4,9 +4,19 @@ from utils.firegexapi import FiregexAPI
|
|||||||
import argparse
|
import argparse
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
parser.add_argument(
|
||||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
"--address",
|
||||||
|
"-a",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Address of firegex backend",
|
||||||
|
default="http://127.0.0.1:4444/",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
sep()
|
sep()
|
||||||
puts("Testing will start on ", color=colors.cyan, end="")
|
puts("Testing will start on ", color=colors.cyan, end="")
|
||||||
@@ -16,19 +26,22 @@ firegex = FiregexAPI(args.address)
|
|||||||
|
|
||||||
# Connect to Firegex
|
# Connect to Firegex
|
||||||
if firegex.status()["status"] == "init":
|
if firegex.status()["status"] == "init":
|
||||||
if (firegex.set_password(args.password)):
|
if firegex.set_password(args.password):
|
||||||
puts(f"Sucessfully set password to {args.password} ✔", color=colors.green)
|
puts(f"Sucessfully set password to {args.password} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or password already put ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Unknown response or password already put ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
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:
|
else:
|
||||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if(firegex.status()["loggined"]):
|
if firegex.status()["loggined"]:
|
||||||
puts("Correctly received status ✔", color=colors.green)
|
puts("Correctly received status ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or not logged in✗", color=colors.red)
|
puts("Test Failed: Unknown response or not logged in✗", color=colors.red)
|
||||||
@@ -36,60 +49,81 @@ else:
|
|||||||
|
|
||||||
# Prepare second instance
|
# Prepare second instance
|
||||||
firegex2 = FiregexAPI(args.address)
|
firegex2 = FiregexAPI(args.address)
|
||||||
if (firegex2.login(args.password)):
|
if firegex2.login(args.password):
|
||||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if(firegex2.status()["loggined"]):
|
if firegex2.status()["loggined"]:
|
||||||
puts("Correctly received status on second instance✔", color=colors.green)
|
puts("Correctly received status on second instance✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or not logged in on second instance✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Unknown response or not logged in on second instance✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Change password
|
# Change password
|
||||||
new_password = secrets.token_hex(10)
|
new_password = secrets.token_hex(10)
|
||||||
if (firegex.change_password(new_password,expire=True)):
|
if firegex.change_password(new_password, expire=True):
|
||||||
puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green)
|
puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Check if we are still logged in
|
# Check if we are still logged in
|
||||||
if(firegex.status()["loggined"]):
|
if firegex.status()["loggined"]:
|
||||||
puts("Correctly received status after password change ✔", color=colors.green)
|
puts("Correctly received status after password change ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Unknown response or not logged after password change ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Check if second session expired and relog
|
# Check if second session expired and relog
|
||||||
|
|
||||||
if(not firegex2.status()["loggined"]):
|
if not firegex2.status()["loggined"]:
|
||||||
puts("Second instance was expired currectly ✔", color=colors.green)
|
puts("Second instance was expired currectly ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Still logged in on second instance, expire expected ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Still logged in on second instance, expire expected ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
if (firegex2.login(new_password)):
|
if firegex2.login(new_password):
|
||||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Change it back
|
# Change it back
|
||||||
if (firegex.change_password(args.password,expire=False)):
|
if firegex.change_password(args.password, expire=False):
|
||||||
puts("Sucessfully restored the password ✔", color=colors.green)
|
puts("Sucessfully restored the password ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Check if we are still logged in
|
# Check if we are still logged in
|
||||||
if(firegex2.status()["loggined"]):
|
if firegex2.status()["loggined"]:
|
||||||
puts("Correctly received status after password change ✔", color=colors.green)
|
puts("Correctly received status after password change ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Unknown response or not logged after password change ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
puts("List of available interfaces:", color=colors.yellow)
|
puts("List of available interfaces:", color=colors.yellow)
|
||||||
for interface in firegex.get_interfaces():
|
for interface in firegex.get_interfaces():
|
||||||
puts("name: {}, address: {}".format(interface["name"], interface["addr"]), color=colors.yellow)
|
puts(
|
||||||
|
"name: {}, address: {}".format(interface["name"], interface["addr"]),
|
||||||
|
color=colors.yellow,
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ from utils.tcpserver import TcpServer
|
|||||||
import argparse
|
import argparse
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--address",
|
"--address",
|
||||||
@@ -15,7 +17,9 @@ parser.add_argument(
|
|||||||
help="Address of firegex backend",
|
help="Address of firegex backend",
|
||||||
default="http://127.0.0.1:4444/",
|
default="http://127.0.0.1:4444/",
|
||||||
)
|
)
|
||||||
parser.add_argument("--password", "-p", type=str, required=True, help="Firegex password")
|
parser.add_argument(
|
||||||
|
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--service_name",
|
"--service_name",
|
||||||
"-n",
|
"-n",
|
||||||
@@ -32,7 +36,9 @@ parser.add_argument(
|
|||||||
help="Port of the test service",
|
help="Port of the test service",
|
||||||
default=1337,
|
default=1337,
|
||||||
)
|
)
|
||||||
parser.add_argument("--ipv6", "-6", action="store_true", help="Test Ipv6", default=False)
|
parser.add_argument(
|
||||||
|
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--verbose", "-V", action="store_true", help="Verbose output", default=False
|
"--verbose", "-V", action="store_true", help="Verbose output", default=False
|
||||||
)
|
)
|
||||||
@@ -68,19 +74,15 @@ else:
|
|||||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def exit_test(code):
|
def exit_test(code):
|
||||||
if service_id:
|
if service_id:
|
||||||
server.stop()
|
server.stop()
|
||||||
"""
|
|
||||||
if firegex.nfproxy_delete_service(service_id):
|
if firegex.nfproxy_delete_service(service_id):
|
||||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||||
"""
|
|
||||||
exit(code)
|
exit(code)
|
||||||
|
|
||||||
|
|
||||||
if firegex.nfproxy_start_service(service_id):
|
if firegex.nfproxy_start_service(service_id):
|
||||||
puts("Sucessfully started service ✔", color=colors.green)
|
puts("Sucessfully started service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
@@ -91,7 +93,9 @@ server.start()
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
try:
|
try:
|
||||||
if server.sendCheckData(secrets.token_bytes(432)):
|
if server.sendCheckData(secrets.token_bytes(432)):
|
||||||
puts("Successfully tested first proxy with no filters ✔", color=colors.green)
|
puts(
|
||||||
|
"Successfully tested first proxy with no filters ✔", color=colors.green
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Data was corrupted ", color=colors.red)
|
puts("Test Failed: Data was corrupted ", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -99,7 +103,7 @@ except Exception:
|
|||||||
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
|
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
BASE_FILTER_VERDICT_TEST = """
|
BASE_FILTER_VERDICT_TEST = textwrap.dedent("""
|
||||||
from firegex.nfproxy.models import RawPacket
|
from firegex.nfproxy.models import RawPacket
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -108,11 +112,10 @@ def verdict_test(packet:RawPacket):
|
|||||||
if b"%%TEST%%" in packet.data:
|
if b"%%TEST%%" in packet.data:
|
||||||
packet.l4_data = packet.l4_data.replace(b"%%TEST%%", b"%%MANGLE%%")
|
packet.l4_data = packet.l4_data.replace(b"%%TEST%%", b"%%MANGLE%%")
|
||||||
return %%ACTION%%
|
return %%ACTION%%
|
||||||
"""
|
""")
|
||||||
|
|
||||||
BASE_FILTER_VERDICT_NAME = "verdict_test"
|
BASE_FILTER_VERDICT_NAME = "verdict_test"
|
||||||
|
|
||||||
|
|
||||||
def get_vedict_test(to_match: str, action: str, mangle_to: str = "REDACTED"):
|
def get_vedict_test(to_match: str, action: str, mangle_to: str = "REDACTED"):
|
||||||
return (
|
return (
|
||||||
BASE_FILTER_VERDICT_TEST.replace("%%TEST%%", to_match)
|
BASE_FILTER_VERDICT_TEST.replace("%%TEST%%", to_match)
|
||||||
@@ -120,12 +123,10 @@ def get_vedict_test(to_match: str, action: str, mangle_to: str = "REDACTED"):
|
|||||||
.replace("%%MANGLE%%", mangle_to)
|
.replace("%%MANGLE%%", mangle_to)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Check if filter is present in the service
|
# Check if filter is present in the service
|
||||||
n_blocked = 0
|
n_blocked = 0
|
||||||
n_mangled = 0
|
n_mangled = 0
|
||||||
|
|
||||||
|
|
||||||
def checkFilter(match_bytes, filter_name, should_work=True, mangle_with=None):
|
def checkFilter(match_bytes, filter_name, should_work=True, mangle_with=None):
|
||||||
if mangle_with:
|
if mangle_with:
|
||||||
if should_work:
|
if should_work:
|
||||||
@@ -176,7 +177,8 @@ def checkFilter(match_bytes, filter_name, should_work=True, mangle_with=None):
|
|||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
puts(
|
puts(
|
||||||
"Test Failed: The request wasn't mangled ✗", color=colors.red
|
"Test Failed: The request wasn't mangled ✗",
|
||||||
|
color=colors.red,
|
||||||
)
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
server.close_client()
|
server.close_client()
|
||||||
@@ -200,7 +202,9 @@ def checkFilter(match_bytes, filter_name, should_work=True, mangle_with=None):
|
|||||||
if r["name"] == filter_name:
|
if r["name"] == filter_name:
|
||||||
# Test the filter
|
# Test the filter
|
||||||
if not server.sendCheckData(
|
if not server.sendCheckData(
|
||||||
secrets.token_bytes(40) + match_bytes + secrets.token_bytes(40)
|
secrets.token_bytes(40)
|
||||||
|
+ match_bytes
|
||||||
|
+ secrets.token_bytes(40)
|
||||||
):
|
):
|
||||||
puts(
|
puts(
|
||||||
"The malicious request was successfully blocked ✔",
|
"The malicious request was successfully blocked ✔",
|
||||||
@@ -226,7 +230,8 @@ def checkFilter(match_bytes, filter_name, should_work=True, mangle_with=None):
|
|||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
puts(
|
puts(
|
||||||
"Test Failed: The request wasn't blocked ✗", color=colors.red
|
"Test Failed: The request wasn't blocked ✗",
|
||||||
|
color=colors.red,
|
||||||
)
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
return
|
return
|
||||||
@@ -244,7 +249,6 @@ def checkFilter(match_bytes, filter_name, should_work=True, mangle_with=None):
|
|||||||
)
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
|
|
||||||
# Add new filter
|
# Add new filter
|
||||||
secret = bytes(secrets.token_hex(16).encode())
|
secret = bytes(secrets.token_hex(16).encode())
|
||||||
|
|
||||||
@@ -280,7 +284,10 @@ checkFilter(secret, BASE_FILTER_VERDICT_NAME)
|
|||||||
|
|
||||||
# Disable filter
|
# Disable filter
|
||||||
if firegex.nfproxy_disable_pyfilter(service_id, BASE_FILTER_VERDICT_NAME):
|
if firegex.nfproxy_disable_pyfilter(service_id, BASE_FILTER_VERDICT_NAME):
|
||||||
puts(f"Sucessfully disabled filter {BASE_FILTER_VERDICT_NAME} ✔", color=colors.green)
|
puts(
|
||||||
|
f"Sucessfully disabled filter {BASE_FILTER_VERDICT_NAME} ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't disable the filter ✗", color=colors.red)
|
puts("Test Failed: Coulnd't disable the filter ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -290,25 +297,27 @@ checkFilter(secret, BASE_FILTER_VERDICT_NAME, should_work=False)
|
|||||||
|
|
||||||
# Enable filter
|
# Enable filter
|
||||||
if firegex.nfproxy_enable_pyfilter(service_id, BASE_FILTER_VERDICT_NAME):
|
if firegex.nfproxy_enable_pyfilter(service_id, BASE_FILTER_VERDICT_NAME):
|
||||||
puts(f"Sucessfully enabled filter {BASE_FILTER_VERDICT_NAME} ✔", color=colors.green)
|
puts(
|
||||||
|
f"Sucessfully enabled filter {BASE_FILTER_VERDICT_NAME} ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
checkFilter(secret, BASE_FILTER_VERDICT_NAME)
|
checkFilter(secret, BASE_FILTER_VERDICT_NAME)
|
||||||
|
|
||||||
|
|
||||||
def remove_filters():
|
def remove_filters():
|
||||||
global n_blocked, n_mangled
|
global n_blocked, n_mangled
|
||||||
server.stop()
|
server.stop()
|
||||||
server.start()
|
server.start()
|
||||||
|
time.sleep(0.5) # Wait for server to fully start
|
||||||
if not firegex.nfproxy_set_code(service_id, ""):
|
if not firegex.nfproxy_set_code(service_id, ""):
|
||||||
puts("Test Failed: Couldn't remove the filter ✗", color=colors.red)
|
puts("Test Failed: Couldn't remove the filter ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
n_blocked = 0
|
n_blocked = 0
|
||||||
n_mangled = 0
|
n_mangled = 0
|
||||||
|
|
||||||
|
|
||||||
remove_filters()
|
remove_filters()
|
||||||
|
|
||||||
# Check if it's actually deleted
|
# Check if it's actually deleted
|
||||||
@@ -317,7 +326,8 @@ checkFilter(secret, BASE_FILTER_VERDICT_NAME, should_work=False)
|
|||||||
# Check if DROP works
|
# Check if DROP works
|
||||||
if firegex.nfproxy_set_code(service_id, get_vedict_test(secret.decode(), "DROP")):
|
if firegex.nfproxy_set_code(service_id, get_vedict_test(secret.decode(), "DROP")):
|
||||||
puts(
|
puts(
|
||||||
f"Sucessfully added filter for {str(secret)} in DROP mode ✔", color=colors.green
|
f"Sucessfully added filter for {str(secret)} in DROP mode ✔",
|
||||||
|
color=colors.green,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
puts(f"Test Failed: Couldn't add the filter {str(secret)} ✗", color=colors.red)
|
puts(f"Test Failed: Couldn't add the filter {str(secret)} ✗", color=colors.red)
|
||||||
@@ -364,7 +374,7 @@ checkFilter(secret, BASE_FILTER_VERDICT_NAME, mangle_with=mangle_result)
|
|||||||
remove_filters()
|
remove_filters()
|
||||||
|
|
||||||
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
||||||
TCP_INPUT_STREAM_TEST = f"""
|
TCP_INPUT_STREAM_TEST = textwrap.dedent(f"""
|
||||||
from firegex.nfproxy.models import TCPInputStream
|
from firegex.nfproxy.models import TCPInputStream
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -373,7 +383,7 @@ def data_type_test(packet:TCPInputStream):
|
|||||||
if {repr(secret)} in packet.data:
|
if {repr(secret)} in packet.data:
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, TCP_INPUT_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, TCP_INPUT_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -403,7 +413,7 @@ server.close_client()
|
|||||||
remove_filters()
|
remove_filters()
|
||||||
|
|
||||||
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
||||||
TCP_OUTPUT_STREAM_TEST = f"""
|
TCP_OUTPUT_STREAM_TEST = textwrap.dedent(f"""
|
||||||
from firegex.nfproxy.models import TCPOutputStream
|
from firegex.nfproxy.models import TCPOutputStream
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -412,7 +422,7 @@ def data_type_test(packet:TCPOutputStream):
|
|||||||
if {repr(secret)} in packet.data:
|
if {repr(secret)} in packet.data:
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, TCP_OUTPUT_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, TCP_OUTPUT_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -443,21 +453,21 @@ remove_filters()
|
|||||||
|
|
||||||
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
||||||
|
|
||||||
REQUEST_HEADER_TEST = f"""POST / HTTP/1.1
|
REQUEST_HEADER_TEST = textwrap.dedent(f"""POST / HTTP/1.1
|
||||||
Host: localhost
|
Host: localhost
|
||||||
X-TeSt: {secret.decode()}
|
X-TeSt: {secret.decode()}
|
||||||
Content-Length: 15
|
Content-Length: 15
|
||||||
|
|
||||||
A Friendly Body""".replace("\n", "\r\n")
|
A Friendly Body""").replace("\n", "\r\n")
|
||||||
|
|
||||||
REQUEST_BODY_TEST = f"""POST / HTTP/1.1
|
REQUEST_BODY_TEST = textwrap.dedent(f"""POST / HTTP/1.1
|
||||||
Host: localhost
|
Host: localhost
|
||||||
X-TeSt: NotTheSecret
|
X-TeSt: NotTheSecret
|
||||||
Content-Length: {len(secret.decode())}
|
Content-Length: {len(secret.decode())}
|
||||||
|
|
||||||
{secret.decode()}""".replace("\n", "\r\n")
|
{secret.decode()}""").replace("\n", "\r\n")
|
||||||
|
|
||||||
HTTP_REQUEST_STREAM_TEST = f"""
|
HTTP_REQUEST_STREAM_TEST = textwrap.dedent(f"""
|
||||||
from firegex.nfproxy.models import HttpRequest
|
from firegex.nfproxy.models import HttpRequest
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -469,7 +479,7 @@ def data_type_test(req:HttpRequest):
|
|||||||
if {repr(secret)} in req.body:
|
if {repr(secret)} in req.body:
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, HTTP_REQUEST_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, HTTP_REQUEST_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -512,7 +522,7 @@ server.close_client()
|
|||||||
|
|
||||||
remove_filters()
|
remove_filters()
|
||||||
|
|
||||||
HTTP_FULL_REQUEST_STREAM_TEST = f"""
|
HTTP_FULL_REQUEST_STREAM_TEST = textwrap.dedent(f"""
|
||||||
from firegex.nfproxy.models import HttpFullRequest
|
from firegex.nfproxy.models import HttpFullRequest
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -527,7 +537,7 @@ def data_type_test(req:HttpFullRequest):
|
|||||||
if {repr(secret)} in req.body:
|
if {repr(secret)} in req.body:
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, HTTP_FULL_REQUEST_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, HTTP_FULL_REQUEST_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -570,8 +580,7 @@ server.close_client()
|
|||||||
|
|
||||||
remove_filters()
|
remove_filters()
|
||||||
|
|
||||||
|
HTTP_REQUEST_HEADER_STREAM_TEST = textwrap.dedent(f"""
|
||||||
HTTP_REQUEST_HEADER_STREAM_TEST = f"""
|
|
||||||
from firegex.nfproxy.models import HttpRequestHeader
|
from firegex.nfproxy.models import HttpRequestHeader
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -580,7 +589,7 @@ def data_type_test(req:HttpRequestHeader):
|
|||||||
if {repr(secret.decode())} in req.get_header("x-test"):
|
if {repr(secret.decode())} in req.get_header("x-test"):
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, HTTP_REQUEST_HEADER_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, HTTP_REQUEST_HEADER_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -610,21 +619,21 @@ remove_filters()
|
|||||||
|
|
||||||
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
secret = b"8331ee1bf75893dd7fa3d34f29bac7fc8935aa3ef6c565fe8b395ef7f485"
|
||||||
|
|
||||||
RESPONSE_HEADER_TEST = f"""HTTP/1.1 200 OK
|
RESPONSE_HEADER_TEST = textwrap.dedent(f"""HTTP/1.1 200 OK
|
||||||
Host: localhost
|
Host: localhost
|
||||||
X-TeSt: {secret.decode()}
|
X-TeSt: {secret.decode()}
|
||||||
Content-Length: 15
|
Content-Length: 15
|
||||||
|
|
||||||
A Friendly Body""".replace("\n", "\r\n")
|
A Friendly Body""").replace("\n", "\r\n")
|
||||||
|
|
||||||
RESPONSE_BODY_TEST = f"""HTTP/1.1 200 OK
|
RESPONSE_BODY_TEST = textwrap.dedent(f"""HTTP/1.1 200 OK
|
||||||
Host: localhost
|
Host: localhost
|
||||||
X-TeSt: NotTheSecret
|
X-TeSt: NotTheSecret
|
||||||
Content-Length: {len(secret.decode())}
|
Content-Length: {len(secret.decode())}
|
||||||
|
|
||||||
{secret.decode()}""".replace("\n", "\r\n")
|
{secret.decode()}""").replace("\n", "\r\n")
|
||||||
|
|
||||||
HTTP_RESPONSE_STREAM_TEST = f"""
|
HTTP_RESPONSE_STREAM_TEST = textwrap.dedent(f"""
|
||||||
from firegex.nfproxy.models import HttpResponse
|
from firegex.nfproxy.models import HttpResponse
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -636,7 +645,7 @@ def data_type_test(req:HttpResponse):
|
|||||||
if {repr(secret)} in req.body:
|
if {repr(secret)} in req.body:
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, HTTP_RESPONSE_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, HTTP_RESPONSE_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -679,8 +688,7 @@ server.close_client()
|
|||||||
|
|
||||||
remove_filters()
|
remove_filters()
|
||||||
|
|
||||||
|
HTTP_FULL_RESPONSE_STREAM_TEST = textwrap.dedent(f"""
|
||||||
HTTP_FULL_RESPONSE_STREAM_TEST = f"""
|
|
||||||
from firegex.nfproxy.models import HttpFullResponse
|
from firegex.nfproxy.models import HttpFullResponse
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -695,7 +703,7 @@ def data_type_test(req:HttpFullResponse):
|
|||||||
if {repr(secret)} in req.body:
|
if {repr(secret)} in req.body:
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, HTTP_FULL_RESPONSE_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, HTTP_FULL_RESPONSE_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -738,8 +746,7 @@ server.close_client()
|
|||||||
|
|
||||||
remove_filters()
|
remove_filters()
|
||||||
|
|
||||||
|
HTTP_RESPONSE_HEADER_STREAM_TEST = textwrap.dedent(f"""
|
||||||
HTTP_RESPONSE_HEADER_STREAM_TEST = f"""
|
|
||||||
from firegex.nfproxy.models import HttpResponseHeader
|
from firegex.nfproxy.models import HttpResponseHeader
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -748,7 +755,7 @@ def data_type_test(req:HttpResponseHeader):
|
|||||||
if {repr(secret.decode())} in req.get_header("x-test"):
|
if {repr(secret.decode())} in req.get_header("x-test"):
|
||||||
return REJECT
|
return REJECT
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, HTTP_RESPONSE_HEADER_STREAM_TEST):
|
if firegex.nfproxy_set_code(service_id, HTTP_RESPONSE_HEADER_STREAM_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -781,7 +788,7 @@ remove_filters()
|
|||||||
WS_REQUEST_PARSING_TEST = b"GET /sock/?EIO=4&transport=websocket HTTP/1.1\r\nHost: localhost:8080\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)\xac AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36\r\nUpgrade: websocket\r\nOrigin: http://localhost:8080\r\nSec-WebSocket-Version: 13\r\nAccept-Encoding: gzip, deflate, br, zstd\r\nAccept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,zh;q=0.5\r\nCookie: cookie-consent=true; _iub_cs-86405163=%7B%22timestamp%22%3A%222024-09-12T18%3A20%3A18.627Z%22%2C%22version%22%3A%221.65.1%22%2C%22purposes%22%3A%7B%221%22%3Atrue%2C%224%22%3Atrue%7D%2C%22id%22%3A86405163%2C%22cons%22%3A%7B%22rand%22%3A%222b09e6%22%7D%7D\r\nSec-WebSocket-Key: eE01O3/ZShPKsrykACLAaA==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n\xc1\x84#\x8a\xb2\xbb\x11\xbb\xb2\xbb"
|
WS_REQUEST_PARSING_TEST = b"GET /sock/?EIO=4&transport=websocket HTTP/1.1\r\nHost: localhost:8080\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)\xac AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36\r\nUpgrade: websocket\r\nOrigin: http://localhost:8080\r\nSec-WebSocket-Version: 13\r\nAccept-Encoding: gzip, deflate, br, zstd\r\nAccept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,zh;q=0.5\r\nCookie: cookie-consent=true; _iub_cs-86405163=%7B%22timestamp%22%3A%222024-09-12T18%3A20%3A18.627Z%22%2C%22version%22%3A%221.65.1%22%2C%22purposes%22%3A%7B%221%22%3Atrue%2C%224%22%3Atrue%7D%2C%22id%22%3A86405163%2C%22cons%22%3A%7B%22rand%22%3A%222b09e6%22%7D%7D\r\nSec-WebSocket-Key: eE01O3/ZShPKsrykACLAaA==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n\xc1\x84#\x8a\xb2\xbb\x11\xbb\xb2\xbb"
|
||||||
WS_RESPONSE_PARSING_TEST = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: eGnJqUSoSKE3wOfKD2M3G82RsS8=\r\nSec-WebSocket-Extensions: permessage-deflate\r\ndate: Sat, 15 Mar 2025 12:04:19 GMT\r\nserver: uvicorn\r\n\r\n\xc1_2\xa8V*\xceLQ\xb2Rr1\xb4\xc8\xf6r\x0c\xf3\xaf\xd25\xf7\x8e\xf4\xb3LsttrW\xd2Q*-H/JLI-V\xb2\x8a\x8e\xd5Q*\xc8\xccK\x0f\xc9\xccM\xcd/-Q\xb222\x00\x02\x88\x98g^IjQYb\x0eP\xd0\x14,\x98\x9bX\x11\x90X\x99\x93\x9f\x084\xda\xd0\x00\x0cj\x01\x00\xc1\x1b21\x80\xd9e\xe1n\x19\x9e\xe3RP\x9a[Z\x99\x93j\xea\x15\x00\xb4\xcbC\xa9\x16\x00"
|
WS_RESPONSE_PARSING_TEST = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: eGnJqUSoSKE3wOfKD2M3G82RsS8=\r\nSec-WebSocket-Extensions: permessage-deflate\r\ndate: Sat, 15 Mar 2025 12:04:19 GMT\r\nserver: uvicorn\r\n\r\n\xc1_2\xa8V*\xceLQ\xb2Rr1\xb4\xc8\xf6r\x0c\xf3\xaf\xd25\xf7\x8e\xf4\xb3LsttrW\xd2Q*-H/JLI-V\xb2\x8a\x8e\xd5Q*\xc8\xccK\x0f\xc9\xccM\xcd/-Q\xb222\x00\x02\x88\x98g^IjQYb\x0eP\xd0\x14,\x98\x9bX\x11\x90X\x99\x93\x9f\x084\xda\xd0\x00\x0cj\x01\x00\xc1\x1b21\x80\xd9e\xe1n\x19\x9e\xe3RP\x9a[Z\x99\x93j\xea\x15\x00\xb4\xcbC\xa9\x16\x00"
|
||||||
|
|
||||||
HTTP_REQUEST_WS_PARSING_TEST = """
|
HTTP_REQUEST_WS_PARSING_TEST = textwrap.dedent("""
|
||||||
from firegex.nfproxy.models import HttpRequest, HttpResponse
|
from firegex.nfproxy.models import HttpRequest, HttpResponse
|
||||||
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
from firegex.nfproxy import pyfilter, ACCEPT, UNSTABLE_MANGLE, DROP, REJECT
|
||||||
|
|
||||||
@@ -793,7 +800,7 @@ def data_type_test(req:HttpRequest):
|
|||||||
def data_type_test(req:HttpResponse):
|
def data_type_test(req:HttpResponse):
|
||||||
print(req)
|
print(req)
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
if firegex.nfproxy_set_code(service_id, HTTP_REQUEST_WS_PARSING_TEST):
|
if firegex.nfproxy_set_code(service_id, HTTP_REQUEST_WS_PARSING_TEST):
|
||||||
puts(
|
puts(
|
||||||
@@ -801,7 +808,9 @@ if firegex.nfproxy_set_code(service_id, HTTP_REQUEST_WS_PARSING_TEST):
|
|||||||
color=colors.green,
|
color=colors.green,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Couldn't add the websocket parsing filter ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Couldn't add the websocket parsing filter ✗", color=colors.red
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
server.connect_client()
|
server.connect_client()
|
||||||
@@ -823,7 +832,9 @@ remove_filters()
|
|||||||
|
|
||||||
# Rename service
|
# Rename service
|
||||||
if firegex.nfproxy_rename_service(service_id, f"{args.service_name}2"):
|
if firegex.nfproxy_rename_service(service_id, f"{args.service_name}2"):
|
||||||
puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green)
|
puts(
|
||||||
|
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -838,7 +849,9 @@ else:
|
|||||||
|
|
||||||
# Rename back service
|
# Rename back service
|
||||||
if firegex.nfproxy_rename_service(service_id, f"{args.service_name}"):
|
if firegex.nfproxy_rename_service(service_id, f"{args.service_name}"):
|
||||||
puts(f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green)
|
puts(
|
||||||
|
f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -850,13 +863,15 @@ if firegex.nfproxy_settings_service(
|
|||||||
srv_updated = firegex.nfproxy_get_service(service_id)
|
srv_updated = firegex.nfproxy_get_service(service_id)
|
||||||
if (
|
if (
|
||||||
srv_updated["port"] == 1338
|
srv_updated["port"] == 1338
|
||||||
and ("::dead:beef" if args.ipv6 else "123.123.123.123") in srv_updated["ip_int"]
|
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
|
||||||
|
in srv_updated["ip_int"]
|
||||||
and srv_updated["fail_open"]
|
and srv_updated["fail_open"]
|
||||||
):
|
):
|
||||||
puts("Sucessfully changed service settings ✔", color=colors.green)
|
puts("Sucessfully changed service settings ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts(
|
puts(
|
||||||
"Test Failed: Service settings weren't updated correctly ✗", color=colors.red
|
"Test Failed: Service settings weren't updated correctly ✗",
|
||||||
|
color=colors.red,
|
||||||
)
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -8,13 +8,47 @@ import secrets
|
|||||||
import base64
|
import base64
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
parser.add_argument(
|
||||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
"--address",
|
||||||
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service")
|
"-a",
|
||||||
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337)
|
type=str,
|
||||||
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False)
|
required=False,
|
||||||
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp")
|
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()
|
||||||
@@ -24,19 +58,21 @@ 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:
|
else:
|
||||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Create server
|
# Create server
|
||||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port,ipv6=args.ipv6)
|
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||||
|
args.port, ipv6=args.ipv6
|
||||||
|
)
|
||||||
|
|
||||||
def exit_test(code):
|
def exit_test(code):
|
||||||
if service_id:
|
if service_id:
|
||||||
server.stop()
|
server.stop()
|
||||||
if(firegex.nfregex_delete_service(service_id)):
|
if firegex.nfregex_delete_service(service_id):
|
||||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||||
@@ -45,17 +81,19 @@ def exit_test(code):
|
|||||||
|
|
||||||
srvs = firegex.nfregex_get_services()
|
srvs = firegex.nfregex_get_services()
|
||||||
for ele in srvs:
|
for ele in srvs:
|
||||||
if ele['name'] == args.service_name:
|
if ele["name"] == args.service_name:
|
||||||
firegex.nfregex_delete_service(ele['service_id'])
|
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" )
|
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:
|
if service_id:
|
||||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if(firegex.nfregex_start_service(service_id)):
|
if firegex.nfregex_start_service(service_id):
|
||||||
puts("Sucessfully started service ✔", color=colors.green)
|
puts("Sucessfully started service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||||
@@ -75,13 +113,14 @@ except Exception:
|
|||||||
# Add new regex
|
# Add new regex
|
||||||
secret = bytes(secrets.token_hex(16).encode())
|
secret = bytes(secrets.token_hex(16).encode())
|
||||||
|
|
||||||
if firegex.nfregex_add_regex(service_id,secret,"B",active=True,is_case_sensitive=True):
|
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)
|
puts(f"Sucessfully added regex {str(secret)} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
||||||
exit_test(1)
|
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
|
||||||
|
|
||||||
@@ -97,42 +136,84 @@ def checkRegex(regex, should_work=True, upper=False, deleted=False):
|
|||||||
if r["regex"] == secret:
|
if r["regex"] == secret:
|
||||||
# Test the regex
|
# Test the regex
|
||||||
s = regex.upper() if upper else regex
|
s = regex.upper() if upper else regex
|
||||||
if not server.sendCheckData(secrets.token_bytes(40) + s + secrets.token_bytes(40)):
|
if not server.sendCheckData(
|
||||||
puts("The malicious request was successfully blocked ✔", color=colors.green)
|
secrets.token_bytes(40) + s + secrets.token_bytes(40)
|
||||||
|
):
|
||||||
|
puts(
|
||||||
|
"The malicious request was successfully blocked ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
n_blocked += 1
|
n_blocked += 1
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
|
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
|
||||||
puts("The packet was reported as blocked in the API ✔", color=colors.green)
|
puts(
|
||||||
|
"The packet was reported as blocked in the API ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: The packet wasn't reported as blocked in the API ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: The packet wasn't reported as blocked in the API ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
if getMetric("firegex_blocked_packets", secret.decode()) == n_blocked:
|
if (
|
||||||
puts("The packet was reported as blocked in the metrics ✔", color=colors.green)
|
getMetric("firegex_blocked_packets", secret.decode())
|
||||||
|
== n_blocked
|
||||||
|
):
|
||||||
|
puts(
|
||||||
|
"The packet was reported as blocked in the metrics ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: The packet wasn't reported as blocked in the metrics ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: The packet wasn't reported as blocked in the metrics ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
if getMetric("firegex_active", secret.decode()) == 1:
|
if getMetric("firegex_active", secret.decode()) == 1:
|
||||||
puts("The regex was reported as active in the metrics ✔", color=colors.green)
|
puts(
|
||||||
|
"The regex was reported as active in the metrics ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: The regex wasn't reported as active in the metrics ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: The regex wasn't reported as active in the metrics ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: The request wasn't blocked ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: The request wasn't blocked ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
return
|
return
|
||||||
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
if server.sendCheckData(secrets.token_bytes(40) + base64.b64decode(regex) + secrets.token_bytes(40)):
|
if server.sendCheckData(
|
||||||
|
secrets.token_bytes(40)
|
||||||
|
+ base64.b64decode(regex)
|
||||||
|
+ secrets.token_bytes(40)
|
||||||
|
):
|
||||||
puts("The request wasn't blocked ✔", color=colors.green)
|
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: The request was blocked when it shouldn't have",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
if getMetric("firegex_active", secret.decode()) == 0:
|
if getMetric("firegex_active", secret.decode()) == 0:
|
||||||
puts("The regex was reported as inactive in the metrics ✔", color=colors.green)
|
puts(
|
||||||
|
"The regex was reported as inactive in the metrics ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: The regex wasn't reported as inactive in the metrics ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: The regex wasn't reported as inactive in the metrics ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
def clear_regexes():
|
def clear_regexes():
|
||||||
@@ -140,8 +221,11 @@ def clear_regexes():
|
|||||||
n_blocked = 0
|
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:
|
||||||
if(firegex.nfregex_delete_regex(r["id"])):
|
if firegex.nfregex_delete_regex(r["id"]):
|
||||||
puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green)
|
puts(
|
||||||
|
f"Sucessfully deleted 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 delete the regex ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -149,13 +233,16 @@ def clear_regexes():
|
|||||||
if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
|
if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
|
||||||
puts("No regex metrics after deletion ✔", color=colors.green)
|
puts("No regex metrics after deletion ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Metrics found after deleting the regex ✗", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Metrics found after deleting the regex ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
checkRegex(secret)
|
checkRegex(secret)
|
||||||
|
|
||||||
# Pause the proxy
|
# Pause the proxy
|
||||||
if(firegex.nfregex_stop_service(service_id)):
|
if firegex.nfregex_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)
|
||||||
@@ -165,7 +252,7 @@ else:
|
|||||||
checkRegex(secret, should_work=False)
|
checkRegex(secret, should_work=False)
|
||||||
|
|
||||||
# Start firewall
|
# Start firewall
|
||||||
if(firegex.nfregex_start_service(service_id)):
|
if firegex.nfregex_start_service(service_id):
|
||||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||||
@@ -176,8 +263,11 @@ checkRegex(secret)
|
|||||||
# Disable regex
|
# 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_disable_regex(r["id"])):
|
if firegex.nfregex_disable_regex(r["id"]):
|
||||||
puts(f"Sucessfully disabled 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 disable the regex ✗", color=colors.red)
|
puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -189,8 +279,10 @@ checkRegex(secret,should_work=False)
|
|||||||
# Enable regex
|
# Enable 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_enable_regex(r["id"])):
|
if firegex.nfregex_enable_regex(r["id"]):
|
||||||
puts(f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green)
|
puts(
|
||||||
|
f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -205,10 +297,18 @@ clear_regexes()
|
|||||||
checkRegex(secret, should_work=False, deleted=True)
|
checkRegex(secret, should_work=False, deleted=True)
|
||||||
|
|
||||||
# Add case insensitive regex
|
# Add case insensitive regex
|
||||||
if(firegex.nfregex_add_regex(service_id,secret,"B",active=True, is_case_sensitive=False)):
|
if firegex.nfregex_add_regex(
|
||||||
puts(f"Sucessfully added case insensitive regex {str(secret)} ✔", color=colors.green)
|
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(f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗", 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, upper=True)
|
checkRegex(secret, upper=True)
|
||||||
@@ -217,8 +317,10 @@ checkRegex(secret)
|
|||||||
clear_regexes()
|
clear_regexes()
|
||||||
|
|
||||||
# Rename service
|
# Rename service
|
||||||
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}2")):
|
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)
|
puts(
|
||||||
|
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
@@ -232,20 +334,37 @@ else:
|
|||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
# Rename back service
|
# Rename back service
|
||||||
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}")):
|
if firegex.nfregex_rename_service(service_id, f"{args.service_name}"):
|
||||||
puts(f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green)
|
puts(
|
||||||
|
f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
# Change settings
|
# Change settings
|
||||||
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
|
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)):
|
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)
|
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"]:
|
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)
|
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: Service settings weren't updated correctly ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
||||||
|
|||||||
@@ -7,13 +7,47 @@ import argparse
|
|||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
parser.add_argument(
|
||||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
"--address",
|
||||||
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service")
|
"-a",
|
||||||
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337)
|
type=str,
|
||||||
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False)
|
required=False,
|
||||||
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp")
|
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()
|
||||||
@@ -23,19 +57,21 @@ 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:
|
else:
|
||||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Create server
|
# Create server
|
||||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+1,ipv6=args.ipv6,proxy_port=args.port)
|
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||||
|
args.port + 1, ipv6=args.ipv6, proxy_port=args.port
|
||||||
|
)
|
||||||
|
|
||||||
def exit_test(code):
|
def exit_test(code):
|
||||||
if service_id:
|
if service_id:
|
||||||
server.stop()
|
server.stop()
|
||||||
if(firegex.ph_delete_service(service_id)):
|
if firegex.ph_delete_service(service_id):
|
||||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||||
@@ -44,18 +80,25 @@ def exit_test(code):
|
|||||||
|
|
||||||
srvs = firegex.ph_get_services()
|
srvs = firegex.ph_get_services()
|
||||||
for ele in srvs:
|
for ele in srvs:
|
||||||
if ele['name'] == args.service_name:
|
if ele["name"] == args.service_name:
|
||||||
firegex.ph_delete_service(ele['service_id'])
|
firegex.ph_delete_service(ele["service_id"])
|
||||||
|
|
||||||
# Create and start serivce
|
# 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")
|
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:
|
if service_id:
|
||||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if(firegex.ph_start_service(service_id)):
|
if firegex.ph_start_service(service_id):
|
||||||
puts("Sucessfully started service ✔", color=colors.green)
|
puts("Sucessfully started service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||||
@@ -87,7 +130,7 @@ def checkData(should_work):
|
|||||||
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)
|
||||||
@@ -96,7 +139,7 @@ else:
|
|||||||
checkData(False)
|
checkData(False)
|
||||||
|
|
||||||
# Start firewall
|
# Start firewall
|
||||||
if(firegex.ph_start_service(service_id)):
|
if firegex.ph_start_service(service_id):
|
||||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||||
@@ -105,7 +148,9 @@ else:
|
|||||||
checkData(True)
|
checkData(True)
|
||||||
|
|
||||||
# Change port
|
# Change port
|
||||||
if(firegex.ph_change_destination(service_id, "::1" if args.ipv6 else "127.0.0.1", args.port+2)):
|
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)
|
puts("Sucessfully changed port ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
|
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
|
||||||
@@ -114,15 +159,19 @@ else:
|
|||||||
checkData(False)
|
checkData(False)
|
||||||
|
|
||||||
server.stop()
|
server.stop()
|
||||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+2,ipv6=args.ipv6,proxy_port=args.port)
|
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||||
|
args.port + 2, ipv6=args.ipv6, proxy_port=args.port
|
||||||
|
)
|
||||||
server.start()
|
server.start()
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
checkData(True)
|
checkData(True)
|
||||||
|
|
||||||
# Rename service
|
# Rename service
|
||||||
if(firegex.ph_rename_service(service_id,f"{args.service_name}2")):
|
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)
|
puts(
|
||||||
|
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,44 @@
|
|||||||
|
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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,35 +1,94 @@
|
|||||||
from multiprocessing import Process
|
from multiprocessing import Process, Queue
|
||||||
import socket
|
import socket
|
||||||
|
import queue
|
||||||
|
import traceback
|
||||||
|
|
||||||
class UdpServer:
|
|
||||||
def __init__(self,port,ipv6, proxy_port = None):
|
def _start_udp_server(port, server_queue: Queue, ipv6, verbose):
|
||||||
def _startServer(port):
|
|
||||||
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
|
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.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sock.bind(('::1' if ipv6 else '127.0.0.1', port))
|
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
||||||
while True:
|
while True:
|
||||||
bytesAddressPair = sock.recvfrom(432)
|
try:
|
||||||
|
bytesAddressPair = sock.recvfrom(4096)
|
||||||
message = bytesAddressPair[0]
|
message = bytesAddressPair[0]
|
||||||
address = bytesAddressPair[1]
|
address = bytesAddressPair[1]
|
||||||
sock.sendto(message, address)
|
|
||||||
|
|
||||||
self.ipv6 = ipv6
|
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:
|
||||||
|
def __init__(self, port, ipv6, proxy_port=None, verbose=False):
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user