Compare commits

...

6 Commits

Author SHA1 Message Date
Ilya Starchak
c387d9b40b sd 2025-12-10 02:30:30 +03:00
Ilya Starchak
d8061985d6 asdas 2025-12-10 02:27:35 +03:00
Ilya Starchak
c237112077 sd 2025-12-10 02:17:54 +03:00
Your Name
811773e009 dsa 2025-12-08 02:08:30 +03:00
Your Name
f1ebada95d asd 2025-12-08 01:55:16 +03:00
Your Name
9af3023a37 dsa 2025-12-08 01:41:08 +03:00
56 changed files with 5424 additions and 4025 deletions

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,37 @@ All the configuration at the startup is customizable in [firegex.py](./run.py) o
- Create basic firewall rules to allow and deny specific traffic, like ufw or iptables but using firegex graphic interface (by using [nftable](https://netfilter.org/projects/nftables/))
- 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

View File

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

View File

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

View File

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

View File

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

View File

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

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

116
docs/TRAFFIC_VIEWER.md Normal file
View File

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

View File

@@ -10,7 +10,7 @@ from firegex.nfproxy.internals.exceptions import (
from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
from dataclasses import dataclass, field
from collections import deque
from compression import zstd
import zstandard as zstd
import gzip
import io
import zlib

View File

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

View File

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

View File

@@ -72,7 +72,7 @@ export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h
│ * port INTEGER The port of the target to proxy [default: None] [required] │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ 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 │

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
run.py
View File

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

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