dsa
This commit is contained in:
78
.github/workflows/pypi-publish-fgex.yml
vendored
78
.github/workflows/pypi-publish-fgex.yml
vendored
@@ -1,46 +1,46 @@
|
|||||||
# This workflow will upload a Python Package using Twine when a release is created
|
# # This workflow will upload a Python Package using Twine when a release is created
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
# # This workflow uses actions that are not certified by GitHub.
|
||||||
# They are provided by a third-party and are governed by
|
# # They are provided by a third-party and are governed by
|
||||||
# separate terms of service, privacy policy, and support
|
# # separate terms of service, privacy policy, and support
|
||||||
# documentation.
|
# # documentation.
|
||||||
|
|
||||||
name: Upload Python Package (fgex alias)
|
# name: Upload Python Package (fgex alias)
|
||||||
|
|
||||||
on:
|
# on:
|
||||||
release:
|
# release:
|
||||||
types:
|
# types:
|
||||||
- published
|
# - published
|
||||||
|
|
||||||
permissions:
|
# permissions:
|
||||||
contents: read
|
# contents: read
|
||||||
|
|
||||||
jobs:
|
# jobs:
|
||||||
deploy:
|
# deploy:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
# - name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
# uses: actions/setup-python@v5
|
||||||
with:
|
# with:
|
||||||
python-version: '3.x'
|
# python-version: '3.x'
|
||||||
- name: Install dependencies
|
# - name: Install dependencies
|
||||||
run: |
|
# run: |
|
||||||
python -m pip install --upgrade pip
|
# python -m pip install --upgrade pip
|
||||||
pip install build
|
# pip install build
|
||||||
- name: Extract tag name
|
# - name: Extract tag name
|
||||||
id: tag
|
# id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
- name: Update version in setup.py
|
# - name: Update version in setup.py
|
||||||
run: >-
|
# run: >-
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||||
- name: Build package
|
# - name: Build package
|
||||||
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
# run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||||
- name: Publish package
|
# - name: Publish package
|
||||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||||
with:
|
# with:
|
||||||
user: __token__
|
# user: __token__
|
||||||
password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
# password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||||
|
|||||||
80
.github/workflows/pypi-publish.yml
vendored
80
.github/workflows/pypi-publish.yml
vendored
@@ -1,47 +1,47 @@
|
|||||||
# This workflow will upload a Python Package using Twine when a release is created
|
# # This workflow will upload a Python Package using Twine when a release is created
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
# # This workflow uses actions that are not certified by GitHub.
|
||||||
# They are provided by a third-party and are governed by
|
# # They are provided by a third-party and are governed by
|
||||||
# separate terms of service, privacy policy, and support
|
# # separate terms of service, privacy policy, and support
|
||||||
# documentation.
|
# # documentation.
|
||||||
|
|
||||||
name: Upload Python Package
|
# name: Upload Python Package
|
||||||
|
|
||||||
on:
|
# on:
|
||||||
release:
|
# release:
|
||||||
types:
|
# types:
|
||||||
- published
|
# - published
|
||||||
|
|
||||||
permissions:
|
# permissions:
|
||||||
contents: read
|
# contents: read
|
||||||
|
|
||||||
jobs:
|
# jobs:
|
||||||
deploy:
|
# deploy:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
# - name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
# uses: actions/setup-python@v5
|
||||||
with:
|
# with:
|
||||||
python-version: '3.x'
|
# python-version: '3.x'
|
||||||
- name: Install dependencies
|
# - name: Install dependencies
|
||||||
run: |
|
# run: |
|
||||||
python -m pip install --upgrade pip
|
# python -m pip install --upgrade pip
|
||||||
pip install build
|
# pip install build
|
||||||
- name: Extract tag name
|
# - name: Extract tag name
|
||||||
id: tag
|
# id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
- name: Update version in setup.py
|
# - name: Update version in setup.py
|
||||||
run: >-
|
# run: >-
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||||
- name: Build package
|
# - name: Build package
|
||||||
run: cd fgex-lib && python -m build && mv ./dist ../
|
# run: cd fgex-lib && python -m build && mv ./dist ../
|
||||||
- name: Publish package
|
# - name: Publish package
|
||||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||||
with:
|
# with:
|
||||||
user: __token__
|
# user: __token__
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
# password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
|||||||
29
Dockerfile
29
Dockerfile
@@ -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/
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
116
docs/TRAFFIC_VIEWER.md
Normal 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
|
||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ export const nfproxy = {
|
|||||||
setpyfilterscode: async (service_id:string, code:string) => {
|
setpyfilterscode: async (service_id:string, code:string) => {
|
||||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
|
},
|
||||||
|
gettraffic: async (service_id:string, limit:number = 500) => {
|
||||||
|
return await getapi(`nfproxy/services/${service_id}/traffic?limit=${limit}`) as { events: any[], count: number };
|
||||||
|
},
|
||||||
|
cleartraffic: async (service_id:string) => {
|
||||||
|
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
|
||||||
|
return status === "ok"?undefined:status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { PiWallLight } from "react-icons/pi";
|
|||||||
import { useNavbarStore } from "../../js/store";
|
import { useNavbarStore } from "../../js/store";
|
||||||
import { getMainPath } from "../../js/utils";
|
import { getMainPath } from "../../js/utils";
|
||||||
import { BsRegex } from "react-icons/bs";
|
import { BsRegex } from "react-icons/bs";
|
||||||
|
import { MdVisibility } 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>
|
||||||
|
|||||||
@@ -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" />}
|
||||||
|
|||||||
239
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
239
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal 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>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
138
frontend/src/pages/TrafficViewer/index.tsx
Normal file
138
frontend/src/pages/TrafficViewer/index.tsx
Normal 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
2
run.py
@@ -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}",
|
||||||
|
|||||||
Reference in New Issue
Block a user