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
# 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,26 @@ 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 nftables \
libhs5 libtins4.4 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 libtins-dev libpcap-dev libboost-dev pkg-config && \
apt-get clean && rm -rf /var/lib/apt/lists/*
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,10 +39,11 @@ 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/

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,6 +86,16 @@ 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"))

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

@@ -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():
@@ -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}")

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 NFProxy from './pages/NFProxy';
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
import TrafficViewer from './pages/NFProxy/TrafficViewer';
import TrafficViewerMain from './pages/TrafficViewer';
import { useAuthStore } from './js/store';
function App() {
@@ -172,7 +174,9 @@ 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="*" element={<HomeRedirector />} />

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 } 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,7 @@ 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} />} />
{/* <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,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": {
"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}",