This commit is contained in:
Your Name
2025-12-08 01:41:08 +03:00
parent 16f96aa6f6
commit 9af3023a37
49 changed files with 4609 additions and 4020 deletions

View File

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

View File

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

View File

@@ -12,24 +12,26 @@ RUN bun i
COPY ./frontend/ . COPY ./frontend/ .
RUN bun run build RUN bun run build
# Base fedora container # Base Ubuntu container
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base FROM --platform=$TARGETARCH ubuntu:24.04 AS base
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \ RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
libnfnetlink libmnl libcap-ng-utils nftables \ libnfnetlink0 libmnl0 libcap-ng-utils nftables \
vectorscan libtins python3-nftables libpcap && dnf clean all libhs5 libtins4.4 python3-nftables libpcap0.8 && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /execute/modules RUN mkdir -p /execute/modules
WORKDIR /execute WORKDIR /execute
FROM --platform=$TARGETARCH base AS compiler FROM --platform=$TARGETARCH base AS compiler
RUN dnf -y update && dnf install -y python3.14-devel @development-tools gcc-c++ \ RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel \ libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
vectorscan-devel libtins-devel libpcap-devel boost-devel libhyperscan-dev libtins-dev libpcap-dev libboost-dev pkg-config && \
apt-get clean && rm -rf /var/lib/apt/lists/*
COPY ./backend/binsrc /execute/binsrc COPY ./backend/binsrc /execute/binsrc
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.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 #Building main conteiner
FROM --platform=$TARGETARCH base AS final FROM --platform=$TARGETARCH base AS final
@@ -37,10 +39,11 @@ FROM --platform=$TARGETARCH base AS final
COPY ./backend/requirements.txt /execute/requirements.txt COPY ./backend/requirements.txt /execute/requirements.txt
COPY ./fgex-lib /execute/fgex-lib COPY ./fgex-lib /execute/fgex-lib
RUN dnf -y update && dnf install -y gcc-c++ python3.14-devel uv git &&\ RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
uv pip install --no-cache --system ./fgex-lib &&\ pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
uv pip install --no-cache --system -r /execute/requirements.txt &&\ pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
uv cache clean && dnf remove -y gcc-c++ python3.14-devel uv git && dnf clean all apt-get remove -y g++ python3-dev git && \
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
COPY ./backend/ /execute/ COPY ./backend/ /execute/
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/ COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/

View File

@@ -5,6 +5,7 @@ import asyncio
import traceback import traceback
from fastapi import HTTPException from fastapi import HTTPException
import time import time
import json
from utils import run_func from utils import run_func
from utils import DEBUG from utils import DEBUG
from utils import nicenessify from utils import nicenessify
@@ -35,11 +36,12 @@ class FiregexInterceptor:
self.last_time_exception = 0 self.last_time_exception = 0
self.outstrem_function = None self.outstrem_function = None
self.expection_function = None self.expection_function = None
self.traffic_function = None
self.outstrem_task: asyncio.Task self.outstrem_task: asyncio.Task
self.outstrem_buffer = "" self.outstrem_buffer = ""
@classmethod @classmethod
async def start(cls, srv: Service, outstream_func=None, exception_func=None): async def start(cls, srv: Service, outstream_func=None, exception_func=None, traffic_func=None):
self = cls() self = cls()
self.srv = srv self.srv = srv
self.filter_map_lock = asyncio.Lock() self.filter_map_lock = asyncio.Lock()
@@ -47,6 +49,7 @@ class FiregexInterceptor:
self.sock_conn_lock = asyncio.Lock() self.sock_conn_lock = asyncio.Lock()
self.outstrem_function = outstream_func self.outstrem_function = outstream_func
self.expection_function = exception_func self.expection_function = exception_func
self.traffic_function = traffic_func
if not self.sock_conn_lock.locked(): if not self.sock_conn_lock.locked():
await self.sock_conn_lock.acquire() await self.sock_conn_lock.acquire()
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock" self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
@@ -83,6 +86,16 @@ class FiregexInterceptor:
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n" self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
if self.outstrem_function: if self.outstrem_function:
await run_func(self.outstrem_function, self.srv.id, out_data) await run_func(self.outstrem_function, self.srv.id, out_data)
# Parse JSON traffic events (if binary emits them)
if self.traffic_function:
for line in out_data.splitlines():
if line.startswith("{"): # JSON event from binary
try:
event = json.loads(line)
if "ts" in event and "verdict" in event: # Basic validation
await run_func(self.traffic_function, self.srv.id, event)
except (json.JSONDecodeError, KeyError):
pass # Ignore malformed JSON, keep backward compat with raw logs
async def _start_binary(self): async def _start_binary(self):
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy")) proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))

View File

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

View File

@@ -113,6 +113,8 @@ async def startup():
utils.socketio.on("nfproxy-outstream-leave", leave_outstream) utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
utils.socketio.on("nfproxy-exception-join", join_exception) utils.socketio.on("nfproxy-exception-join", join_exception)
utils.socketio.on("nfproxy-exception-leave", leave_exception) utils.socketio.on("nfproxy-exception-leave", leave_exception)
utils.socketio.on("nfproxy-traffic-join", join_traffic)
utils.socketio.on("nfproxy-traffic-leave", leave_traffic)
async def shutdown(): async def shutdown():
db.backup() db.backup()
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
async def exception_func(service_id, timestamp): async def exception_func(service_id, timestamp):
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}") await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func) async def traffic_func(service_id, event):
await utils.socketio.emit(f"nfproxy-traffic-{service_id}", event, room=f"nfproxy-traffic-{service_id}")
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func, traffic_func=traffic_func)
@app.get('/services', response_model=list[ServiceModel]) @app.get('/services', response_model=list[ServiceModel])
async def get_service_list(): async def get_service_list():
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
except FileNotFoundError: except FileNotFoundError:
return "" return ""
@app.get('/services/{service_id}/traffic')
async def get_traffic_events(service_id: str, limit: int = 500):
"""Get recent traffic events from the service ring buffer"""
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
raise HTTPException(status_code=400, detail="This service does not exists!")
try:
events = firewall.get(service_id).get_traffic_events(limit)
return {"events": events, "count": len(events)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post('/services/{service_id}/traffic/clear', response_model=StatusMessageModel)
async def clear_traffic_events(service_id: str):
"""Clear traffic event history for a service"""
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
raise HTTPException(status_code=400, detail="This service does not exists!")
try:
firewall.get(service_id).clear_traffic_events()
return {"status": "ok"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
#Socket io events #Socket io events
async def join_outstream(sid, data): async def join_outstream(sid, data):
"""Client joins a room.""" """Client joins a room."""
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
if srv: if srv:
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}") await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
async def join_traffic(sid, data):
"""Client joins traffic viewer room and gets initial event history."""
srv = data.get("service")
if srv:
room = f"nfproxy-traffic-{srv}"
await utils.socketio.enter_room(sid, room)
try:
events = firewall.get(srv).get_traffic_events(500)
await utils.socketio.emit("nfproxy-traffic-history", {"events": events}, room=sid)
except Exception:
pass # Service may not exist or not started
async def leave_traffic(sid, data):
"""Client leaves traffic viewer room."""
srv = data.get("service")
if srv:
await utils.socketio.leave_room(sid, f"nfproxy-traffic-{srv}")

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

@@ -13,6 +13,8 @@ import { Firewall } from './pages/Firewall';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import NFProxy from './pages/NFProxy'; import NFProxy from './pages/NFProxy';
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails'; import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
import TrafficViewer from './pages/NFProxy/TrafficViewer';
import TrafficViewerMain from './pages/TrafficViewer';
import { useAuthStore } from './js/store'; import { useAuthStore } from './js/store';
function App() { function App() {
@@ -172,7 +174,9 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
</Route> </Route>
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} > <Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
<Route path=":srv" element={<ServiceDetailsNFProxy />} /> <Route path=":srv" element={<ServiceDetailsNFProxy />} />
<Route path=":srv/traffic" element={<TrafficViewer />} />
</Route> </Route>
<Route path="traffic" element={<TrafficViewerMain />} />
<Route path="firewall" element={<Firewall />} /> <Route path="firewall" element={<Firewall />} />
<Route path="porthijack" element={<PortHijack />} /> <Route path="porthijack" element={<PortHijack />} />
<Route path="*" element={<HomeRedirector />} /> <Route path="*" element={<HomeRedirector />} />

View File

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

View File

@@ -7,6 +7,7 @@ import { PiWallLight } from "react-icons/pi";
import { useNavbarStore } from "../../js/store"; import { useNavbarStore } from "../../js/store";
import { getMainPath } from "../../js/utils"; import { getMainPath } from "../../js/utils";
import { BsRegex } from "react-icons/bs"; import { BsRegex } from "react-icons/bs";
import { MdVisibility } from "react-icons/md";
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }: function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) { { navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
@@ -40,6 +41,7 @@ export default function NavBar() {
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} /> <NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} /> <NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected 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} />} />
{/* <Box px="xs" mt="lg"> {/* <Box px="xs" mt="lg">
<Title order={5}>Experimental Features 🧪</Title> <Title order={5}>Experimental Features 🧪</Title>
</Box> </Box>

View File

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

View File

@@ -0,0 +1,239 @@
import { ActionIcon, Badge, Box, Code, Divider, Grid, LoadingOverlay, Modal, ScrollArea, 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 [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) => {
if (!filterText) return true;
const search = filterText.toLowerCase();
return (
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)
);
});
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">
<TextInput
placeholder="Filter by IP, verdict, filter name, or protocol..."
value={filterText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
leftSection={<FaFilter />}
mb="md"
/>
<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,138 @@
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Space, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useNavigate } from 'react-router';
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { isMediumScreen } from '../../js/utils';
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
import { TbPlugConnected } from 'react-icons/tb';
import { FaServer } from 'react-icons/fa';
export default function TrafficViewer() {
const services = nfproxyServiceQuery();
const navigate = useNavigate();
const isMedium = isMediumScreen();
if (services.isLoading) return <LoadingOverlay visible={true} />;
const activeServices = services.data?.filter(s => s.status === 'active') || [];
const stoppedServices = services.data?.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 all NFProxy services
</Text>
</Box>
<Divider my="lg" />
{services.data?.length === 0 ? (
<Box px="md">
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
No NFProxy services found
</Title>
<Space h="xs" />
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
Create a service in the Netfilter Proxy section to start monitoring traffic
</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.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md">
<Group justify="space-between">
<Box>
<Group>
<ThemeIcon color="lime" variant="light" size="lg">
<TbPlugConnected size={20} />
</ThemeIcon>
<div>
<Text fw={700} size="lg">{service.name}</Text>
<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' }}>
<Badge color="orange" size="sm" mb={4}>
{service.edited_packets} edited
</Badge>
<br />
<Badge color="yellow" size="sm">
{service.blocked_packets} blocked
</Badge>
</Box>
<Tooltip label="View traffic">
<ActionIcon
color="cyan"
size="xl"
radius="md"
variant="filled"
onClick={() => navigate(`/nfproxy/${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="gray" variant="light" size="lg">
<FaServer size={18} />
</ThemeIcon>
<div>
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
<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": { "firewall": {
"restart": "unless-stopped", "restart": "unless-stopped",
"container_name": "firegex", "container_name": "firegex",
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}", "build" if g.build else "image": "." if g.build else f"ghcr.io/ilyastar9999/firegex:{args.version}",
"network_mode": "host", "network_mode": "host",
"environment": [ "environment": [
f"PORT={args.port}", f"PORT={args.port}",