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