Compare commits
6 Commits
16f96aa6f6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c387d9b40b | ||
|
|
d8061985d6 | ||
|
|
c237112077 | ||
|
|
811773e009 | ||
|
|
f1ebada95d | ||
|
|
9af3023a37 |
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
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
# # This workflow will upload a Python Package using Twine when a release is created
|
||||
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
name: Upload Python Package (fgex alias)
|
||||
# name: Upload Python Package (fgex alias)
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# run: >-
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# 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
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
# # This workflow will upload a Python Package using Twine when a release is created
|
||||
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
name: Upload Python Package
|
||||
# name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# run: >-
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
40
Dockerfile
40
Dockerfile
@@ -12,24 +12,34 @@ RUN bun i
|
||||
COPY ./frontend/ .
|
||||
RUN bun run build
|
||||
|
||||
# Base fedora container
|
||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
||||
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
||||
libnfnetlink libmnl libcap-ng-utils nftables \
|
||||
vectorscan libtins python3-nftables libpcap && dnf clean all
|
||||
# Base Ubuntu container
|
||||
FROM --platform=$TARGETARCH ubuntu:24.04 AS base
|
||||
RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
|
||||
libnfnetlink0 libmnl0 libcap-ng-utils libcap2-bin nftables \
|
||||
libhyperscan5 python3-nftables libpcap0.8 && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /execute/modules
|
||||
WORKDIR /execute
|
||||
|
||||
FROM --platform=$TARGETARCH base AS compiler
|
||||
|
||||
RUN dnf -y update && dnf install -y python3.14-devel @development-tools gcc-c++ \
|
||||
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel \
|
||||
vectorscan-devel libtins-devel libpcap-devel boost-devel
|
||||
RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
|
||||
libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
|
||||
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
|
||||
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.14 -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
|
||||
FROM --platform=$TARGETARCH base AS final
|
||||
@@ -37,13 +47,17 @@ FROM --platform=$TARGETARCH base AS final
|
||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||
COPY ./fgex-lib /execute/fgex-lib
|
||||
|
||||
RUN dnf -y update && dnf install -y gcc-c++ python3.14-devel uv git &&\
|
||||
uv pip install --no-cache --system ./fgex-lib &&\
|
||||
uv pip install --no-cache --system -r /execute/requirements.txt &&\
|
||||
uv cache clean && dnf remove -y gcc-c++ python3.14-devel uv git && dnf clean all
|
||||
RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
|
||||
pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
|
||||
pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
|
||||
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 --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||
COPY --from=compiler /usr/local/lib/libtins* /usr/local/lib/
|
||||
COPY --from=frontend /app/dist/ ./frontend/
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
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/))
|
||||
- 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).
|
||||
- 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
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
import traceback
|
||||
from fastapi import HTTPException
|
||||
import time
|
||||
import json
|
||||
from utils import run_func
|
||||
from utils import DEBUG
|
||||
from utils import nicenessify
|
||||
@@ -35,11 +36,12 @@ class FiregexInterceptor:
|
||||
self.last_time_exception = 0
|
||||
self.outstrem_function = None
|
||||
self.expection_function = None
|
||||
self.traffic_function = None
|
||||
self.outstrem_task: asyncio.Task
|
||||
self.outstrem_buffer = ""
|
||||
|
||||
@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.srv = srv
|
||||
self.filter_map_lock = asyncio.Lock()
|
||||
@@ -47,6 +49,7 @@ class FiregexInterceptor:
|
||||
self.sock_conn_lock = asyncio.Lock()
|
||||
self.outstrem_function = outstream_func
|
||||
self.expection_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
if not self.sock_conn_lock.locked():
|
||||
await self.sock_conn_lock.acquire()
|
||||
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"
|
||||
if self.outstrem_function:
|
||||
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):
|
||||
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(
|
||||
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
@@ -93,7 +108,9 @@ class FiregexInterceptor:
|
||||
env={
|
||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||
"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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from modules.nfproxy.firegex import FiregexInterceptor
|
||||
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
||||
from modules.nfproxy.models import Service, PyFilter
|
||||
@@ -12,7 +13,7 @@ class STATUS:
|
||||
nft = FiregexTables()
|
||||
|
||||
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.db = db
|
||||
self.status = STATUS.STOP
|
||||
@@ -21,11 +22,17 @@ class ServiceManager:
|
||||
self.interceptor = None
|
||||
self.outstream_function = outstream_func
|
||||
self.last_exception_time = 0
|
||||
self.traffic_events = deque(maxlen=500) # Ring buffer for traffic viewer
|
||||
async def excep_internal_handler(srv, exc_time):
|
||||
self.last_exception_time = exc_time
|
||||
if exception_func:
|
||||
await run_func(exception_func, srv, exc_time)
|
||||
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):
|
||||
pyfilters = [
|
||||
@@ -69,7 +76,7 @@ class ServiceManager:
|
||||
async def start(self):
|
||||
if not self.interceptor:
|
||||
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()
|
||||
self._set_status(STATUS.ACTIVE)
|
||||
|
||||
@@ -88,13 +95,23 @@ class ServiceManager:
|
||||
async with self.lock:
|
||||
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:
|
||||
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.service_table: dict[str, ServiceManager] = {}
|
||||
self.lock = asyncio.Lock()
|
||||
self.outstream_function = outstream_func
|
||||
self.exception_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
|
||||
async def close(self):
|
||||
for key in list(self.service_table.keys()):
|
||||
@@ -116,7 +133,7 @@ class FirewallManager:
|
||||
srv = Service.from_dict(srv)
|
||||
if srv.id in self.service_table:
|
||||
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)
|
||||
|
||||
def get(self,srv_id) -> ServiceManager:
|
||||
|
||||
@@ -6,6 +6,8 @@ def convert_protocol_to_l4(proto:str):
|
||||
return "tcp"
|
||||
elif proto == "http":
|
||||
return "tcp"
|
||||
elif proto == "udp":
|
||||
return "udp"
|
||||
else:
|
||||
raise Exception("Invalid protocol")
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ psutil
|
||||
python-jose[cryptography]
|
||||
python-socketio
|
||||
brotli
|
||||
zstandard
|
||||
#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',
|
||||
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
||||
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))',
|
||||
'proto': 'VARCHAR(4) NOT NULL CHECK (proto IN ("tcp", "http", "udp"))',
|
||||
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||
'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-exception-join", join_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():
|
||||
db.backup()
|
||||
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
|
||||
async def exception_func(service_id, timestamp):
|
||||
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])
|
||||
async def get_service_list():
|
||||
@@ -300,7 +305,7 @@ async def add_new_service(form: ServiceAddForm):
|
||||
form.ip_int = ip_parse(form.ip_int)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid address")
|
||||
if form.proto not in ["tcp", "http"]:
|
||||
if form.proto not in ["tcp", "http", "udp"]:
|
||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||
srv_id = None
|
||||
try:
|
||||
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
|
||||
except FileNotFoundError:
|
||||
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
|
||||
async def join_outstream(sid, data):
|
||||
"""Client joins a room."""
|
||||
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
|
||||
if 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 dataclasses import dataclass, field
|
||||
from collections import deque
|
||||
from compression import zstd
|
||||
import zstandard as zstd
|
||||
import gzip
|
||||
import io
|
||||
import zlib
|
||||
|
||||
@@ -13,6 +13,9 @@ import { Firewall } from './pages/Firewall';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import NFProxy from './pages/NFProxy';
|
||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
||||
import TrafficViewerMain from './pages/TrafficViewer';
|
||||
import SetupPage from './pages/Setup';
|
||||
import { useAuthStore } from './js/store';
|
||||
|
||||
function App() {
|
||||
@@ -172,9 +175,12 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
||||
</Route>
|
||||
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
||||
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
||||
<Route path=":srv/traffic" element={<TrafficViewer />} />
|
||||
</Route>
|
||||
<Route path="traffic" element={<TrafficViewerMain />} />
|
||||
<Route path="firewall" element={<Firewall />} />
|
||||
<Route path="porthijack" element={<PortHijack />} />
|
||||
<Route path="setup" element={<SetupPage />} />
|
||||
<Route path="*" element={<HomeRedirector />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -26,7 +26,7 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
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",
|
||||
}
|
||||
})
|
||||
@@ -115,6 +115,7 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...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] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─ 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-port INTEGER The port of the local server [default: 7474] │
|
||||
│ -6 Use IPv6 for the connection │
|
||||
|
||||
@@ -94,6 +94,13 @@ export const nfproxy = {
|
||||
setpyfilterscode: async (service_id:string, code:string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||
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 { getMainPath } from "../../js/utils";
|
||||
import { BsRegex } from "react-icons/bs";
|
||||
import { MdVisibility, MdSettings } from "react-icons/md";
|
||||
|
||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||
@@ -40,6 +41,8 @@ export default function NavBar() {
|
||||
<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="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>
|
||||
</Box>
|
||||
|
||||
@@ -151,6 +151,12 @@ export default function ServiceDetailsNFProxy() {
|
||||
<FiFileText size="20px" />
|
||||
</ActionIcon>
|
||||
</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>
|
||||
{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>
|
||||
)}
|
||||
</>;
|
||||
}
|
||||
2
run.py
2
run.py
@@ -268,7 +268,7 @@ def write_compose(skip_password = True):
|
||||
"firewall": {
|
||||
"restart": "unless-stopped",
|
||||
"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",
|
||||
"environment": [
|
||||
f"PORT={args.port}",
|
||||
|
||||
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": []
|
||||
}
|
||||
Reference in New Issue
Block a user