From 9af3023a37619f78145cea51542f91b7d9efda00 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Dec 2025 01:41:08 +0300 Subject: [PATCH] dsa --- .github/workflows/docker-image.yml | 420 +++++++-------- .github/workflows/pypi-publish-fgex.yml | 92 ++-- .github/workflows/pypi-publish.yml | 94 ++-- Dockerfile | 101 ++-- backend/modules/nfproxy/firegex.py | 15 +- backend/modules/nfproxy/firewall.py | 25 +- backend/requirements.txt | 16 +- backend/routers/nfproxy.py | 46 +- backend/utils/__init__.py | 442 ++++++++-------- docs/TRAFFIC_VIEWER.md | 116 +++++ frontend/src/App.tsx | 4 + frontend/src/components/AddNewRegex.tsx | 230 ++++----- frontend/src/components/Header/index.tsx | 164 +++--- frontend/src/components/MainLayout.tsx | 102 ++-- .../src/components/NFProxy/AddEditService.tsx | 278 +++++----- .../components/NFProxy/ServiceRow/index.tsx | 328 ++++++------ frontend/src/components/NFProxy/utils.ts | 357 ++++++------- .../src/components/NFRegex/AddEditService.tsx | 278 +++++----- .../components/NFRegex/ServiceRow/index.tsx | 316 ++++++------ frontend/src/components/NFRegex/utils.ts | 188 +++---- frontend/src/components/NavBar/index.tsx | 2 + .../components/PortHijack/AddNewService.tsx | 226 ++++----- .../PortHijack/ServiceRow/index.tsx | 304 +++++------ frontend/src/components/PortHijack/utils.ts | 126 ++--- .../src/components/PyFilterView/index.tsx | 88 ++-- frontend/src/components/RegexView/index.tsx | 170 +++---- frontend/src/components/YesNoModal.tsx | 36 +- frontend/src/js/utils.tsx | 452 ++++++++--------- frontend/src/pages/NFProxy/ServiceDetails.tsx | 480 +++++++++--------- frontend/src/pages/NFProxy/TrafficViewer.tsx | 239 +++++++++ frontend/src/pages/NFProxy/index.tsx | 344 ++++++------- frontend/src/pages/NFRegex/ServiceDetails.tsx | 388 +++++++------- frontend/src/pages/NFRegex/index.tsx | 200 ++++---- frontend/src/pages/PortHijack/index.tsx | 154 +++--- frontend/src/pages/TrafficViewer/index.tsx | 138 +++++ frontend/tsconfig.json | 44 +- run.py | 2 +- tests/results/2.3.3-1T.csv | 102 ++-- tests/results/2.3.3-8T.csv | 102 ++-- tests/results/2.4.0-1T.csv | 102 ++-- tests/results/2.4.0-8T.csv | 102 ++-- tests/results/2.5.1-1T-withload.csv | 102 ++-- tests/results/2.5.1-1T.csv | 102 ++-- tests/results/2.5.1-8T-withload.csv | 102 ++-- tests/results/2.5.1-8T.csv | 102 ++-- tests/results/comparemark_nfproxy_1T.csv | 202 ++++---- tests/results/comparemark_nfproxy_8T.csv | 202 ++++---- tests/results/comparemark_nfregex_1T.csv | 202 ++++---- tests/results/comparemark_nfregex_8T.csv | 202 ++++---- 49 files changed, 4609 insertions(+), 4020 deletions(-) create mode 100644 docs/TRAFFIC_VIEWER.md create mode 100644 frontend/src/pages/NFProxy/TrafficViewer.tsx create mode 100644 frontend/src/pages/TrafficViewer/index.tsx diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 86ee84a..4e2b0dd 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,210 +1,210 @@ -name: Create and publish Docker images - -on: - release: - types: - - published - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - docker_build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: ubuntu-latest - arch: amd64 - run_tests: true - - os: ubuntu-24.04-arm - arch: arm64 - run_tests: true - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Convert repository name to lowercase - id: lowercase - run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT - - - name: Build and run firegex - if: matrix.run_tests - run: python3 run.py start -P testpassword - - - name: Run tests - if: matrix.run_tests - run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@master - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }} - - - 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" backend/utils/__init__.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; - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - builder: ${{ steps.buildx.outputs.name }} - platforms: linux/${{ matrix.arch }} - push: true - tags: | - ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }} - ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ matrix.arch }} - provenance: false - sbom: false - - docker_manifest: - needs: docker_build - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Convert repository name to lowercase - id: lowercase - run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract tag name - id: tag - run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT - - - name: Create and push multi-platform manifest - run: | - # Create manifest list for specific tag - docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ - --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \ - --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 - - # Annotate the manifest with architecture info - docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ - ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \ - --arch amd64 --os linux - - docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ - ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \ - --arch arm64 --os linux - - docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} - - # Create manifest list for latest tag - docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ - --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \ - --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 - - # Annotate the latest manifest with architecture info - docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ - ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \ - --arch amd64 --os linux - - docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ - ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \ - --arch arm64 --os linux - - docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest - - create-rootfs-assets: - runs-on: ubuntu-latest - needs: [docker_manifest] - permissions: - contents: write - packages: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Convert repository name to lowercase - id: lowercase - run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT - - - name: Set up QEMU - uses: docker/setup-qemu-action@master - with: - platforms: all - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@master - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get latest release tag - id: get_tag - run: | - LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name') - echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT - echo "Latest release tag: $LATEST_TAG" - - - name: Export rootfs for amd64 - run: | - echo "Creating and exporting amd64 container..." - CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }}) - docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar" - docker rm $CONTAINER_ID - echo "Compressing amd64 rootfs..." - gzip firegex-rootfs-amd64.tar - ls -lh firegex-rootfs-amd64.tar.gz - - - name: Export rootfs for arm64 - run: | - echo "Creating and exporting arm64 container..." - CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }}) - docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar" - docker rm $CONTAINER_ID - echo "Compressing arm64 rootfs..." - gzip firegex-rootfs-arm64.tar - ls -lh firegex-rootfs-arm64.tar.gz - - - name: Upload rootfs assets to release - run: | - echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..." - gh release upload ${{ steps.get_tag.outputs.tag }} \ - firegex-rootfs-amd64.tar.gz \ - firegex-rootfs-arm64.tar.gz \ - --clobber - echo "Assets uploaded successfully!" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +name: Create and publish Docker images + +on: + release: + types: + - published + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + docker_build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + arch: amd64 + run_tests: true + - os: ubuntu-24.04-arm + arch: arm64 + run_tests: true + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Convert repository name to lowercase + id: lowercase + run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Build and run firegex + if: matrix.run_tests + run: python3 run.py start -P testpassword + + - name: Run tests + if: matrix.run_tests + run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@master + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }} + + - 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" backend/utils/__init__.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; + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + builder: ${{ steps.buildx.outputs.name }} + platforms: linux/${{ matrix.arch }} + push: true + tags: | + ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }} + ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=${{ matrix.arch }} + provenance: false + sbom: false + + docker_manifest: + needs: docker_build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Convert repository name to lowercase + id: lowercase + run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract tag name + id: tag + run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT + + - name: Create and push multi-platform manifest + run: | + # Create manifest list for specific tag + docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ + --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \ + --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 + + # Annotate the manifest with architecture info + docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ + ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \ + --arch amd64 --os linux + + docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ + ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \ + --arch arm64 --os linux + + docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} + + # Create manifest list for latest tag + docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ + --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \ + --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 + + # Annotate the latest manifest with architecture info + docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ + ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \ + --arch amd64 --os linux + + docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ + ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \ + --arch arm64 --os linux + + docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest + + create-rootfs-assets: + runs-on: ubuntu-latest + needs: [docker_manifest] + permissions: + contents: write + packages: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Convert repository name to lowercase + id: lowercase + run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Set up QEMU + uses: docker/setup-qemu-action@master + with: + platforms: all + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@master + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get latest release tag + id: get_tag + run: | + LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name') + echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT + echo "Latest release tag: $LATEST_TAG" + + - name: Export rootfs for amd64 + run: | + echo "Creating and exporting amd64 container..." + CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }}) + docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar" + docker rm $CONTAINER_ID + echo "Compressing amd64 rootfs..." + gzip firegex-rootfs-amd64.tar + ls -lh firegex-rootfs-amd64.tar.gz + + - name: Export rootfs for arm64 + run: | + echo "Creating and exporting arm64 container..." + CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }}) + docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar" + docker rm $CONTAINER_ID + echo "Compressing arm64 rootfs..." + gzip firegex-rootfs-arm64.tar + ls -lh firegex-rootfs-arm64.tar.gz + + - name: Upload rootfs assets to release + run: | + echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..." + gh release upload ${{ steps.get_tag.outputs.tag }} \ + firegex-rootfs-amd64.tar.gz \ + firegex-rootfs-arm64.tar.gz \ + --clobber + echo "Assets uploaded successfully!" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pypi-publish-fgex.yml b/.github/workflows/pypi-publish-fgex.yml index 5a6e509..0938a98 100644 --- a/.github/workflows/pypi-publish-fgex.yml +++ b/.github/workflows/pypi-publish-fgex.yml @@ -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 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) - -on: - release: - types: - - published - -permissions: - contents: read - -jobs: - deploy: - - 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 }} +# # 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. + +# name: Upload Python Package (fgex alias) + +# on: +# release: +# types: +# - published + +# permissions: +# contents: read + +# jobs: +# deploy: + +# 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 }} diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 0f03adc..53f06e0 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -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 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 - -on: - release: - types: - - published - -permissions: - contents: read - -jobs: - deploy: - - 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 }} +# # 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. + +# name: Upload Python Package + +# on: +# release: +# types: +# - published + +# permissions: +# contents: read + +# jobs: +# deploy: + +# 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 }} diff --git a/Dockerfile b/Dockerfile index a9474a8..5323efc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,49 +1,52 @@ - -# Firegex Dockerfile UUID signature -# cf1795af-3284-4183-a888-81ad3590ad84 -# Needed for run.py to detect the Dockerfile - - -FROM --platform=$BUILDPLATFORM oven/bun AS frontend -WORKDIR /app -ADD ./frontend/package.json . -ADD ./frontend/bun.lock . -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 - -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 - -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) - -#Building main conteiner -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 - -COPY ./backend/ /execute/ -COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/ -COPY --from=frontend /app/dist/ ./frontend/ - -CMD ["/bin/sh", "/execute/docker-entrypoint.sh"] + +# Firegex Dockerfile UUID signature +# cf1795af-3284-4183-a888-81ad3590ad84 +# Needed for run.py to detect the Dockerfile + + +FROM --platform=$BUILDPLATFORM oven/bun AS frontend +WORKDIR /app +ADD ./frontend/package.json . +ADD ./frontend/bun.lock . +RUN bun i +COPY ./frontend/ . +RUN bun run build + +# 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 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.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3) + +#Building main conteiner +FROM --platform=$TARGETARCH base AS final + +COPY ./backend/requirements.txt /execute/requirements.txt +COPY ./fgex-lib /execute/fgex-lib + +RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \ + pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \ + pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \ + apt-get remove -y g++ python3-dev git && \ + apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY ./backend/ /execute/ +COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/ +COPY --from=frontend /app/dist/ ./frontend/ + +CMD ["/bin/sh", "/execute/docker-entrypoint.sh"] diff --git a/backend/modules/nfproxy/firegex.py b/backend/modules/nfproxy/firegex.py index 9ef3fef..54cd7cf 100644 --- a/backend/modules/nfproxy/firegex.py +++ b/backend/modules/nfproxy/firegex.py @@ -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")) diff --git a/backend/modules/nfproxy/firewall.py b/backend/modules/nfproxy/firewall.py index 045f9ab..80d326a 100644 --- a/backend/modules/nfproxy/firewall.py +++ b/backend/modules/nfproxy/firewall.py @@ -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) @@ -87,14 +94,24 @@ class ServiceManager: async def update_filters(self): 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: diff --git a/backend/requirements.txt b/backend/requirements.txt index 55d6516..26382bf 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,8 +1,8 @@ -fastapi[all] -httpx -uvicorn[standard] -psutil -python-jose[cryptography] -python-socketio -brotli -#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py +fastapi[all] +httpx +uvicorn[standard] +psutil +python-jose[cryptography] +python-socketio +brotli +#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py diff --git a/backend/routers/nfproxy.py b/backend/routers/nfproxy.py index 27335b3..e9a40b3 100644 --- a/backend/routers/nfproxy.py +++ b/backend/routers/nfproxy.py @@ -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}") diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py index 98bcb68..eea2059 100644 --- a/backend/utils/__init__.py +++ b/backend/utils/__init__.py @@ -1,221 +1,221 @@ -import asyncio -from ipaddress import ip_address, ip_interface -import os -import socket -import psutil -import sys -import nftables -from socketio import AsyncServer -from fastapi import Path -from typing import Annotated -from functools import wraps -from pydantic import BaseModel, ValidationError -import traceback -from utils.models import StatusMessageModel -from typing import List - -LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) - -socketio:AsyncServer = None -sid_list:set = set() - -ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -ROUTERS_DIR = os.path.join(ROOT_DIR,"routers") -ON_DOCKER = "DOCKER" in sys.argv -DEBUG = "DEBUG" in sys.argv -NORELOAD = "NORELOAD" in sys.argv -FIREGEX_PORT = int(os.getenv("PORT","4444")) -FIREGEX_HOST = os.getenv("HOST","0.0.0.0") -FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None) -FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None -JWT_ALGORITHM: str = "HS256" -API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" - -PortType = Annotated[int, Path(gt=0, lt=65536)] - -async def run_func(func, *args, **kwargs): - if asyncio.iscoroutinefunction(func): - return await func(*args, **kwargs) - else: - return func(*args, **kwargs) - -async def socketio_emit(elements:list[str]): - await socketio.emit("update",elements) - -def refactor_name(name:str): - name = name.strip() - while " " in name: - name = name.replace(" "," ") - return name - -class SysctlManager: - def __init__(self, ctl_table): - self.old_table = {} - self.new_table = {} - if os.path.isdir("/sys_host/"): - self.old_table = dict() - self.new_table = dict(ctl_table) - for name in ctl_table.keys(): - self.old_table[name] = read_sysctl(name) - - def write_table(self, table) -> bool: - for name, value in table.items(): - if read_sysctl(name) != value: - write_sysctl(name, value) - - def set(self): - self.write_table(self.new_table) - - def reset(self): - self.write_table(self.old_table) - -def read_sysctl(name:str): - with open(f"/sys_host/{name}", "rt") as f: - return "1" in f.read() - -def write_sysctl(name:str, value:bool): - with open(f"/sys_host/{name}", "wt") as f: - f.write("1" if value else "0") - -def list_files(mypath): - from os import listdir - from os.path import isfile, join - return [f for f in listdir(mypath) if isfile(join(mypath, f))] - -def ip_parse(ip:str): - return str(ip_interface(ip).network) - -def is_ip_parse(ip:str): - try: - ip_parse(ip) - return True - except Exception: - return False - -def addr_parse(ip:str): - return str(ip_address(ip)) - -def ip_family(ip:str): - return "ip6" if ip_interface(ip).version == 6 else "ip" - -def get_interfaces(): - def _get_interfaces(): - for int_name, interfs in psutil.net_if_addrs().items(): - for interf in interfs: - if interf.family in [socket.AF_INET, socket.AF_INET6]: - yield {"name": int_name, "addr":interf.address} - return list(_get_interfaces()) - -def nftables_int_to_json(ip_int): - ip_int = ip_parse(ip_int) - ip_addr = str(ip_int).split("/")[0] - ip_addr_cidr = int(str(ip_int).split("/")[1]) - return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}} - -def nftables_json_to_int(ip_json_int): - if isinstance(ip_json_int,str): - return str(ip_parse(ip_json_int)) - else: - return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}' - -class Singleton(object): - __instance = None - def __new__(class_, *args, **kwargs): - if not isinstance(class_.__instance, class_): - class_.__instance = object.__new__(class_, *args, **kwargs) - return class_.__instance - -class NFTableManager(Singleton): - - table_name = "firegex" - - def __init__(self, init_cmd, reset_cmd): - self.__init_cmds = init_cmd - self.__reset_cmds = reset_cmd - self.nft = nftables.Nftables() - - def raw_cmd(self, *cmds): - return self.nft.json_cmd({"nftables": list(cmds)}) - - def cmd(self, *cmds): - code, out, err = self.raw_cmd(*cmds) - if code == 0: - return out - else: - raise Exception(err) - - def init(self): - self.reset() - self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}}) - self.cmd(*self.__init_cmds) - - def reset(self): - self.raw_cmd(*self.__reset_cmds) - - def list_rules(self, tables = None, chains = None): - for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]: - if tables and filter["table"] not in tables: - continue - if chains and filter["chain"] not in chains: - continue - yield filter - - def raw_list(self): - return self.cmd({"list": {"ruleset": None}})["nftables"] - -def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"): - res = obj.model_dump(mode=mode, exclude_unset=not unset) - if convert_keys: - for from_k, to_k in convert_keys.items(): - if from_k in res: - res[to_k] = res.pop(from_k) - if exclude: - for ele in exclude: - if ele in res: - del res[ele] - return res - -def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict: - if isinstance(obj, list): - return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj] - return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) - -def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None): - def decorator(func): - @sio_server.on(event_name) # Automatically registers the event - @wraps(func) - async def wrapper(sid, data): - try: - # Parse and validate incoming data - parsed_data = model.model_validate(data) - except ValidationError: - return json_like(StatusMessageModel(status=f"Invalid {event_name} request")) - - # Call the original function with the parsed data - result = await func(sid, parsed_data) - # If a response model is provided, validate the output - if response_model: - try: - parsed_result = response_model.model_validate(result) - except ValidationError: - traceback.print_exc() - return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response")) - else: - parsed_result = result - # Emit the validated result - if parsed_result: - if isinstance(parsed_result, BaseModel): - return json_like(parsed_result) - return parsed_result - return wrapper - return decorator - -def nicenessify(priority:int, pid:int|None=None): - try: - pid = os.getpid() if pid is None else pid - ps = psutil.Process(pid) - if os.name == 'posix': - ps.nice(priority) - except Exception as e: - print(f"Error setting priority: {e} {traceback.format_exc()}") - pass +import asyncio +from ipaddress import ip_address, ip_interface +import os +import socket +import psutil +import sys +import nftables +from socketio import AsyncServer +from fastapi import Path +from typing import Annotated +from functools import wraps +from pydantic import BaseModel, ValidationError +import traceback +from utils.models import StatusMessageModel +from typing import List + +LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) + +socketio:AsyncServer = None +sid_list:set = set() + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +ROUTERS_DIR = os.path.join(ROOT_DIR,"routers") +ON_DOCKER = "DOCKER" in sys.argv +DEBUG = "DEBUG" in sys.argv +NORELOAD = "NORELOAD" in sys.argv +FIREGEX_PORT = int(os.getenv("PORT","4444")) +FIREGEX_HOST = os.getenv("HOST","0.0.0.0") +FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None) +FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None +JWT_ALGORITHM: str = "HS256" +API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" + +PortType = Annotated[int, Path(gt=0, lt=65536)] + +async def run_func(func, *args, **kwargs): + if asyncio.iscoroutinefunction(func): + return await func(*args, **kwargs) + else: + return func(*args, **kwargs) + +async def socketio_emit(elements:list[str]): + await socketio.emit("update",elements) + +def refactor_name(name:str): + name = name.strip() + while " " in name: + name = name.replace(" "," ") + return name + +class SysctlManager: + def __init__(self, ctl_table): + self.old_table = {} + self.new_table = {} + if os.path.isdir("/sys_host/"): + self.old_table = dict() + self.new_table = dict(ctl_table) + for name in ctl_table.keys(): + self.old_table[name] = read_sysctl(name) + + def write_table(self, table) -> bool: + for name, value in table.items(): + if read_sysctl(name) != value: + write_sysctl(name, value) + + def set(self): + self.write_table(self.new_table) + + def reset(self): + self.write_table(self.old_table) + +def read_sysctl(name:str): + with open(f"/sys_host/{name}", "rt") as f: + return "1" in f.read() + +def write_sysctl(name:str, value:bool): + with open(f"/sys_host/{name}", "wt") as f: + f.write("1" if value else "0") + +def list_files(mypath): + from os import listdir + from os.path import isfile, join + return [f for f in listdir(mypath) if isfile(join(mypath, f))] + +def ip_parse(ip:str): + return str(ip_interface(ip).network) + +def is_ip_parse(ip:str): + try: + ip_parse(ip) + return True + except Exception: + return False + +def addr_parse(ip:str): + return str(ip_address(ip)) + +def ip_family(ip:str): + return "ip6" if ip_interface(ip).version == 6 else "ip" + +def get_interfaces(): + def _get_interfaces(): + for int_name, interfs in psutil.net_if_addrs().items(): + for interf in interfs: + if interf.family in [socket.AF_INET, socket.AF_INET6]: + yield {"name": int_name, "addr":interf.address} + return list(_get_interfaces()) + +def nftables_int_to_json(ip_int): + ip_int = ip_parse(ip_int) + ip_addr = str(ip_int).split("/")[0] + ip_addr_cidr = int(str(ip_int).split("/")[1]) + return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}} + +def nftables_json_to_int(ip_json_int): + if isinstance(ip_json_int,str): + return str(ip_parse(ip_json_int)) + else: + return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}' + +class Singleton(object): + __instance = None + def __new__(class_, *args, **kwargs): + if not isinstance(class_.__instance, class_): + class_.__instance = object.__new__(class_, *args, **kwargs) + return class_.__instance + +class NFTableManager(Singleton): + + table_name = "firegex" + + def __init__(self, init_cmd, reset_cmd): + self.__init_cmds = init_cmd + self.__reset_cmds = reset_cmd + self.nft = nftables.Nftables() + + def raw_cmd(self, *cmds): + return self.nft.json_cmd({"nftables": list(cmds)}) + + def cmd(self, *cmds): + code, out, err = self.raw_cmd(*cmds) + if code == 0: + return out + else: + raise Exception(err) + + def init(self): + self.reset() + self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}}) + self.cmd(*self.__init_cmds) + + def reset(self): + self.raw_cmd(*self.__reset_cmds) + + def list_rules(self, tables = None, chains = None): + for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]: + if tables and filter["table"] not in tables: + continue + if chains and filter["chain"] not in chains: + continue + yield filter + + def raw_list(self): + return self.cmd({"list": {"ruleset": None}})["nftables"] + +def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"): + res = obj.model_dump(mode=mode, exclude_unset=not unset) + if convert_keys: + for from_k, to_k in convert_keys.items(): + if from_k in res: + res[to_k] = res.pop(from_k) + if exclude: + for ele in exclude: + if ele in res: + del res[ele] + return res + +def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict: + if isinstance(obj, list): + return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj] + return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) + +def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None): + def decorator(func): + @sio_server.on(event_name) # Automatically registers the event + @wraps(func) + async def wrapper(sid, data): + try: + # Parse and validate incoming data + parsed_data = model.model_validate(data) + except ValidationError: + return json_like(StatusMessageModel(status=f"Invalid {event_name} request")) + + # Call the original function with the parsed data + result = await func(sid, parsed_data) + # If a response model is provided, validate the output + if response_model: + try: + parsed_result = response_model.model_validate(result) + except ValidationError: + traceback.print_exc() + return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response")) + else: + parsed_result = result + # Emit the validated result + if parsed_result: + if isinstance(parsed_result, BaseModel): + return json_like(parsed_result) + return parsed_result + return wrapper + return decorator + +def nicenessify(priority:int, pid:int|None=None): + try: + pid = os.getpid() if pid is None else pid + ps = psutil.Process(pid) + if os.name == 'posix': + ps.nice(priority) + except Exception as e: + print(f"Error setting priority: {e} {traceback.format_exc()}") + pass diff --git a/docs/TRAFFIC_VIEWER.md b/docs/TRAFFIC_VIEWER.md new file mode 100644 index 0000000..70fbbe4 --- /dev/null +++ b/docs/TRAFFIC_VIEWER.md @@ -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 + 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 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 49ba445..2242bc4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 }) => { } > } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/AddNewRegex.tsx b/frontend/src/components/AddNewRegex.tsx index 5437ec5..24b47c0 100644 --- a/frontend/src/components/AddNewRegex.tsx +++ b/frontend/src/components/AddNewRegex.tsx @@ -1,115 +1,115 @@ -import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useState } from 'react'; -import { RegexAddForm } from '../js/models'; -import { b64decode, b64encode, okNotify } from '../js/utils'; -import { ImCross } from "react-icons/im" -import { nfregex } from './NFRegex/utils'; - -type RegexAddInfo = { - regex:string, - mode:string, - is_case_insensitive:boolean, - deactive:boolean -} - -function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) { - - const form = useForm({ - initialValues: { - regex:"", - mode:"C", - is_case_insensitive:false, - deactive:false - }, - validate:{ - regex: (value) => value !== "" ? null : "Regex is required", - mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode", - } - }) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = (values:RegexAddInfo) => { - setSubmitLoading(true) - - const request:RegexAddForm = { - is_case_sensitive: !values.is_case_insensitive, - service_id: service, - mode: values.mode?values.mode:"B", - regex: b64encode(values.regex), - active: !values.deactive - } - setSubmitLoading(false) - nfregex.regexesadd(request).then( res => { - if (!res){ - setSubmitLoading(false) - close(); - okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`) - }else if (res.toLowerCase() === "invalid regex"){ - setSubmitLoading(false) - form.setFieldError("regex", "Invalid Regex") - }else{ - setSubmitLoading(false) - setError("Error: [ "+res+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - - } - - - return -
- - - - - - - Server' }, + { value: 'S', label: 'Server -> Client' }, + { value: 'B', label: 'Both (Client <-> Server)' }, + ]} + label="Choose the source of the packets to filter" + variant="filled" + {...form.getInputProps('mode')} + /> + + + + + + + {error?<> + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + :null} + + +
+ +} + +export default AddNewRegex; diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index 105845e..e87946a 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -1,82 +1,82 @@ -import React, { useState } from 'react'; -import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core'; -import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils'; -import { AiFillHome } from "react-icons/ai" -import { useNavigate } from 'react-router'; -import { FaLock } from 'react-icons/fa'; -import { MdOutlineSettingsBackupRestore } from 'react-icons/md'; -import { ImExit } from 'react-icons/im'; -import ResetPasswordModal from './ResetPasswordModal'; -import ResetModal from './ResetModal'; -import { MenuDropDownWithButton } from '../MainLayout'; -import { useNavbarStore } from '../../js/store'; - - -function HeaderPage(props: any) { - - const navigator = useNavigate() - const { navOpened, toggleNav } = useNavbarStore() - - const logout_action = () => { - logout().then(r => { - window.location.reload() - }).catch(r => { - errorNotify("Logout failed!",`Error: ${r}`) - }) - } - - const go_to_home = () => { - navigator(`/${getMainPath()}`) - } - - const [changePasswordModal, setChangePasswordModal] = useState(false); - const [resetFiregexModal, setResetFiregexModal] = useState(false); - return - - - - - Firegex logonavigator("/")}/> - - - - [Fi]*regex -

By Pwnzer0tt1

-
-
- - - - - Firewall Access - } onClick={() => setChangePasswordModal(true)}>Change Password - - Actions - } onClick={() => setResetFiregexModal(true)}>Reset Firegex - - - - - - - - - - - - setChangePasswordModal(false)} /> - setResetFiregexModal(false)} /> - -
-} - -export default HeaderPage; +import React, { useState } from 'react'; +import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core'; +import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils'; +import { AiFillHome } from "react-icons/ai" +import { useNavigate } from 'react-router'; +import { FaLock } from 'react-icons/fa'; +import { MdOutlineSettingsBackupRestore } from 'react-icons/md'; +import { ImExit } from 'react-icons/im'; +import ResetPasswordModal from './ResetPasswordModal'; +import ResetModal from './ResetModal'; +import { MenuDropDownWithButton } from '../MainLayout'; +import { useNavbarStore } from '../../js/store'; + + +function HeaderPage(props: any) { + + const navigator = useNavigate() + const { navOpened, toggleNav } = useNavbarStore() + + const logout_action = () => { + logout().then(r => { + window.location.reload() + }).catch(r => { + errorNotify("Logout failed!",`Error: ${r}`) + }) + } + + const go_to_home = () => { + navigator(`/${getMainPath()}`) + } + + const [changePasswordModal, setChangePasswordModal] = useState(false); + const [resetFiregexModal, setResetFiregexModal] = useState(false); + return + + + + + Firegex logonavigator("/")}/> + + + + [Fi]*regex +

By Pwnzer0tt1

+
+
+ + + + + Firewall Access + } onClick={() => setChangePasswordModal(true)}>Change Password + + Actions + } onClick={() => setResetFiregexModal(true)}>Reset Firegex + + + + + + + + + + + + setChangePasswordModal(false)} /> + setResetFiregexModal(false)} /> + +
+} + +export default HeaderPage; diff --git a/frontend/src/components/MainLayout.tsx b/frontend/src/components/MainLayout.tsx index 12b4811..cf30b0f 100644 --- a/frontend/src/components/MainLayout.tsx +++ b/frontend/src/components/MainLayout.tsx @@ -1,51 +1,51 @@ -import { useEffect } from 'react'; -import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core'; -import { AppShell } from '@mantine/core'; -import NavBar from './NavBar'; -import HeaderPage from './Header'; -import { getMainPath } from '../js/utils'; -import { useLocation } from 'react-router'; -import { useNavbarStore } from '../js/store'; -import { HiMenu } from "react-icons/hi"; - - -function MainLayout({ children }:{ children:any }) { - const { navOpened } = useNavbarStore() - const location = useLocation() - useEffect(()=>{ - if (location.pathname !== "/"){ - sessionStorage.setItem('home_section', getMainPath()) - } - },[location.pathname]) - return - - - - - {children} - - - - - - -} - -export default MainLayout; - -export const MenuDropDownWithButton = ({children}:{children:any}) => - - - - - - - - - {children} - - +import { useEffect } from 'react'; +import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core'; +import { AppShell } from '@mantine/core'; +import NavBar from './NavBar'; +import HeaderPage from './Header'; +import { getMainPath } from '../js/utils'; +import { useLocation } from 'react-router'; +import { useNavbarStore } from '../js/store'; +import { HiMenu } from "react-icons/hi"; + + +function MainLayout({ children }:{ children:any }) { + const { navOpened } = useNavbarStore() + const location = useLocation() + useEffect(()=>{ + if (location.pathname !== "/"){ + sessionStorage.setItem('home_section', getMainPath()) + } + },[location.pathname]) + return + + + + + {children} + + + + + + +} + +export default MainLayout; + +export const MenuDropDownWithButton = ({children}:{children:any}) => + + + + + + + + + {children} + + diff --git a/frontend/src/components/NFProxy/AddEditService.tsx b/frontend/src/components/NFProxy/AddEditService.tsx index daa5585..c827259 100644 --- a/frontend/src/components/NFProxy/AddEditService.tsx +++ b/frontend/src/components/NFProxy/AddEditService.tsx @@ -1,139 +1,139 @@ -import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useEffect, useState } from 'react'; -import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; -import { ImCross } from "react-icons/im" -import { nfproxy, Service } from './utils'; -import PortAndInterface from '../PortAndInterface'; -import { IoMdInformationCircleOutline } from "react-icons/io"; -import { ServiceAddForm as ServiceAddFormOriginal } from './utils'; - -type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean} - -function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) { - - const initialValues = { - name: "", - port:edit?.port??8080, - ip_int:edit?.ip_int??"", - proto:edit?.proto??"tcp", - fail_open: edit?.fail_open??false, - autostart: true - } - - const form = useForm({ - initialValues: initialValues, - validate:{ - name: (value) => edit? null : value !== "" ? null : "Service name is required", - port: (value) => (value>0 && value<65536) ? null : "Invalid port", - proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol", - ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", - } - }) - - useEffect(() => { - if (opened){ - form.setInitialValues(initialValues) - form.reset() - } - }, [opened]) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ - setSubmitLoading(true) - if (edit){ - nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => { - if (!res){ - setSubmitLoading(false) - close(); - okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`) - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - }else{ - nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => { - if (res.status === "ok" && res.service_id){ - setSubmitLoading(false) - close(); - if (autostart) nfproxy.servicestart(res.service_id) - okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) - }else{ - setSubmitLoading(false) - setError("Invalid request! [ "+res.status+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - } - } - - - return -
- {!edit?:null} - - - - - - - {!edit?:null} - - - Enable fail-open nfqueue - - - Firegex use internally nfqueue to handle packets
enabling this option will allow packets to pass through the firewall
in case the filtering is too slow or too many traffic is coming
- }> - -
-
} - {...form.getInputProps('fail_open', { type: 'checkbox' })} - /> -
- - {edit?null:} - - - - - - - {error?<> - - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - - :null} - - -
- -} - -export default AddEditService; +import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useEffect, useState } from 'react'; +import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; +import { ImCross } from "react-icons/im" +import { nfproxy, Service } from './utils'; +import PortAndInterface from '../PortAndInterface'; +import { IoMdInformationCircleOutline } from "react-icons/io"; +import { ServiceAddForm as ServiceAddFormOriginal } from './utils'; + +type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean} + +function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) { + + const initialValues = { + name: "", + port:edit?.port??8080, + ip_int:edit?.ip_int??"", + proto:edit?.proto??"tcp", + fail_open: edit?.fail_open??false, + autostart: true + } + + const form = useForm({ + initialValues: initialValues, + validate:{ + name: (value) => edit? null : value !== "" ? null : "Service name is required", + port: (value) => (value>0 && value<65536) ? null : "Invalid port", + proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol", + ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", + } + }) + + useEffect(() => { + if (opened){ + form.setInitialValues(initialValues) + form.reset() + } + }, [opened]) + + const close = () =>{ + onClose() + form.reset() + setError(null) + } + + const [submitLoading, setSubmitLoading] = useState(false) + const [error, setError] = useState(null) + + const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ + setSubmitLoading(true) + if (edit){ + nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => { + if (!res){ + setSubmitLoading(false) + close(); + okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`) + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + }else{ + nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => { + if (res.status === "ok" && res.service_id){ + setSubmitLoading(false) + close(); + if (autostart) nfproxy.servicestart(res.service_id) + okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) + }else{ + setSubmitLoading(false) + setError("Invalid request! [ "+res.status+" ]") + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + } + } + + + return +
+ {!edit?:null} + + + + + + + {!edit?:null} + + + Enable fail-open nfqueue + + + Firegex use internally nfqueue to handle packets
enabling this option will allow packets to pass through the firewall
in case the filtering is too slow or too many traffic is coming
+ }> + +
+
} + {...form.getInputProps('fail_open', { type: 'checkbox' })} + /> +
+ + {edit?null:} + + + + + + + {error?<> + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} + + +
+ +} + +export default AddEditService; diff --git a/frontend/src/components/NFProxy/ServiceRow/index.tsx b/frontend/src/components/NFProxy/ServiceRow/index.tsx index f90627e..a579125 100644 --- a/frontend/src/components/NFProxy/ServiceRow/index.tsx +++ b/frontend/src/components/NFProxy/ServiceRow/index.tsx @@ -1,164 +1,164 @@ -import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core'; -import { useState } from 'react'; -import { FaPlay, FaStop } from 'react-icons/fa'; -import { nfproxy, Service, serviceQueryKey } from '../utils'; -import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md" -import YesNoModal from '../../YesNoModal'; -import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils'; -import { BsTrashFill } from 'react-icons/bs'; -import { BiRename } from 'react-icons/bi' -import RenameForm from './RenameForm'; -import { MenuDropDownWithButton } from '../../MainLayout'; -import { useQueryClient } from '@tanstack/react-query'; -import { TbPlugConnected } from "react-icons/tb"; -import { FaFilter } from "react-icons/fa"; -import { IoSettingsSharp } from 'react-icons/io5'; -import AddEditService from '../AddEditService'; -import { FaPencilAlt } from "react-icons/fa"; -import { ExceptionWarning } from '../ExceptionWarning'; - -export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { - - let status_color = "gray"; - switch(service.status){ - case "stop": status_color = "red"; break; - case "active": status_color = "teal"; break; - } - - const queryClient = useQueryClient() - const [buttonLoading, setButtonLoading] = useState(false) - const [deleteModal, setDeleteModal] = useState(false) - const [renameModal, setRenameModal] = useState(false) - const [editModal, setEditModal] = useState(false) - const isMedium = isMediumScreen() - - const stopService = async () => { - setButtonLoading(true) - - await nfproxy.servicestop(service.service_id).then(res => { - if(!res){ - okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`) - }) - setButtonLoading(false); - } - - const startService = async () => { - setButtonLoading(true) - await nfproxy.servicestart(service.service_id).then(res => { - if(!res){ - okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`) - }) - setButtonLoading(false) - } - - const deleteService = () => { - nfproxy.servicedelete(service.service_id).then(res => { - if (!res){ - okNotify("Service delete complete!",`The service ${service.name} has been deleted!`) - queryClient.invalidateQueries(serviceQueryKey) - }else - errorNotify("An error occurred while deleting a service",`Error: ${res}`) - }).catch(err => { - errorNotify("An error occurred while deleting a service",`Error: ${err}`) - }) - - } - - return <> - - - - - - - {service.name} - - - - {service.status} - - :{service.port} - - - {isMedium?null:} - - - - - {service.ip_int} on {service.proto} - - - {service.blocked_packets} - - {service.edited_packets} - - {service.n_filters} - - - {isMedium?:} - - - - - Edit service - } onClick={()=>setEditModal(true)}>Service Settings - } onClick={()=>setRenameModal(true)}>Change service name - - Danger zone - } onClick={()=>setDeleteModal(true)}>Delete Service - - - - - - - - - - - - - - {isMedium?:} - {onClick? - - :null} - - - - - setDeleteModal(false) } - action={deleteService} - opened={deleteModal} - /> - setRenameModal(false)} - opened={renameModal} - service={service} - /> - setEditModal(false)} - edit={service} - /> - -} +import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core'; +import { useState } from 'react'; +import { FaPlay, FaStop } from 'react-icons/fa'; +import { nfproxy, Service, serviceQueryKey } from '../utils'; +import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md" +import YesNoModal from '../../YesNoModal'; +import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils'; +import { BsTrashFill } from 'react-icons/bs'; +import { BiRename } from 'react-icons/bi' +import RenameForm from './RenameForm'; +import { MenuDropDownWithButton } from '../../MainLayout'; +import { useQueryClient } from '@tanstack/react-query'; +import { TbPlugConnected } from "react-icons/tb"; +import { FaFilter } from "react-icons/fa"; +import { IoSettingsSharp } from 'react-icons/io5'; +import AddEditService from '../AddEditService'; +import { FaPencilAlt } from "react-icons/fa"; +import { ExceptionWarning } from '../ExceptionWarning'; + +export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { + + let status_color = "gray"; + switch(service.status){ + case "stop": status_color = "red"; break; + case "active": status_color = "teal"; break; + } + + const queryClient = useQueryClient() + const [buttonLoading, setButtonLoading] = useState(false) + const [deleteModal, setDeleteModal] = useState(false) + const [renameModal, setRenameModal] = useState(false) + const [editModal, setEditModal] = useState(false) + const isMedium = isMediumScreen() + + const stopService = async () => { + setButtonLoading(true) + + await nfproxy.servicestop(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`) + }) + setButtonLoading(false); + } + + const startService = async () => { + setButtonLoading(true) + await nfproxy.servicestart(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`) + }) + setButtonLoading(false) + } + + const deleteService = () => { + nfproxy.servicedelete(service.service_id).then(res => { + if (!res){ + okNotify("Service delete complete!",`The service ${service.name} has been deleted!`) + queryClient.invalidateQueries(serviceQueryKey) + }else + errorNotify("An error occurred while deleting a service",`Error: ${res}`) + }).catch(err => { + errorNotify("An error occurred while deleting a service",`Error: ${err}`) + }) + + } + + return <> + + + + + + + {service.name} + + + + {service.status} + + :{service.port} + + + {isMedium?null:} + + + + + {service.ip_int} on {service.proto} + + + {service.blocked_packets} + + {service.edited_packets} + + {service.n_filters} + + + {isMedium?:} + + + + + Edit service + } onClick={()=>setEditModal(true)}>Service Settings + } onClick={()=>setRenameModal(true)}>Change service name + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + + + + + + + + + + + {isMedium?:} + {onClick? + + :null} + + + + + setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={service} + /> + setEditModal(false)} + edit={service} + /> + +} diff --git a/frontend/src/components/NFProxy/utils.ts b/frontend/src/components/NFProxy/utils.ts index 3ce66de..7b4ba27 100644 --- a/frontend/src/components/NFProxy/utils.ts +++ b/frontend/src/components/NFProxy/utils.ts @@ -1,175 +1,182 @@ -import { PyFilter, ServerResponse } from "../../js/models" -import { deleteapi, getapi, postapi, putapi } from "../../js/utils" -import { useQuery } from "@tanstack/react-query" - -export type Service = { - service_id:string, - name:string, - status:string, - port:number, - proto: string, - ip_int: string, - n_filters:number, - edited_packets:number, - blocked_packets:number, - fail_open:boolean, -} - -export type ServiceAddForm = { - name:string, - port:number, - proto:string, - ip_int:string, - fail_open: boolean, -} - -export type ServiceSettings = { - port?:number, - ip_int?:string, - fail_open?: boolean, -} - -export type ServiceAddResponse = { - status: string, - service_id?: string, -} - -export const serviceQueryKey = ["nfproxy","services"] - -export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services}) -export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({ - queryKey:[...serviceQueryKey,service_id,"pyfilters"], - queryFn:() => nfproxy.servicepyfilters(service_id) -}) - -export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({ - queryKey:[...serviceQueryKey,service_id,"pyfilters","code"], - queryFn:() => nfproxy.getpyfilterscode(service_id) -}) - -export const nfproxy = { - services: async () => { - return await getapi("nfproxy/services") as Service[]; - }, - serviceinfo: async (service_id:string) => { - return await getapi(`nfproxy/services/${service_id}`) as Service; - }, - pyfilterenable: async (service_id:string, filter_name:string) => { - const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse; - return status === "ok"?undefined:status - }, - pyfilterdisable: async (service_id:string, filter_name:string) => { - const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicestart: async (service_id:string) => { - const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicerename: async (service_id:string, name: string) => { - const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse; - return status === "ok"?undefined:status - }, - servicestop: async (service_id:string) => { - const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicesadd: async (data:ServiceAddForm) => { - return await postapi("nfproxy/services",data) as ServiceAddResponse; - }, - servicedelete: async (service_id:string) => { - const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicepyfilters: async (service_id:string) => { - return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[]; - }, - settings: async (service_id:string, data:ServiceSettings) => { - const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse; - return status === "ok"?undefined:status - }, - getpyfilterscode: async (service_id:string) => { - return await getapi(`nfproxy/services/${service_id}/code`) as string; - }, - setpyfilterscode: async (service_id:string, code:string) => { - const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse; - return status === "ok"?undefined:status - } -} - - -export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol - -# From here we can import the DataTypes that we want to use: -# The data type must be specified in the filter functions -# And will also interally be used to decide when call some filters and how aggregate data -from firegex.nfproxy.models import RawPacket - -# global context in this execution is dedicated to a single TCP stream -# - This code will be executed once at the TCP stream start -# - The filter will be called for each packet in the stream -# - You can store in global context some data you need, but exceeding with data stored could be dangerous -# - At the end of the stream the global context will be destroyed - -from firegex.nfproxy import pyfilter -# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type - -from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP -# - The filter must return one of the following values: -# - ACCEPT: The packet will be accepted -# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream) -# - UNSTABLE_MANGLE: The packet will be mangled and accepted -# - DROP: All the packets in this stream will be easly dropped - -# If you want, you can use print to debug your filters, but this could slow down the filter - -# Filter names must be unique and are specified by the name of the function wrapped by the decorator -@pyfilter -# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction -def strange_filter(packet:RawPacket): - # Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below - # Also is not garanteed that l4_data is the same of the packet data: - # packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue - # Unorder packets in TCP are accepted by default, and python is not called in this case - # For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only - if b"TEST_MANGLING" in packet.l4_data: - # It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead - packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE") - return UNSTABLE_MANGLE - # Drops the traffic - if b"BAD DATA 1" in packet.data: - return DROP - # Rejects the traffic - if b"BAD DATA 2" in packet.data: - return REJECT - # Accepts the traffic (default if None is returned) - return ACCEPT - -# Example with a higher level of abstraction -@pyfilter -def http_filter(http:HTTPRequest): - if http.method == "GET" and "test" in http.url: - return REJECT - -# ADVANCED OPTIONS -# You can specify some additional options on the streaming managment -# pyproxy will automatically store all the packets (already ordered by the c++ binary): -# -# If the stream is too big, you can specify what actions to take: -# This can be done defining some variables in the global context -# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB) -# NOTE: the stream size is calculated and managed indipendently by the data type handling system -# Only types required by at least 1 filter will be stored. -# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full -# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default) -# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter -# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter -# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic - -from firege.nfproxy import FullStreamAction - -# Example of a global context -FGEX_STREAM_MAX_SIZE = 4096 -FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT -# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic -` +import { PyFilter, ServerResponse } from "../../js/models" +import { deleteapi, getapi, postapi, putapi } from "../../js/utils" +import { useQuery } from "@tanstack/react-query" + +export type Service = { + service_id:string, + name:string, + status:string, + port:number, + proto: string, + ip_int: string, + n_filters:number, + edited_packets:number, + blocked_packets:number, + fail_open:boolean, +} + +export type ServiceAddForm = { + name:string, + port:number, + proto:string, + ip_int:string, + fail_open: boolean, +} + +export type ServiceSettings = { + port?:number, + ip_int?:string, + fail_open?: boolean, +} + +export type ServiceAddResponse = { + status: string, + service_id?: string, +} + +export const serviceQueryKey = ["nfproxy","services"] + +export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services}) +export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({ + queryKey:[...serviceQueryKey,service_id,"pyfilters"], + queryFn:() => nfproxy.servicepyfilters(service_id) +}) + +export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({ + queryKey:[...serviceQueryKey,service_id,"pyfilters","code"], + queryFn:() => nfproxy.getpyfilterscode(service_id) +}) + +export const nfproxy = { + services: async () => { + return await getapi("nfproxy/services") as Service[]; + }, + serviceinfo: async (service_id:string) => { + return await getapi(`nfproxy/services/${service_id}`) as Service; + }, + pyfilterenable: async (service_id:string, filter_name:string) => { + const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse; + return status === "ok"?undefined:status + }, + pyfilterdisable: async (service_id:string, filter_name:string) => { + const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicestart: async (service_id:string) => { + const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicerename: async (service_id:string, name: string) => { + const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse; + return status === "ok"?undefined:status + }, + servicestop: async (service_id:string) => { + const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicesadd: async (data:ServiceAddForm) => { + return await postapi("nfproxy/services",data) as ServiceAddResponse; + }, + servicedelete: async (service_id:string) => { + const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicepyfilters: async (service_id:string) => { + return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[]; + }, + settings: async (service_id:string, data:ServiceSettings) => { + const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse; + return status === "ok"?undefined:status + }, + getpyfilterscode: async (service_id:string) => { + return await getapi(`nfproxy/services/${service_id}/code`) as string; + }, + 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 + } +} + + +export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol + +# From here we can import the DataTypes that we want to use: +# The data type must be specified in the filter functions +# And will also interally be used to decide when call some filters and how aggregate data +from firegex.nfproxy.models import RawPacket + +# global context in this execution is dedicated to a single TCP stream +# - This code will be executed once at the TCP stream start +# - The filter will be called for each packet in the stream +# - You can store in global context some data you need, but exceeding with data stored could be dangerous +# - At the end of the stream the global context will be destroyed + +from firegex.nfproxy import pyfilter +# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type + +from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP +# - The filter must return one of the following values: +# - ACCEPT: The packet will be accepted +# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream) +# - UNSTABLE_MANGLE: The packet will be mangled and accepted +# - DROP: All the packets in this stream will be easly dropped + +# If you want, you can use print to debug your filters, but this could slow down the filter + +# Filter names must be unique and are specified by the name of the function wrapped by the decorator +@pyfilter +# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction +def strange_filter(packet:RawPacket): + # Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below + # Also is not garanteed that l4_data is the same of the packet data: + # packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue + # Unorder packets in TCP are accepted by default, and python is not called in this case + # For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only + if b"TEST_MANGLING" in packet.l4_data: + # It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead + packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE") + return UNSTABLE_MANGLE + # Drops the traffic + if b"BAD DATA 1" in packet.data: + return DROP + # Rejects the traffic + if b"BAD DATA 2" in packet.data: + return REJECT + # Accepts the traffic (default if None is returned) + return ACCEPT + +# Example with a higher level of abstraction +@pyfilter +def http_filter(http:HTTPRequest): + if http.method == "GET" and "test" in http.url: + return REJECT + +# ADVANCED OPTIONS +# You can specify some additional options on the streaming managment +# pyproxy will automatically store all the packets (already ordered by the c++ binary): +# +# If the stream is too big, you can specify what actions to take: +# This can be done defining some variables in the global context +# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB) +# NOTE: the stream size is calculated and managed indipendently by the data type handling system +# Only types required by at least 1 filter will be stored. +# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full +# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default) +# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter +# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter +# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic + +from firege.nfproxy import FullStreamAction + +# Example of a global context +FGEX_STREAM_MAX_SIZE = 4096 +FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT +# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic +` diff --git a/frontend/src/components/NFRegex/AddEditService.tsx b/frontend/src/components/NFRegex/AddEditService.tsx index 1a696f5..6dfa96a 100644 --- a/frontend/src/components/NFRegex/AddEditService.tsx +++ b/frontend/src/components/NFRegex/AddEditService.tsx @@ -1,139 +1,139 @@ -import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useEffect, useState } from 'react'; -import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; -import { ImCross } from "react-icons/im" -import { nfregex, Service } from './utils'; -import PortAndInterface from '../PortAndInterface'; -import { IoMdInformationCircleOutline } from "react-icons/io"; -import { ServiceAddForm as ServiceAddFormOriginal } from './utils'; - -type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean} - -function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) { - - const initialValues = { - name: "", - port:edit?.port??8080, - ip_int:edit?.ip_int??"", - proto:edit?.proto??"tcp", - fail_open: edit?.fail_open??false, - autostart: true - } - - const form = useForm({ - initialValues: initialValues, - validate:{ - name: (value) => edit? null : value !== "" ? null : "Service name is required", - port: (value) => (value>0 && value<65536) ? null : "Invalid port", - proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", - ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", - } - }) - - useEffect(() => { - if (opened){ - form.setInitialValues(initialValues) - form.reset() - } - }, [opened]) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ - setSubmitLoading(true) - if (edit){ - nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => { - if (!res){ - setSubmitLoading(false) - close(); - okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`) - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - }else{ - nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => { - if (res.status === "ok" && res.service_id){ - setSubmitLoading(false) - close(); - if (autostart) nfregex.servicestart(res.service_id) - okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) - }else{ - setSubmitLoading(false) - setError("Invalid request! [ "+res.status+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - } - } - - - return -
- {!edit?:null} - - - - - - - {!edit?:null} - - - Enable fail-open nfqueue - - - Firegex use internally nfqueue to handle packets
enabling this option will allow packets to pass through the firewall
in case the filtering is too slow or too many traffic is coming
- }> - -
-
} - {...form.getInputProps('fail_open', { type: 'checkbox' })} - /> -
- - - - - - - - - {error?<> - - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - - :null} - - -
- -} - -export default AddEditService; +import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useEffect, useState } from 'react'; +import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; +import { ImCross } from "react-icons/im" +import { nfregex, Service } from './utils'; +import PortAndInterface from '../PortAndInterface'; +import { IoMdInformationCircleOutline } from "react-icons/io"; +import { ServiceAddForm as ServiceAddFormOriginal } from './utils'; + +type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean} + +function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) { + + const initialValues = { + name: "", + port:edit?.port??8080, + ip_int:edit?.ip_int??"", + proto:edit?.proto??"tcp", + fail_open: edit?.fail_open??false, + autostart: true + } + + const form = useForm({ + initialValues: initialValues, + validate:{ + name: (value) => edit? null : value !== "" ? null : "Service name is required", + port: (value) => (value>0 && value<65536) ? null : "Invalid port", + proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", + ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", + } + }) + + useEffect(() => { + if (opened){ + form.setInitialValues(initialValues) + form.reset() + } + }, [opened]) + + const close = () =>{ + onClose() + form.reset() + setError(null) + } + + const [submitLoading, setSubmitLoading] = useState(false) + const [error, setError] = useState(null) + + const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ + setSubmitLoading(true) + if (edit){ + nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => { + if (!res){ + setSubmitLoading(false) + close(); + okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`) + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + }else{ + nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => { + if (res.status === "ok" && res.service_id){ + setSubmitLoading(false) + close(); + if (autostart) nfregex.servicestart(res.service_id) + okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) + }else{ + setSubmitLoading(false) + setError("Invalid request! [ "+res.status+" ]") + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + } + } + + + return +
+ {!edit?:null} + + + + + + + {!edit?:null} + + + Enable fail-open nfqueue + + + Firegex use internally nfqueue to handle packets
enabling this option will allow packets to pass through the firewall
in case the filtering is too slow or too many traffic is coming
+ }> + +
+
} + {...form.getInputProps('fail_open', { type: 'checkbox' })} + /> +
+ + + + + + + + + {error?<> + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} + + +
+ +} + +export default AddEditService; diff --git a/frontend/src/components/NFRegex/ServiceRow/index.tsx b/frontend/src/components/NFRegex/ServiceRow/index.tsx index 9086d54..cd28079 100644 --- a/frontend/src/components/NFRegex/ServiceRow/index.tsx +++ b/frontend/src/components/NFRegex/ServiceRow/index.tsx @@ -1,158 +1,158 @@ -import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core'; -import { useState } from 'react'; -import { FaPlay, FaStop } from 'react-icons/fa'; -import { nfregex, Service, serviceQueryKey } from '../utils'; -import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md" -import YesNoModal from '../../YesNoModal'; -import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils'; -import { BsTrashFill } from 'react-icons/bs'; -import { BiRename } from 'react-icons/bi' -import RenameForm from './RenameForm'; -import { MenuDropDownWithButton } from '../../MainLayout'; -import { useQueryClient } from '@tanstack/react-query'; -import { FaFilter } from "react-icons/fa"; -import { VscRegex } from "react-icons/vsc"; -import { IoSettingsSharp } from 'react-icons/io5'; -import AddEditService from '../AddEditService'; - -export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { - - let status_color = "gray"; - switch(service.status){ - case "stop": status_color = "red"; break; - case "active": status_color = "teal"; break; - } - - const queryClient = useQueryClient() - const [buttonLoading, setButtonLoading] = useState(false) - const [deleteModal, setDeleteModal] = useState(false) - const [renameModal, setRenameModal] = useState(false) - const [editModal, setEditModal] = useState(false) - const isMedium = isMediumScreen() - - const stopService = async () => { - setButtonLoading(true) - - await nfregex.servicestop(service.service_id).then(res => { - if(!res){ - okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`) - }) - setButtonLoading(false); - } - - const startService = async () => { - setButtonLoading(true) - await nfregex.servicestart(service.service_id).then(res => { - if(!res){ - okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`) - }) - setButtonLoading(false) - } - - const deleteService = () => { - nfregex.servicedelete(service.service_id).then(res => { - if (!res){ - okNotify("Service delete complete!",`The service ${service.name} has been deleted!`) - queryClient.invalidateQueries(serviceQueryKey) - }else - errorNotify("An error occurred while deleting a service",`Error: ${res}`) - }).catch(err => { - errorNotify("An error occurred while deleting a service",`Error: ${err}`) - }) - - } - - return <> - - - - - - - {service.name} - - - - {service.status} - - :{service.port} - - - {isMedium?null:} - - - - - {service.ip_int} on {service.proto} - - - {service.n_packets} - - {service.n_regex} - - - {isMedium?:} - - - Edit service - } onClick={()=>setEditModal(true)}>Service Settings - } onClick={()=>setRenameModal(true)}>Change service name - - Danger zone - } onClick={()=>setDeleteModal(true)}>Delete Service - - - - - - - - - - - - - - {isMedium?:} - {onClick? - - :null} - - - - - setDeleteModal(false) } - action={deleteService} - opened={deleteModal} - /> - setRenameModal(false)} - opened={renameModal} - service={service} - /> - setEditModal(false)} - edit={service} - /> - -} +import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core'; +import { useState } from 'react'; +import { FaPlay, FaStop } from 'react-icons/fa'; +import { nfregex, Service, serviceQueryKey } from '../utils'; +import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md" +import YesNoModal from '../../YesNoModal'; +import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils'; +import { BsTrashFill } from 'react-icons/bs'; +import { BiRename } from 'react-icons/bi' +import RenameForm from './RenameForm'; +import { MenuDropDownWithButton } from '../../MainLayout'; +import { useQueryClient } from '@tanstack/react-query'; +import { FaFilter } from "react-icons/fa"; +import { VscRegex } from "react-icons/vsc"; +import { IoSettingsSharp } from 'react-icons/io5'; +import AddEditService from '../AddEditService'; + +export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { + + let status_color = "gray"; + switch(service.status){ + case "stop": status_color = "red"; break; + case "active": status_color = "teal"; break; + } + + const queryClient = useQueryClient() + const [buttonLoading, setButtonLoading] = useState(false) + const [deleteModal, setDeleteModal] = useState(false) + const [renameModal, setRenameModal] = useState(false) + const [editModal, setEditModal] = useState(false) + const isMedium = isMediumScreen() + + const stopService = async () => { + setButtonLoading(true) + + await nfregex.servicestop(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`) + }) + setButtonLoading(false); + } + + const startService = async () => { + setButtonLoading(true) + await nfregex.servicestart(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`) + }) + setButtonLoading(false) + } + + const deleteService = () => { + nfregex.servicedelete(service.service_id).then(res => { + if (!res){ + okNotify("Service delete complete!",`The service ${service.name} has been deleted!`) + queryClient.invalidateQueries(serviceQueryKey) + }else + errorNotify("An error occurred while deleting a service",`Error: ${res}`) + }).catch(err => { + errorNotify("An error occurred while deleting a service",`Error: ${err}`) + }) + + } + + return <> + + + + + + + {service.name} + + + + {service.status} + + :{service.port} + + + {isMedium?null:} + + + + + {service.ip_int} on {service.proto} + + + {service.n_packets} + + {service.n_regex} + + + {isMedium?:} + + + Edit service + } onClick={()=>setEditModal(true)}>Service Settings + } onClick={()=>setRenameModal(true)}>Change service name + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + + + + + + + + + + + {isMedium?:} + {onClick? + + :null} + + + + + setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={service} + /> + setEditModal(false)} + edit={service} + /> + +} diff --git a/frontend/src/components/NFRegex/utils.ts b/frontend/src/components/NFRegex/utils.ts index 0fd8b3a..d8d1106 100644 --- a/frontend/src/components/NFRegex/utils.ts +++ b/frontend/src/components/NFRegex/utils.ts @@ -1,95 +1,95 @@ -import { RegexFilter, ServerResponse } from "../../js/models" -import { deleteapi, getapi, postapi, putapi } from "../../js/utils" -import { RegexAddForm } from "../../js/models" -import { useQuery, useQueryClient } from "@tanstack/react-query" - -export type Service = { - name:string, - service_id:string, - status:string, - port:number, - proto: string, - ip_int: string, - n_packets:number, - n_regex:number, - fail_open:boolean, -} - -export type ServiceAddForm = { - name:string, - port:number, - proto:string, - ip_int:string, - fail_open: boolean, -} - -export type ServiceSettings = { - port?:number, - proto?:string, - ip_int?:string, - fail_open?: boolean, -} - -export type ServiceAddResponse = { - status: string, - service_id?: string, -} - -export const serviceQueryKey = ["nfregex","services"] - -export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services}) -export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({ - queryKey:[...serviceQueryKey,service_id,"regexes"], - queryFn:() => nfregex.serviceregexes(service_id) -}) - -export const nfregex = { - services: async () => { - return await getapi("nfregex/services") as Service[]; - }, - serviceinfo: async (service_id:string) => { - return await getapi(`nfregex/services/${service_id}`) as Service; - }, - regexdelete: async (regex_id:number) => { - const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse; - return status === "ok"?undefined:status - }, - regexenable: async (regex_id:number) => { - const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse; - return status === "ok"?undefined:status - }, - regexdisable: async (regex_id:number) => { - const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicestart: async (service_id:string) => { - const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicerename: async (service_id:string, name: string) => { - const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse; - return status === "ok"?undefined:status - }, - servicestop: async (service_id:string) => { - const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicesadd: async (data:ServiceAddForm) => { - return await postapi("nfregex/services",data) as ServiceAddResponse; - }, - servicedelete: async (service_id:string) => { - const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse; - return status === "ok"?undefined:status - }, - regexesadd: async (data:RegexAddForm) => { - const { status } = await postapi("nfregex/regexes",data) as ServerResponse; - return status === "ok"?undefined:status - }, - serviceregexes: async (service_id:string) => { - return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[]; - }, - settings: async (service_id:string, data:ServiceSettings) => { - const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse; - return status === "ok"?undefined:status - }, +import { RegexFilter, ServerResponse } from "../../js/models" +import { deleteapi, getapi, postapi, putapi } from "../../js/utils" +import { RegexAddForm } from "../../js/models" +import { useQuery, useQueryClient } from "@tanstack/react-query" + +export type Service = { + name:string, + service_id:string, + status:string, + port:number, + proto: string, + ip_int: string, + n_packets:number, + n_regex:number, + fail_open:boolean, +} + +export type ServiceAddForm = { + name:string, + port:number, + proto:string, + ip_int:string, + fail_open: boolean, +} + +export type ServiceSettings = { + port?:number, + proto?:string, + ip_int?:string, + fail_open?: boolean, +} + +export type ServiceAddResponse = { + status: string, + service_id?: string, +} + +export const serviceQueryKey = ["nfregex","services"] + +export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services}) +export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({ + queryKey:[...serviceQueryKey,service_id,"regexes"], + queryFn:() => nfregex.serviceregexes(service_id) +}) + +export const nfregex = { + services: async () => { + return await getapi("nfregex/services") as Service[]; + }, + serviceinfo: async (service_id:string) => { + return await getapi(`nfregex/services/${service_id}`) as Service; + }, + regexdelete: async (regex_id:number) => { + const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse; + return status === "ok"?undefined:status + }, + regexenable: async (regex_id:number) => { + const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse; + return status === "ok"?undefined:status + }, + regexdisable: async (regex_id:number) => { + const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicestart: async (service_id:string) => { + const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicerename: async (service_id:string, name: string) => { + const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse; + return status === "ok"?undefined:status + }, + servicestop: async (service_id:string) => { + const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicesadd: async (data:ServiceAddForm) => { + return await postapi("nfregex/services",data) as ServiceAddResponse; + }, + servicedelete: async (service_id:string) => { + const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse; + return status === "ok"?undefined:status + }, + regexesadd: async (data:RegexAddForm) => { + const { status } = await postapi("nfregex/regexes",data) as ServerResponse; + return status === "ok"?undefined:status + }, + serviceregexes: async (service_id:string) => { + return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[]; + }, + settings: async (service_id:string, data:ServiceSettings) => { + const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse; + return status === "ok"?undefined:status + }, } \ No newline at end of file diff --git a/frontend/src/components/NavBar/index.tsx b/frontend/src/components/NavBar/index.tsx index 13d84f1..06a7544 100644 --- a/frontend/src/components/NavBar/index.tsx +++ b/frontend/src/components/NavBar/index.tsx @@ -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() { } /> } /> } /> + } /> {/* Experimental Features 🧪 diff --git a/frontend/src/components/PortHijack/AddNewService.tsx b/frontend/src/components/PortHijack/AddNewService.tsx index 44622be..13c264a 100644 --- a/frontend/src/components/PortHijack/AddNewService.tsx +++ b/frontend/src/components/PortHijack/AddNewService.tsx @@ -1,113 +1,113 @@ -import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useState } from 'react'; -import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils'; -import { ImCross } from "react-icons/im" -import { porthijack } from './utils'; -import PortAndInterface from '../PortAndInterface'; - -type ServiceAddForm = { - name:string, - public_port:number, - proxy_port:number, - proto:string, - ip_src:string, - ip_dst:string, - autostart: boolean, -} - -function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { - - const form = useForm({ - initialValues: { - name:"", - public_port:80, - proxy_port:8080, - proto:"tcp", - ip_src:"", - ip_dst:"127.0.0.1", - autostart: false, - }, - validate:{ - name: (value) => value !== ""? null : "Service name is required", - public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port", - proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port", - proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", - ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address", - ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address", - } - }) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{ - setSubmitLoading(true) - porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => { - if (res.status === "ok" && res.service_id){ - setSubmitLoading(false) - close(); - if (autostart) porthijack.servicestart(res.service_id) - okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`) - }else{ - setSubmitLoading(false) - setError("Invalid request! [ "+res.status+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - } - - - return -
- - - - - - - - - - - - - - - - - - {error?<> - - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - - :null} - - -
- -} - -export default AddNewService; +import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useState } from 'react'; +import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils'; +import { ImCross } from "react-icons/im" +import { porthijack } from './utils'; +import PortAndInterface from '../PortAndInterface'; + +type ServiceAddForm = { + name:string, + public_port:number, + proxy_port:number, + proto:string, + ip_src:string, + ip_dst:string, + autostart: boolean, +} + +function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { + + const form = useForm({ + initialValues: { + name:"", + public_port:80, + proxy_port:8080, + proto:"tcp", + ip_src:"", + ip_dst:"127.0.0.1", + autostart: false, + }, + validate:{ + name: (value) => value !== ""? null : "Service name is required", + public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port", + proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port", + proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", + ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address", + ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address", + } + }) + + const close = () =>{ + onClose() + form.reset() + setError(null) + } + + const [submitLoading, setSubmitLoading] = useState(false) + const [error, setError] = useState(null) + + const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{ + setSubmitLoading(true) + porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => { + if (res.status === "ok" && res.service_id){ + setSubmitLoading(false) + close(); + if (autostart) porthijack.servicestart(res.service_id) + okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`) + }else{ + setSubmitLoading(false) + setError("Invalid request! [ "+res.status+" ]") + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + } + + + return +
+ + + + + + + + + + + + + + + + + + {error?<> + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} + + +
+ +} + +export default AddNewService; diff --git a/frontend/src/components/PortHijack/ServiceRow/index.tsx b/frontend/src/components/PortHijack/ServiceRow/index.tsx index 7b29996..3a1d96a 100644 --- a/frontend/src/components/PortHijack/ServiceRow/index.tsx +++ b/frontend/src/components/PortHijack/ServiceRow/index.tsx @@ -1,152 +1,152 @@ -import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core'; -import { useState } from 'react'; -import { FaPlay, FaStop } from 'react-icons/fa'; -import { porthijack, Service } from '../utils'; -import YesNoModal from '../../YesNoModal'; -import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils'; -import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs'; -import { BiRename } from 'react-icons/bi' -import RenameForm from './RenameForm'; -import ChangeDestination from './ChangeDestination'; -import { useForm } from '@mantine/form'; -import { MenuDropDownWithButton } from '../../MainLayout'; -import { MdDoubleArrow } from "react-icons/md"; - -export default function ServiceRow({ service }:{ service:Service }) { - - let status_color = service.active ? "teal": "red" - - const [buttonLoading, setButtonLoading] = useState(false) - const [deleteModal, setDeleteModal] = useState(false) - const [renameModal, setRenameModal] = useState(false) - const [changeDestModal, setChangeDestModal] = useState(false) - const isMedium = isMediumScreen() - - const form = useForm({ - initialValues: { proxy_port:service.proxy_port }, - validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" } - }) - - const stopService = async () => { - setButtonLoading(true) - - await porthijack.servicestop(service.service_id).then(res => { - if(!res){ - okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`) - }else{ - errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`) - }) - setButtonLoading(false); - } - - const startService = async () => { - setButtonLoading(true) - await porthijack.servicestart(service.service_id).then(res => { - if(!res){ - okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`) - }else{ - errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`) - }) - setButtonLoading(false) - } - - const deleteService = () => { - porthijack.servicedelete(service.service_id).then(res => { - if (!res){ - okNotify("Service delete complete!",`The service ${service.name} has been deleted!`) - }else - errorNotify("An error occurred while deleting a service",`Error: ${res}`) - }).catch(err => { - errorNotify("An error occurred while deleting a service",`Error: ${err}`) - }) - - } - - return <> - - - - - - - {service.name} - - - - {service.active?"ENABLED":"DISABLED"} - - {service.proto} - - - {isMedium?null:} - - - - - - FROM {service.ip_src} :{service.public_port} - - - - - TO {service.ip_dst} :{service.proxy_port} - - - - {isMedium?:} - - - Rename service - } onClick={()=>setRenameModal(true)}>Change service name - Change destination - } onClick={()=>setChangeDestModal(true)}>Change hijacking destination - - Danger zone - } onClick={()=>setDeleteModal(true)}>Delete Service - - - - - - - - - - - - - - - - - - - setDeleteModal(false) } - action={deleteService} - opened={deleteModal} - /> - setRenameModal(false)} - opened={renameModal} - service={service} - /> - setChangeDestModal(false)} - opened={changeDestModal} - service={service} - /> - -} +import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core'; +import { useState } from 'react'; +import { FaPlay, FaStop } from 'react-icons/fa'; +import { porthijack, Service } from '../utils'; +import YesNoModal from '../../YesNoModal'; +import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils'; +import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs'; +import { BiRename } from 'react-icons/bi' +import RenameForm from './RenameForm'; +import ChangeDestination from './ChangeDestination'; +import { useForm } from '@mantine/form'; +import { MenuDropDownWithButton } from '../../MainLayout'; +import { MdDoubleArrow } from "react-icons/md"; + +export default function ServiceRow({ service }:{ service:Service }) { + + let status_color = service.active ? "teal": "red" + + const [buttonLoading, setButtonLoading] = useState(false) + const [deleteModal, setDeleteModal] = useState(false) + const [renameModal, setRenameModal] = useState(false) + const [changeDestModal, setChangeDestModal] = useState(false) + const isMedium = isMediumScreen() + + const form = useForm({ + initialValues: { proxy_port:service.proxy_port }, + validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" } + }) + + const stopService = async () => { + setButtonLoading(true) + + await porthijack.servicestop(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`) + }else{ + errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`) + }) + setButtonLoading(false); + } + + const startService = async () => { + setButtonLoading(true) + await porthijack.servicestart(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`) + }else{ + errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`) + }) + setButtonLoading(false) + } + + const deleteService = () => { + porthijack.servicedelete(service.service_id).then(res => { + if (!res){ + okNotify("Service delete complete!",`The service ${service.name} has been deleted!`) + }else + errorNotify("An error occurred while deleting a service",`Error: ${res}`) + }).catch(err => { + errorNotify("An error occurred while deleting a service",`Error: ${err}`) + }) + + } + + return <> + + + + + + + {service.name} + + + + {service.active?"ENABLED":"DISABLED"} + + {service.proto} + + + {isMedium?null:} + + + + + + FROM {service.ip_src} :{service.public_port} + + + + + TO {service.ip_dst} :{service.proxy_port} + + + + {isMedium?:} + + + Rename service + } onClick={()=>setRenameModal(true)}>Change service name + Change destination + } onClick={()=>setChangeDestModal(true)}>Change hijacking destination + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + + + + + + + + + + + + + + + + setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={service} + /> + setChangeDestModal(false)} + opened={changeDestModal} + service={service} + /> + +} diff --git a/frontend/src/components/PortHijack/utils.ts b/frontend/src/components/PortHijack/utils.ts index 2c00417..7f464a3 100644 --- a/frontend/src/components/PortHijack/utils.ts +++ b/frontend/src/components/PortHijack/utils.ts @@ -1,64 +1,64 @@ -import { ServerResponse } from "../../js/models" -import { deleteapi, getapi, postapi, putapi } from "../../js/utils" -import { useQuery } from "@tanstack/react-query" - -export type GeneralStats = { - services:number -} - -export type Service = { - name:string, - service_id:string, - active:boolean, - proto: string, - ip_src: string, - ip_dst: string, - proxy_port: number, - public_port: number, -} - -export type ServiceAddForm = { - name:string, - public_port:number, - proxy_port:number, - proto:string, - ip_src: string, - ip_dst: string, -} - -export type ServiceAddResponse = ServerResponse & { service_id: string } - -export const queryKey = ["porthijack","services"] - -export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services}) - -export const porthijack = { - services: async () : Promise => { - return await getapi("porthijack/services") as Service[]; - }, - serviceinfo: async (service_id:string) => { - return await getapi(`porthijack/services/${service_id}`) as Service; - }, - servicestart: async (service_id:string) => { - const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicerename: async (service_id:string, name: string) => { - const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse; - return status === "ok"?undefined:status - }, - servicestop: async (service_id:string) => { - const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse; - return status === "ok"?undefined:status - }, - servicesadd: async (data:ServiceAddForm) => { - return await postapi("porthijack/services",data) as ServiceAddResponse; - }, - servicedelete: async (service_id:string) => { - const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse; - return status === "ok"?undefined:status - }, - changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => { - return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse; - } +import { ServerResponse } from "../../js/models" +import { deleteapi, getapi, postapi, putapi } from "../../js/utils" +import { useQuery } from "@tanstack/react-query" + +export type GeneralStats = { + services:number +} + +export type Service = { + name:string, + service_id:string, + active:boolean, + proto: string, + ip_src: string, + ip_dst: string, + proxy_port: number, + public_port: number, +} + +export type ServiceAddForm = { + name:string, + public_port:number, + proxy_port:number, + proto:string, + ip_src: string, + ip_dst: string, +} + +export type ServiceAddResponse = ServerResponse & { service_id: string } + +export const queryKey = ["porthijack","services"] + +export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services}) + +export const porthijack = { + services: async () : Promise => { + return await getapi("porthijack/services") as Service[]; + }, + serviceinfo: async (service_id:string) => { + return await getapi(`porthijack/services/${service_id}`) as Service; + }, + servicestart: async (service_id:string) => { + const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicerename: async (service_id:string, name: string) => { + const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse; + return status === "ok"?undefined:status + }, + servicestop: async (service_id:string) => { + const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicesadd: async (data:ServiceAddForm) => { + return await postapi("porthijack/services",data) as ServiceAddResponse; + }, + servicedelete: async (service_id:string) => { + const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse; + return status === "ok"?undefined:status + }, + changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => { + return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse; + } } \ No newline at end of file diff --git a/frontend/src/components/PyFilterView/index.tsx b/frontend/src/components/PyFilterView/index.tsx index 0d39d47..a40ac23 100644 --- a/frontend/src/components/PyFilterView/index.tsx +++ b/frontend/src/components/PyFilterView/index.tsx @@ -1,44 +1,44 @@ -import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; -import { useState } from 'react'; -import { PyFilter } from '../../js/models'; -import { errorNotify, isMediumScreen, okNotify } from '../../js/utils'; -import { FaPause, FaPlay } from 'react-icons/fa'; -import { FaFilter } from "react-icons/fa"; -import { nfproxy } from '../NFProxy/utils'; -import { FaPencilAlt } from 'react-icons/fa'; - -export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) { - - const isMedium = isMediumScreen() - - const changeRegexStatus = () => { - (filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => { - if(!res){ - okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`) - }else{ - errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`) - } - }).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`)) - } - - return - - - - {filterInfo.name} - - - {isMedium?<> - {filterInfo.blocked_packets} - - {filterInfo.edited_packets} - - :null} - - - {filterInfo.active?:} - - - - -} +import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; +import { useState } from 'react'; +import { PyFilter } from '../../js/models'; +import { errorNotify, isMediumScreen, okNotify } from '../../js/utils'; +import { FaPause, FaPlay } from 'react-icons/fa'; +import { FaFilter } from "react-icons/fa"; +import { nfproxy } from '../NFProxy/utils'; +import { FaPencilAlt } from 'react-icons/fa'; + +export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) { + + const isMedium = isMediumScreen() + + const changeRegexStatus = () => { + (filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => { + if(!res){ + okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`) + }else{ + errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`) + } + }).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`)) + } + + return + + + + {filterInfo.name} + + + {isMedium?<> + {filterInfo.blocked_packets} + + {filterInfo.edited_packets} + + :null} + + + {filterInfo.active?:} + + + + +} diff --git a/frontend/src/components/RegexView/index.tsx b/frontend/src/components/RegexView/index.tsx index 787ae1b..b92c05f 100644 --- a/frontend/src/components/RegexView/index.tsx +++ b/frontend/src/components/RegexView/index.tsx @@ -1,85 +1,85 @@ -import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; -import { useState } from 'react'; -import { RegexFilter } from '../../js/models'; -import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils'; -import { BsTrashFill } from "react-icons/bs" -import YesNoModal from '../YesNoModal'; -import { FaPause, FaPlay } from 'react-icons/fa'; -import { useClipboard } from '@mantine/hooks'; -import { FaFilter } from "react-icons/fa"; - -import { nfregex } from '../NFRegex/utils'; - -function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { - - const mode_string = regexInfo.mode === "C"? "C -> S": - regexInfo.mode === "S"? "S -> C": - regexInfo.mode === "B"? "C <-> S": "🤔" - - let regex_expr = b64decode(regexInfo.regex); - - const [deleteModal, setDeleteModal] = useState(false); - const clipboard = useClipboard({ timeout: 500 }); - - const deleteRegex = () => { - nfregex.regexdelete(regexInfo.id).then(res => { - if(!res){ - okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`) - }else{ - errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`) - } - }).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`)) - } - - const changeRegexStatus = () => { - (regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => { - if(!res){ - okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`) - }else{ - errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`) - } - }).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`)) - } - - return - - - - { - clipboard.copy(regex_expr) - okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`) - }}>{regex_expr} - - - - {regexInfo.active?:} - - - - setDeleteModal(true)} size="xl" radius="md" variant="filled"> - - - - - {regexInfo.n_packets} - - {regexInfo.active?"ACTIVE":"DISABLED"} - - {regexInfo.is_case_sensitive?"Strict":"Loose"} - - {mode_string} - - - setDeleteModal(false)} - action={deleteRegex} - opened={deleteModal} - /> - - -} - -export default RegexView; +import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core'; +import { useState } from 'react'; +import { RegexFilter } from '../../js/models'; +import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils'; +import { BsTrashFill } from "react-icons/bs" +import YesNoModal from '../YesNoModal'; +import { FaPause, FaPlay } from 'react-icons/fa'; +import { useClipboard } from '@mantine/hooks'; +import { FaFilter } from "react-icons/fa"; + +import { nfregex } from '../NFRegex/utils'; + +function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) { + + const mode_string = regexInfo.mode === "C"? "C -> S": + regexInfo.mode === "S"? "S -> C": + regexInfo.mode === "B"? "C <-> S": "🤔" + + let regex_expr = b64decode(regexInfo.regex); + + const [deleteModal, setDeleteModal] = useState(false); + const clipboard = useClipboard({ timeout: 500 }); + + const deleteRegex = () => { + nfregex.regexdelete(regexInfo.id).then(res => { + if(!res){ + okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`) + }else{ + errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`) + } + }).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`)) + } + + const changeRegexStatus = () => { + (regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => { + if(!res){ + okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`) + }else{ + errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`) + } + }).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`)) + } + + return + + + + { + clipboard.copy(regex_expr) + okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`) + }}>{regex_expr} + + + + {regexInfo.active?:} + + + + setDeleteModal(true)} size="xl" radius="md" variant="filled"> + + + + + {regexInfo.n_packets} + + {regexInfo.active?"ACTIVE":"DISABLED"} + + {regexInfo.is_case_sensitive?"Strict":"Loose"} + + {mode_string} + + + setDeleteModal(false)} + action={deleteRegex} + opened={deleteModal} + /> + + +} + +export default RegexView; diff --git a/frontend/src/components/YesNoModal.tsx b/frontend/src/components/YesNoModal.tsx index dbee9c9..5391b0a 100644 --- a/frontend/src/components/YesNoModal.tsx +++ b/frontend/src/components/YesNoModal.tsx @@ -1,19 +1,19 @@ -import { Button, Group, Modal } from '@mantine/core'; -import React from 'react'; - -function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){ - - return - {description} - - - - - - -} - +import { Button, Group, Modal } from '@mantine/core'; +import React from 'react'; + +function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){ + + return + {description} + + + + + + +} + export default YesNoModal; \ No newline at end of file diff --git a/frontend/src/js/utils.tsx b/frontend/src/js/utils.tsx index 4fafe54..80c1f39 100644 --- a/frontend/src/js/utils.tsx +++ b/frontend/src/js/utils.tsx @@ -1,227 +1,227 @@ -import { showNotification } from "@mantine/notifications"; -import { ImCross } from "react-icons/im"; -import { TiTick } from "react-icons/ti" -import { Navigate } from "react-router"; -import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models"; -import { Buffer } from "buffer" -import { QueryClient, useQuery } from "@tanstack/react-query"; -import { useMediaQuery } from "@mantine/hooks"; -import { io } from "socket.io-client"; -import { useAuthStore, useSessionStore } from "./store"; - -export const IS_DEV = import.meta.env.DEV - -export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"; -export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$"; -export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$" -export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" -export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$" -export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$" -export const DEV_IP_BACKEND = "127.0.0.1:4444" - -export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes - -export type EnumToPrimitiveUnion = `${T & string}` | ParseNumber<`${T & number}`>; -type ParseNumber = T extends `${infer U extends number}` ? U : never; - -export function typeCastEnum(value: EnumToPrimitiveUnion): E { - return value as E; -} - -export const socketio = import.meta.env.DEV? - io("ws://"+DEV_IP_BACKEND, { - path:"/sock/socket.io", - transports: ['websocket'], - auth: { - token: useAuthStore.getState().getAccessToken() - } - }): - io({ - path:"/sock/socket.io", - transports: ['websocket'], - auth: { - token: useAuthStore.getState().getAccessToken() - } - }) - -export const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: Infinity -} }}) - -export function getErrorMessage(e: any) { - let error = "Unknown error"; - if(typeof e == "string") return e - if (e.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - error = e.response.data.error; - } else { - // Something happened in setting up the request that triggered an Error - error = e.message || e.error; - } - return error; -} - -export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") { - if (e.status){ - return e.status - } - if (e.detail){ - if (typeof e.detail == "string") - return e.detail - if (e.detail[0] && e.detail[0].msg) - return e.detail[0].msg - } - if (e.error){ - return e.error - } - return def -} - - -export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise{ - return await new Promise((resolve, reject) => { - fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, { - method: method, - credentials: "same-origin", - cache: 'no-cache', - headers: { - ...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}), - "Authorization" : "Bearer " + useAuthStore.getState().getAccessToken() - }, - body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined - }).then(res => { - if(res.status === 401) window.location.reload() - if(res.status === 406) resolve({status:"Wrong Password"}) - if(!res.ok){ - const errorDefault = res.statusText - return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault)) - } - res.text().then(t => { - try{ - resolve(JSON.parse(t)) - }catch(e){ - resolve(t) - } - }).catch( err => reject(err)) - }).catch(err => { - reject(err) - }) - }); -} - -export async function getapi(path:string):Promise{ - return await genericapi("GET",path) -} - -export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise{ - return await genericapi("POST",path,data,is_form) -} - -export async function deleteapi(path:string):Promise{ - return await genericapi("DELETE",path) -} - -export async function putapi(path:string,data:any):Promise{ - return await genericapi("PUT",path,data) -} - -export function getMainPath(){ - const paths = window.location.pathname.split("/") - if (paths.length > 1) return paths[1] - return "" -} - -export function HomeRedirector(){ - const section = useSessionStore.getState().getHomeSection(); - const path = section?`/${section}`:`/nfregex` - return -} - -export async function resetfiregex(delete_data:boolean = false){ - const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse; - return (status === "ok"?undefined:status) -} - -export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces) - -export async function getipinterfaces(){ - return await getapi("interfaces") as IpInterface[]; -} - -export async function getstatus(){ - return await getapi(`status`) as ServerStatusResponse; -} - -export async function logout(){ - useAuthStore.getState().clearAccessToken(); -} - -export async function setpassword(data:PasswordSend) { - const { status, access_token } = await postapi("set-password",data) as ServerResponseToken; - if (access_token) - useAuthStore.getState().setAccessToken(access_token); - return status === "ok"?undefined:status -} - -export async function changepassword(data:ChangePassword) { - const { status, access_token } = await postapi("change-password",data) as ServerResponseToken; - if (access_token) - useAuthStore.getState().setAccessToken(access_token); - return status === "ok"?undefined:status -} - -export async function login(data:PasswordSend) { - const from = {username: "login", password: data.password}; - const { status, access_token } = await postapi("login",from,true) as LoginResponse; - useAuthStore.getState().setAccessToken(access_token); - return status; -} - -export function errorNotify(title:string, description:string ){ - showNotification({ - autoClose: 2000, - title: title, - message: description, - color: 'red', - icon: , - }); -} - -export function okNotify(title:string, description:string ){ - showNotification({ - autoClose: 2000, - title: title, - message: description, - color: 'teal', - icon: , - }); -} - -export const makeid = (length:number) => { - let result = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - let counter = 0; - while (counter < length) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - counter += 1; - } - return result; -} - -export function b64encode(data:number[]|string){ - return Buffer.from(data).toString('base64') -} - -export function b64decode(regexB64:string){ - return Buffer.from(regexB64, "base64").toString() -} - -export function isMediumScreen(){ - return useMediaQuery('(min-width: 600px)'); -} - -export function isLargeScreen(){ - return useMediaQuery('(min-width: 992px)'); +import { showNotification } from "@mantine/notifications"; +import { ImCross } from "react-icons/im"; +import { TiTick } from "react-icons/ti" +import { Navigate } from "react-router"; +import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models"; +import { Buffer } from "buffer" +import { QueryClient, useQuery } from "@tanstack/react-query"; +import { useMediaQuery } from "@mantine/hooks"; +import { io } from "socket.io-client"; +import { useAuthStore, useSessionStore } from "./store"; + +export const IS_DEV = import.meta.env.DEV + +export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$"; +export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$"; +export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$" +export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" +export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$" +export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$" +export const DEV_IP_BACKEND = "127.0.0.1:4444" + +export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes + +export type EnumToPrimitiveUnion = `${T & string}` | ParseNumber<`${T & number}`>; +type ParseNumber = T extends `${infer U extends number}` ? U : never; + +export function typeCastEnum(value: EnumToPrimitiveUnion): E { + return value as E; +} + +export const socketio = import.meta.env.DEV? + io("ws://"+DEV_IP_BACKEND, { + path:"/sock/socket.io", + transports: ['websocket'], + auth: { + token: useAuthStore.getState().getAccessToken() + } + }): + io({ + path:"/sock/socket.io", + transports: ['websocket'], + auth: { + token: useAuthStore.getState().getAccessToken() + } + }) + +export const queryClient = new QueryClient({ defaultOptions: { queries: { + staleTime: Infinity +} }}) + +export function getErrorMessage(e: any) { + let error = "Unknown error"; + if(typeof e == "string") return e + if (e.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + error = e.response.data.error; + } else { + // Something happened in setting up the request that triggered an Error + error = e.message || e.error; + } + return error; +} + +export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") { + if (e.status){ + return e.status + } + if (e.detail){ + if (typeof e.detail == "string") + return e.detail + if (e.detail[0] && e.detail[0].msg) + return e.detail[0].msg + } + if (e.error){ + return e.error + } + return def +} + + +export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise{ + return await new Promise((resolve, reject) => { + fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, { + method: method, + credentials: "same-origin", + cache: 'no-cache', + headers: { + ...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}), + "Authorization" : "Bearer " + useAuthStore.getState().getAccessToken() + }, + body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined + }).then(res => { + if(res.status === 401) window.location.reload() + if(res.status === 406) resolve({status:"Wrong Password"}) + if(!res.ok){ + const errorDefault = res.statusText + return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault)) + } + res.text().then(t => { + try{ + resolve(JSON.parse(t)) + }catch(e){ + resolve(t) + } + }).catch( err => reject(err)) + }).catch(err => { + reject(err) + }) + }); +} + +export async function getapi(path:string):Promise{ + return await genericapi("GET",path) +} + +export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise{ + return await genericapi("POST",path,data,is_form) +} + +export async function deleteapi(path:string):Promise{ + return await genericapi("DELETE",path) +} + +export async function putapi(path:string,data:any):Promise{ + return await genericapi("PUT",path,data) +} + +export function getMainPath(){ + const paths = window.location.pathname.split("/") + if (paths.length > 1) return paths[1] + return "" +} + +export function HomeRedirector(){ + const section = useSessionStore.getState().getHomeSection(); + const path = section?`/${section}`:`/nfregex` + return +} + +export async function resetfiregex(delete_data:boolean = false){ + const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse; + return (status === "ok"?undefined:status) +} + +export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces) + +export async function getipinterfaces(){ + return await getapi("interfaces") as IpInterface[]; +} + +export async function getstatus(){ + return await getapi(`status`) as ServerStatusResponse; +} + +export async function logout(){ + useAuthStore.getState().clearAccessToken(); +} + +export async function setpassword(data:PasswordSend) { + const { status, access_token } = await postapi("set-password",data) as ServerResponseToken; + if (access_token) + useAuthStore.getState().setAccessToken(access_token); + return status === "ok"?undefined:status +} + +export async function changepassword(data:ChangePassword) { + const { status, access_token } = await postapi("change-password",data) as ServerResponseToken; + if (access_token) + useAuthStore.getState().setAccessToken(access_token); + return status === "ok"?undefined:status +} + +export async function login(data:PasswordSend) { + const from = {username: "login", password: data.password}; + const { status, access_token } = await postapi("login",from,true) as LoginResponse; + useAuthStore.getState().setAccessToken(access_token); + return status; +} + +export function errorNotify(title:string, description:string ){ + showNotification({ + autoClose: 2000, + title: title, + message: description, + color: 'red', + icon: , + }); +} + +export function okNotify(title:string, description:string ){ + showNotification({ + autoClose: 2000, + title: title, + message: description, + color: 'teal', + icon: , + }); +} + +export const makeid = (length:number) => { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + let counter = 0; + while (counter < length) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + counter += 1; + } + return result; +} + +export function b64encode(data:number[]|string){ + return Buffer.from(data).toString('base64') +} + +export function b64decode(regexB64:string){ + return Buffer.from(regexB64, "base64").toString() +} + +export function isMediumScreen(){ + return useMediaQuery('(min-width: 600px)'); +} + +export function isLargeScreen(){ + return useMediaQuery('(min-width: 992px)'); } \ No newline at end of file diff --git a/frontend/src/pages/NFProxy/ServiceDetails.tsx b/frontend/src/pages/NFProxy/ServiceDetails.tsx index 3533bc6..5dad9bf 100644 --- a/frontend/src/pages/NFProxy/ServiceDetails.tsx +++ b/frontend/src/pages/NFProxy/ServiceDetails.tsx @@ -1,237 +1,243 @@ -import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; -import { Navigate, useNavigate, useParams } from 'react-router'; -import { Badge, Divider, Menu } from '@mantine/core'; -import { useEffect, useState } from 'react'; -import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa'; -import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils'; -import { MdDoubleArrow } from "react-icons/md" -import YesNoModal from '../../components/YesNoModal'; -import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils'; -import { BsTrashFill } from 'react-icons/bs'; -import { BiRename } from 'react-icons/bi' -import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm'; -import { MenuDropDownWithButton } from '../../components/MainLayout'; -import { useQueryClient } from '@tanstack/react-query'; -import { FaArrowLeft } from "react-icons/fa"; -import { IoSettingsSharp } from 'react-icons/io5'; -import AddEditService from '../../components/NFProxy/AddEditService'; -import PyFilterView from '../../components/PyFilterView'; -import { TbPlugConnected } from 'react-icons/tb'; -import { CodeHighlight } from '@mantine/code-highlight'; -import { FaPython } from "react-icons/fa"; -import { FiFileText } from "react-icons/fi"; -import { ModalLog } from '../../components/ModalLog'; -import { useListState } from '@mantine/hooks'; -import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning'; -import { DocsButton } from '../../components/DocsButton'; - -export default function ServiceDetailsNFProxy() { - - const {srv} = useParams() - const services = nfproxyServiceQuery() - const serviceInfo = services.data?.find(s => s.service_id == srv) - const filtersList = nfproxyServicePyfiltersQuery(srv??"") - const [deleteModal, setDeleteModal] = useState(false) - const [renameModal, setRenameModal] = useState(false) - const [editModal, setEditModal] = useState(false) - const [buttonLoading, setButtonLoading] = useState(false) - const queryClient = useQueryClient() - const filterCode = nfproxyServiceFilterCodeQuery(srv??"") - const navigate = useNavigate() - const isMedium = isMediumScreen() - const [openLogModal, setOpenLogModal] = useState(false) - const [logData, logDataSetters] = useListState([]); - - - useEffect(()=>{ - if (srv){ - if (openLogModal){ - logDataSetters.setState([]) - socketio.emit("nfproxy-outstream-join", { service: srv }); - socketio.on(`nfproxy-outstream-${srv}`, (data) => { - logDataSetters.append(data) - }); - }else{ - socketio.emit("nfproxy-outstream-leave", { service: srv }); - socketio.off(`nfproxy-outstream-${srv}`); - logDataSetters.setState([]) - } - return () => { - socketio.emit("nfproxy-outstream-leave", { service: srv }); - socketio.off(`nfproxy-outstream-${srv}`); - logDataSetters.setState([]) - } - } - }, [openLogModal, srv]) - - if (services.isLoading) return - if (!srv || !serviceInfo || filtersList.isError) return - - let status_color = "gray"; - switch(serviceInfo.status){ - case "stop": status_color = "red"; break; - case "active": status_color = "teal"; break; - } - - const startService = async () => { - setButtonLoading(true) - await nfproxy.servicestart(serviceInfo.service_id).then(res => { - if(!res){ - okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`) - }) - setButtonLoading(false) - } - - const deleteService = () => { - nfproxy.servicedelete(serviceInfo.service_id).then(res => { - if (!res){ - okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`) - queryClient.invalidateQueries(serviceQueryKey) - }else - errorNotify("An error occurred while deleting a service",`Error: ${res}`) - }).catch(err => { - errorNotify("An error occurred while deleting a service",`Error: ${err}`) - }) - } - - const stopService = async () => { - setButtonLoading(true) - - await nfproxy.servicestop(serviceInfo.service_id).then(res => { - if(!res){ - okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`) - }) - setButtonLoading(false); - } - - return <> - - - - - <Box className="center-flex"> - <MdDoubleArrow /><Space w="sm" />{serviceInfo.name} - </Box> - - - {isMedium?null:} - - - - - {serviceInfo.status} - - - :{serviceInfo.port} - - - - Edit service - } onClick={()=>setEditModal(true)}>Service Settings - } onClick={()=>setRenameModal(true)}>Change service name - - Danger zone - } onClick={()=>setDeleteModal(true)}>Delete Service - - - - setOpenLogModal(true)} loading={buttonLoading} variant="filled"> - - - - - - {isMedium?null:} - - - - {serviceInfo.edited_packets} - - {serviceInfo.blocked_packets} - - {serviceInfo.n_filters} - - {isMedium?:} - {serviceInfo.ip_int} on {serviceInfo.proto} - - {isMedium?null:} - - - navigate("/")} size="xl" radius="md" variant="filled" - aria-describedby="tooltip-back-id"> - - - - - - - - - - - - - - - - - - - - - {filterCode.data?<> - <FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code - - : null} - - {(!filtersList.data || filtersList.data.length == 0)?<> - - No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code> - - Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/> - - Then create a new filter file with the following syntax and upload it here (using the button above) - :<>{filtersList.data?.map( (filterInfo) => )} - } - setDeleteModal(false) } - action={deleteService} - opened={deleteModal} - /> - setRenameModal(false)} - opened={renameModal} - service={serviceInfo} - /> - setEditModal(false)} - edit={serviceInfo} - /> - setOpenLogModal(false)} - title={`Logs for service ${serviceInfo.name}`} - data={logData.join("")} - /> - -} +import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; +import { Navigate, useNavigate, useParams } from 'react-router'; +import { Badge, Divider, Menu } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa'; +import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils'; +import { MdDoubleArrow } from "react-icons/md" +import YesNoModal from '../../components/YesNoModal'; +import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils'; +import { BsTrashFill } from 'react-icons/bs'; +import { BiRename } from 'react-icons/bi' +import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm'; +import { MenuDropDownWithButton } from '../../components/MainLayout'; +import { useQueryClient } from '@tanstack/react-query'; +import { FaArrowLeft } from "react-icons/fa"; +import { IoSettingsSharp } from 'react-icons/io5'; +import AddEditService from '../../components/NFProxy/AddEditService'; +import PyFilterView from '../../components/PyFilterView'; +import { TbPlugConnected } from 'react-icons/tb'; +import { CodeHighlight } from '@mantine/code-highlight'; +import { FaPython } from "react-icons/fa"; +import { FiFileText } from "react-icons/fi"; +import { ModalLog } from '../../components/ModalLog'; +import { useListState } from '@mantine/hooks'; +import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning'; +import { DocsButton } from '../../components/DocsButton'; + +export default function ServiceDetailsNFProxy() { + + const {srv} = useParams() + const services = nfproxyServiceQuery() + const serviceInfo = services.data?.find(s => s.service_id == srv) + const filtersList = nfproxyServicePyfiltersQuery(srv??"") + const [deleteModal, setDeleteModal] = useState(false) + const [renameModal, setRenameModal] = useState(false) + const [editModal, setEditModal] = useState(false) + const [buttonLoading, setButtonLoading] = useState(false) + const queryClient = useQueryClient() + const filterCode = nfproxyServiceFilterCodeQuery(srv??"") + const navigate = useNavigate() + const isMedium = isMediumScreen() + const [openLogModal, setOpenLogModal] = useState(false) + const [logData, logDataSetters] = useListState([]); + + + useEffect(()=>{ + if (srv){ + if (openLogModal){ + logDataSetters.setState([]) + socketio.emit("nfproxy-outstream-join", { service: srv }); + socketio.on(`nfproxy-outstream-${srv}`, (data) => { + logDataSetters.append(data) + }); + }else{ + socketio.emit("nfproxy-outstream-leave", { service: srv }); + socketio.off(`nfproxy-outstream-${srv}`); + logDataSetters.setState([]) + } + return () => { + socketio.emit("nfproxy-outstream-leave", { service: srv }); + socketio.off(`nfproxy-outstream-${srv}`); + logDataSetters.setState([]) + } + } + }, [openLogModal, srv]) + + if (services.isLoading) return + if (!srv || !serviceInfo || filtersList.isError) return + + let status_color = "gray"; + switch(serviceInfo.status){ + case "stop": status_color = "red"; break; + case "active": status_color = "teal"; break; + } + + const startService = async () => { + setButtonLoading(true) + await nfproxy.servicestart(serviceInfo.service_id).then(res => { + if(!res){ + okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`) + }) + setButtonLoading(false) + } + + const deleteService = () => { + nfproxy.servicedelete(serviceInfo.service_id).then(res => { + if (!res){ + okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`) + queryClient.invalidateQueries(serviceQueryKey) + }else + errorNotify("An error occurred while deleting a service",`Error: ${res}`) + }).catch(err => { + errorNotify("An error occurred while deleting a service",`Error: ${err}`) + }) + } + + const stopService = async () => { + setButtonLoading(true) + + await nfproxy.servicestop(serviceInfo.service_id).then(res => { + if(!res){ + okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`) + }) + setButtonLoading(false); + } + + return <> + + + + + <Box className="center-flex"> + <MdDoubleArrow /><Space w="sm" />{serviceInfo.name} + </Box> + + + {isMedium?null:} + + + + + {serviceInfo.status} + + + :{serviceInfo.port} + + + + Edit service + } onClick={()=>setEditModal(true)}>Service Settings + } onClick={()=>setRenameModal(true)}>Change service name + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + setOpenLogModal(true)} loading={buttonLoading} variant="filled"> + + + + + + navigate(`/nfproxy/${srv}/traffic`)} variant="filled"> + + + + + + {isMedium?null:} + + + + {serviceInfo.edited_packets} + + {serviceInfo.blocked_packets} + + {serviceInfo.n_filters} + + {isMedium?:} + {serviceInfo.ip_int} on {serviceInfo.proto} + + {isMedium?null:} + + + navigate("/")} size="xl" radius="md" variant="filled" + aria-describedby="tooltip-back-id"> + + + + + + + + + + + + + + + + + + + + + {filterCode.data?<> + <FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code + + : null} + + {(!filtersList.data || filtersList.data.length == 0)?<> + + No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code> + + Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/> + + Then create a new filter file with the following syntax and upload it here (using the button above) + :<>{filtersList.data?.map( (filterInfo) => )} + } + setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={serviceInfo} + /> + setEditModal(false)} + edit={serviceInfo} + /> + setOpenLogModal(false)} + title={`Logs for service ${serviceInfo.name}`} + data={logData.join("")} + /> + +} diff --git a/frontend/src/pages/NFProxy/TrafficViewer.tsx b/frontend/src/pages/NFProxy/TrafficViewer.tsx new file mode 100644 index 0000000..1562d0c --- /dev/null +++ b/frontend/src/pages/NFProxy/TrafficViewer.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [filterText, setFilterText] = useState(''); + const [selectedEvent, setSelectedEvent] = useState(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 ; + if (!srv || !serviceInfo) return ; + + 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 <> + + + + <Box className="center-flex"> + <MdDoubleArrow /><Space w="sm" />Traffic Viewer - {serviceInfo.name} + </Box> + + + + {filteredEvents.length} events + + + + + + + + + navigate(-1)} size="lg" radius="md" variant="filled"> + + + + + + + + + + ) => setFilterText(e.currentTarget.value)} + leftSection={} + mb="md" + /> + + + + + + Time + Direction + Source + Destination + Protocol + Size + Filter + Verdict + + + + {filteredEvents.length === 0 ? ( + + + + {filterText ? 'No events match your filter' : 'No traffic events yet. Waiting for traffic...'} + + + + ) : ( + filteredEvents.slice(-500).reverse().map((event: TrafficEvent, idx: number) => ( + openDetails(event)} style={{ cursor: 'pointer' }}> + {formatTimestamp(event.ts)} + + + {event.direction || 'unknown'} + + + {event.src_ip || '-'}:{event.src_port || '-'} + {event.dst_ip || '-'}:{event.dst_port || '-'} + {event.proto || 'unknown'} + {event.size ? `${event.size} B` : '-'} + {event.filter || '-'} + + + {event.verdict || 'unknown'} + + + + )) + )} + +
+
+
+ + {/* Payload details modal */} + setModalOpened(false)} + title="Event Details" + size="xl" + > + {selectedEvent && ( + + + Timestamp: {formatTimestamp(selectedEvent.ts)} + Direction: {selectedEvent.direction || 'unknown'} + Source: {selectedEvent.src_ip}:{selectedEvent.src_port} + Destination: {selectedEvent.dst_ip}:{selectedEvent.dst_port} + Protocol: {selectedEvent.proto || 'unknown'} + Size: {selectedEvent.size ? `${selectedEvent.size} B` : '-'} + Filter: {selectedEvent.filter || '-'} + Verdict: {selectedEvent.verdict || 'unknown'} + + {selectedEvent.sample_hex && ( + <> + + + {selectedEvent.sample_hex} + + + )} + + )} + + ; +} diff --git a/frontend/src/pages/NFProxy/index.tsx b/frontend/src/pages/NFProxy/index.tsx index df69d95..560597c 100644 --- a/frontend/src/pages/NFProxy/index.tsx +++ b/frontend/src/pages/NFProxy/index.tsx @@ -1,172 +1,172 @@ -import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core'; -import { useEffect, useState } from 'react'; -import { BsPlusLg } from "react-icons/bs"; -import { useNavigate, useParams } from 'react-router'; -import ServiceRow from '../../components/NFProxy/ServiceRow'; -import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; -import AddEditService from '../../components/NFProxy/AddEditService'; -import { useQueryClient } from '@tanstack/react-query'; -import { TbPlugConnected, TbReload } from 'react-icons/tb'; -import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils'; -import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa'; -import { MdUploadFile } from "react-icons/md"; -import { notifications } from '@mantine/notifications'; -import { useFileDialog } from '@mantine/hooks'; -import { CodeHighlight } from '@mantine/code-highlight'; -import { DocsButton } from '../../components/DocsButton'; - - -export default function NFProxy({ children }: { children: any }) { - - const navigator = useNavigate() - const [open, setOpen] = useState(false); - const {srv} = useParams() - const queryClient = useQueryClient() - const isMedium = isMediumScreen() - const services = nfproxyServiceQuery() - const fileDialog = useFileDialog({ - accept: ".py", - multiple: false, - resetOnOpen: true, - onChange: (files) => { - if (files?.length??0 > 0) - setFile(files![0]) - } - }); - const [file, setFile] = useState(null); - useEffect(() => { - if (!srv) return - const service = services.data?.find(s => s.service_id === srv) - if (!service) return - if (file){ - console.log("Uploading code") - const notify_id = notifications.show( - { - title: "Uploading code", - message: `Uploading code for service ${service.name}`, - color: "blue", - icon: , - autoClose: false, - loading: true, - } - ) - file.text() - .then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString())) - .then( res => { - if (!res){ - notifications.update({ - id: notify_id, - title: "Code uploaded", - message: `Successfully uploaded code for service ${service.name}`, - color: "green", - icon: , - autoClose: 5000, - loading: false, - }) - }else{ - notifications.update({ - id: notify_id, - title: "Code upload failed", - message: `Error: ${res}`, - color: "red", - icon: , - autoClose: 5000, - loading: false, - }) - } - }).catch( err => { - notifications.update({ - id: notify_id, - title: "Code upload failed", - message: `Error: ${err}`, - color: "red", - icon: , - autoClose: 5000, - loading: false, - }) - }).finally(()=>{setFile(null)}) - } - }, [file]) - - useEffect(()=> { - if(services.isError) - errorNotify("NFProxy Update failed!", getErrorMessage(services.error)) - },[services.isError]) - - const closeModal = () => {setOpen(false);} - - return <> - - - <ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy - {isMedium?:} - - {isMedium?"General stats:":null} - - Services: {services.isLoading?0:services.data?.length} - - {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)} - - {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)} - - {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)} - - - {isMedium?null:} - - { srv? - - - - - - : - setOpen(true)} size="lg" radius="md" variant="filled"> - - - - } - - - queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}> - - - - - - - - - - {srv?null:<> - - {(services.data && services.data?.length > 0)?services.data.map( srv => { - navigator("/nfproxy/"+srv.service_id) - }} />):<> - - - Netfilter proxy is a simulated proxy written using python with a c++ core - - Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code> - - Then you can create a new service and write custom filters for the service - - - - setOpen(true)} size="xl" radius="md" variant="filled"> - - - - - - - } - } - - {srv?children:null} - {!srv? - :null - } - -} - +import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import { BsPlusLg } from "react-icons/bs"; +import { useNavigate, useParams } from 'react-router'; +import ServiceRow from '../../components/NFProxy/ServiceRow'; +import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; +import AddEditService from '../../components/NFProxy/AddEditService'; +import { useQueryClient } from '@tanstack/react-query'; +import { TbPlugConnected, TbReload } from 'react-icons/tb'; +import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils'; +import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa'; +import { MdUploadFile } from "react-icons/md"; +import { notifications } from '@mantine/notifications'; +import { useFileDialog } from '@mantine/hooks'; +import { CodeHighlight } from '@mantine/code-highlight'; +import { DocsButton } from '../../components/DocsButton'; + + +export default function NFProxy({ children }: { children: any }) { + + const navigator = useNavigate() + const [open, setOpen] = useState(false); + const {srv} = useParams() + const queryClient = useQueryClient() + const isMedium = isMediumScreen() + const services = nfproxyServiceQuery() + const fileDialog = useFileDialog({ + accept: ".py", + multiple: false, + resetOnOpen: true, + onChange: (files) => { + if (files?.length??0 > 0) + setFile(files![0]) + } + }); + const [file, setFile] = useState(null); + useEffect(() => { + if (!srv) return + const service = services.data?.find(s => s.service_id === srv) + if (!service) return + if (file){ + console.log("Uploading code") + const notify_id = notifications.show( + { + title: "Uploading code", + message: `Uploading code for service ${service.name}`, + color: "blue", + icon: , + autoClose: false, + loading: true, + } + ) + file.text() + .then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString())) + .then( res => { + if (!res){ + notifications.update({ + id: notify_id, + title: "Code uploaded", + message: `Successfully uploaded code for service ${service.name}`, + color: "green", + icon: , + autoClose: 5000, + loading: false, + }) + }else{ + notifications.update({ + id: notify_id, + title: "Code upload failed", + message: `Error: ${res}`, + color: "red", + icon: , + autoClose: 5000, + loading: false, + }) + } + }).catch( err => { + notifications.update({ + id: notify_id, + title: "Code upload failed", + message: `Error: ${err}`, + color: "red", + icon: , + autoClose: 5000, + loading: false, + }) + }).finally(()=>{setFile(null)}) + } + }, [file]) + + useEffect(()=> { + if(services.isError) + errorNotify("NFProxy Update failed!", getErrorMessage(services.error)) + },[services.isError]) + + const closeModal = () => {setOpen(false);} + + return <> + + + <ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy + {isMedium?:} + + {isMedium?"General stats:":null} + + Services: {services.isLoading?0:services.data?.length} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)} + + + {isMedium?null:} + + { srv? + + + + + + : + setOpen(true)} size="lg" radius="md" variant="filled"> + + + + } + + + queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}> + + + + + + + + + + {srv?null:<> + + {(services.data && services.data?.length > 0)?services.data.map( srv => { + navigator("/nfproxy/"+srv.service_id) + }} />):<> + + + Netfilter proxy is a simulated proxy written using python with a c++ core + + Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code> + + Then you can create a new service and write custom filters for the service + + + + setOpen(true)} size="xl" radius="md" variant="filled"> + + + + + + + } + } + + {srv?children:null} + {!srv? + :null + } + +} + diff --git a/frontend/src/pages/NFRegex/ServiceDetails.tsx b/frontend/src/pages/NFRegex/ServiceDetails.tsx index c68f814..7b22509 100644 --- a/frontend/src/pages/NFRegex/ServiceDetails.tsx +++ b/frontend/src/pages/NFRegex/ServiceDetails.tsx @@ -1,194 +1,194 @@ -import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; -import { Navigate, useNavigate, useParams } from 'react-router'; -import RegexView from '../../components/RegexView'; -import AddNewRegex from '../../components/AddNewRegex'; -import { BsPlusLg } from "react-icons/bs"; -import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils'; -import { Badge, Divider, Menu } from '@mantine/core'; -import { useState } from 'react'; -import { FaFilter, FaPlay, FaStop } from 'react-icons/fa'; -import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils'; -import { MdDoubleArrow } from "react-icons/md" -import YesNoModal from '../../components/YesNoModal'; -import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils'; -import { BsTrashFill } from 'react-icons/bs'; -import { BiRename } from 'react-icons/bi' -import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm'; -import { MenuDropDownWithButton } from '../../components/MainLayout'; -import { useQueryClient } from '@tanstack/react-query'; -import { FaArrowLeft } from "react-icons/fa"; -import { VscRegex } from 'react-icons/vsc'; -import { IoSettingsSharp } from 'react-icons/io5'; -import AddEditService from '../../components/NFRegex/AddEditService'; - -export default function ServiceDetailsNFRegex() { - - const {srv} = useParams() - const [open, setOpen] = useState(false) - const services = nfregexServiceQuery() - const serviceInfo = services.data?.find(s => s.service_id == srv) - const regexesList = nfregexServiceRegexesQuery(srv??"") - const [deleteModal, setDeleteModal] = useState(false) - const [renameModal, setRenameModal] = useState(false) - const [editModal, setEditModal] = useState(false) - const [buttonLoading, setButtonLoading] = useState(false) - const queryClient = useQueryClient() - const navigate = useNavigate() - const isMedium = isMediumScreen() - - if (services.isLoading) return - if (!srv || !serviceInfo || regexesList.isError) return - - let status_color = "gray"; - switch(serviceInfo.status){ - case "stop": status_color = "red"; break; - case "active": status_color = "teal"; break; - } - - const startService = async () => { - setButtonLoading(true) - await nfregex.servicestart(serviceInfo.service_id).then(res => { - if(!res){ - okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`) - }) - setButtonLoading(false) - } - - const deleteService = () => { - nfregex.servicedelete(serviceInfo.service_id).then(res => { - if (!res){ - okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`) - queryClient.invalidateQueries(serviceQueryKey) - }else - errorNotify("An error occurred while deleting a service",`Error: ${res}`) - }).catch(err => { - errorNotify("An error occurred while deleting a service",`Error: ${err}`) - }) - } - - const stopService = async () => { - setButtonLoading(true) - - await nfregex.servicestop(serviceInfo.service_id).then(res => { - if(!res){ - okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`) - queryClient.invalidateQueries(serviceQueryKey) - }else{ - errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`) - } - }).catch(err => { - errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`) - }) - setButtonLoading(false); - } - - return <> - - - - - <Box className="center-flex"> - <MdDoubleArrow /><Space w="sm" />{serviceInfo.name} - </Box> - - - {isMedium?null:} - - - {serviceInfo.status} - - - :{serviceInfo.port} - - - - Edit service - } onClick={()=>setEditModal(true)}>Service Settings - } onClick={()=>setRenameModal(true)}>Change service name - - Danger zone - } onClick={()=>setDeleteModal(true)}>Delete Service - - - - {isMedium?null:} - - - - {serviceInfo.n_packets} - - {serviceInfo.n_regex} - - {isMedium?:} - {serviceInfo.ip_int} on {serviceInfo.proto} - - {isMedium?null:} - - - navigate("/")} size="xl" radius="md" variant="filled" - aria-describedby="tooltip-back-id"> - - - - - - - - - - - - - - - - - - - {(!regexesList.data || regexesList.data.length == 0)?<> - - No regex found for this service! Add one by clicking the "+" buttons - - - - setOpen(true)} size="xl" radius="md" variant="filled" - aria-describedby="tooltip-AddRegex-id"> - - - : - - {regexesList.data?.map( (regexInfo) => )} - - } - - {srv? {setOpen(false);}} service={srv} />:null} - setDeleteModal(false) } - action={deleteService} - opened={deleteModal} - /> - setRenameModal(false)} - opened={renameModal} - service={serviceInfo} - /> - setEditModal(false)} - edit={serviceInfo} - /> - -} +import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; +import { Navigate, useNavigate, useParams } from 'react-router'; +import RegexView from '../../components/RegexView'; +import AddNewRegex from '../../components/AddNewRegex'; +import { BsPlusLg } from "react-icons/bs"; +import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils'; +import { Badge, Divider, Menu } from '@mantine/core'; +import { useState } from 'react'; +import { FaFilter, FaPlay, FaStop } from 'react-icons/fa'; +import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils'; +import { MdDoubleArrow } from "react-icons/md" +import YesNoModal from '../../components/YesNoModal'; +import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils'; +import { BsTrashFill } from 'react-icons/bs'; +import { BiRename } from 'react-icons/bi' +import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm'; +import { MenuDropDownWithButton } from '../../components/MainLayout'; +import { useQueryClient } from '@tanstack/react-query'; +import { FaArrowLeft } from "react-icons/fa"; +import { VscRegex } from 'react-icons/vsc'; +import { IoSettingsSharp } from 'react-icons/io5'; +import AddEditService from '../../components/NFRegex/AddEditService'; + +export default function ServiceDetailsNFRegex() { + + const {srv} = useParams() + const [open, setOpen] = useState(false) + const services = nfregexServiceQuery() + const serviceInfo = services.data?.find(s => s.service_id == srv) + const regexesList = nfregexServiceRegexesQuery(srv??"") + const [deleteModal, setDeleteModal] = useState(false) + const [renameModal, setRenameModal] = useState(false) + const [editModal, setEditModal] = useState(false) + const [buttonLoading, setButtonLoading] = useState(false) + const queryClient = useQueryClient() + const navigate = useNavigate() + const isMedium = isMediumScreen() + + if (services.isLoading) return + if (!srv || !serviceInfo || regexesList.isError) return + + let status_color = "gray"; + switch(serviceInfo.status){ + case "stop": status_color = "red"; break; + case "active": status_color = "teal"; break; + } + + const startService = async () => { + setButtonLoading(true) + await nfregex.servicestart(serviceInfo.service_id).then(res => { + if(!res){ + okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`) + }) + setButtonLoading(false) + } + + const deleteService = () => { + nfregex.servicedelete(serviceInfo.service_id).then(res => { + if (!res){ + okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`) + queryClient.invalidateQueries(serviceQueryKey) + }else + errorNotify("An error occurred while deleting a service",`Error: ${res}`) + }).catch(err => { + errorNotify("An error occurred while deleting a service",`Error: ${err}`) + }) + } + + const stopService = async () => { + setButtonLoading(true) + + await nfregex.servicestop(serviceInfo.service_id).then(res => { + if(!res){ + okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`) + queryClient.invalidateQueries(serviceQueryKey) + }else{ + errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`) + }) + setButtonLoading(false); + } + + return <> + + + + + <Box className="center-flex"> + <MdDoubleArrow /><Space w="sm" />{serviceInfo.name} + </Box> + + + {isMedium?null:} + + + {serviceInfo.status} + + + :{serviceInfo.port} + + + + Edit service + } onClick={()=>setEditModal(true)}>Service Settings + } onClick={()=>setRenameModal(true)}>Change service name + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + {isMedium?null:} + + + + {serviceInfo.n_packets} + + {serviceInfo.n_regex} + + {isMedium?:} + {serviceInfo.ip_int} on {serviceInfo.proto} + + {isMedium?null:} + + + navigate("/")} size="xl" radius="md" variant="filled" + aria-describedby="tooltip-back-id"> + + + + + + + + + + + + + + + + + + + {(!regexesList.data || regexesList.data.length == 0)?<> + + No regex found for this service! Add one by clicking the "+" buttons + + + + setOpen(true)} size="xl" radius="md" variant="filled" + aria-describedby="tooltip-AddRegex-id"> + + + : + + {regexesList.data?.map( (regexInfo) => )} + + } + + {srv? {setOpen(false);}} service={srv} />:null} + setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={serviceInfo} + /> + setEditModal(false)} + edit={serviceInfo} + /> + +} diff --git a/frontend/src/pages/NFRegex/index.tsx b/frontend/src/pages/NFRegex/index.tsx index 7903b74..b378cb6 100644 --- a/frontend/src/pages/NFRegex/index.tsx +++ b/frontend/src/pages/NFRegex/index.tsx @@ -1,100 +1,100 @@ -import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core'; -import { useEffect, useState } from 'react'; -import { BsPlusLg, BsRegex } from "react-icons/bs"; -import { useNavigate, useParams } from 'react-router'; -import ServiceRow from '../../components/NFRegex/ServiceRow'; -import { nfregexServiceQuery } from '../../components/NFRegex/utils'; -import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; -import AddEditService from '../../components/NFRegex/AddEditService'; -import AddNewRegex from '../../components/AddNewRegex'; -import { useQueryClient } from '@tanstack/react-query'; -import { TbReload } from 'react-icons/tb'; -import { FaFilter } from 'react-icons/fa'; -import { FaServer } from "react-icons/fa6"; -import { VscRegex } from "react-icons/vsc"; -import { DocsButton } from '../../components/DocsButton'; - -function NFRegex({ children }: { children: any }) { - - const navigator = useNavigate() - const [open, setOpen] = useState(false); - const {srv} = useParams() - const queryClient = useQueryClient() - const isMedium = isMediumScreen() - const services = nfregexServiceQuery() - - useEffect(()=> { - if(services.isError) - errorNotify("NFRegex Update failed!", getErrorMessage(services.error)) - },[services.isError]) - - const closeModal = () => {setOpen(false);} - - return <> - - - <ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex - {isMedium?:} - - {isMedium?"General stats:":null} - - Services: {services.isLoading?0:services.data?.length} - - {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)} - - {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)} - - - {isMedium?null:} - - { srv? - - setOpen(true)} size="lg" radius="md" variant="filled"> - - : - setOpen(true)} size="lg" radius="md" variant="filled"> - - } - - - queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled" - loading={services.isFetching}> - - - - - - - - {srv?null:<> - - {(services.data && services.data?.length > 0)?services.data.map( srv => { - navigator("/nfregex/"+srv.service_id) - }} />):<> - - - Netfilter Regex allows you to filter traffic using regexes - - Start a service, add your regexes and it's already done! - - - - setOpen(true)} size="xl" radius="md" variant="filled"> - - - - - - - } - } - - {srv?children:null} - {srv? - : - - } - -} - -export default NFRegex; +import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import { BsPlusLg, BsRegex } from "react-icons/bs"; +import { useNavigate, useParams } from 'react-router'; +import ServiceRow from '../../components/NFRegex/ServiceRow'; +import { nfregexServiceQuery } from '../../components/NFRegex/utils'; +import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; +import AddEditService from '../../components/NFRegex/AddEditService'; +import AddNewRegex from '../../components/AddNewRegex'; +import { useQueryClient } from '@tanstack/react-query'; +import { TbReload } from 'react-icons/tb'; +import { FaFilter } from 'react-icons/fa'; +import { FaServer } from "react-icons/fa6"; +import { VscRegex } from "react-icons/vsc"; +import { DocsButton } from '../../components/DocsButton'; + +function NFRegex({ children }: { children: any }) { + + const navigator = useNavigate() + const [open, setOpen] = useState(false); + const {srv} = useParams() + const queryClient = useQueryClient() + const isMedium = isMediumScreen() + const services = nfregexServiceQuery() + + useEffect(()=> { + if(services.isError) + errorNotify("NFRegex Update failed!", getErrorMessage(services.error)) + },[services.isError]) + + const closeModal = () => {setOpen(false);} + + return <> + + + <ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex + {isMedium?:} + + {isMedium?"General stats:":null} + + Services: {services.isLoading?0:services.data?.length} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)} + + {services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)} + + + {isMedium?null:} + + { srv? + + setOpen(true)} size="lg" radius="md" variant="filled"> + + : + setOpen(true)} size="lg" radius="md" variant="filled"> + + } + + + queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled" + loading={services.isFetching}> + + + + + + + + {srv?null:<> + + {(services.data && services.data?.length > 0)?services.data.map( srv => { + navigator("/nfregex/"+srv.service_id) + }} />):<> + + + Netfilter Regex allows you to filter traffic using regexes + + Start a service, add your regexes and it's already done! + + + + setOpen(true)} size="xl" radius="md" variant="filled"> + + + + + + + } + } + + {srv?children:null} + {srv? + : + + } + +} + +export default NFRegex; diff --git a/frontend/src/pages/PortHijack/index.tsx b/frontend/src/pages/PortHijack/index.tsx index 288fa63..c4287d9 100644 --- a/frontend/src/pages/PortHijack/index.tsx +++ b/frontend/src/pages/PortHijack/index.tsx @@ -1,77 +1,77 @@ -import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core'; -import { useEffect, useState } from 'react'; -import { BsPlusLg } from "react-icons/bs"; -import ServiceRow from '../../components/PortHijack/ServiceRow'; -import { porthijackServiceQuery } from '../../components/PortHijack/utils'; -import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; -import AddNewService from '../../components/PortHijack/AddNewService'; -import { useQueryClient } from '@tanstack/react-query'; -import { TbReload } from 'react-icons/tb'; -import { FaServer } from 'react-icons/fa'; -import { GrDirections } from 'react-icons/gr'; -import { DocsButton } from '../../components/DocsButton'; - - -function PortHijack() { - - const [open, setOpen] = useState(false); - const queryClient = useQueryClient() - const isMedium = isMediumScreen() - - const services = porthijackServiceQuery() - - useEffect(()=>{ - if(services.isError) - errorNotify("Porthijack Update failed!", getErrorMessage(services.error)) - },[services.isError]) - - const closeModal = () => {setOpen(false);} - - return <> - - - <ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy - {isMedium?:} - - Services: {services.isLoading?0:services.data?.length} - - - setOpen(true)} size="lg" radius="md" variant="filled"> - - - - queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled" - loading={services.isFetching}> - - - - - - - - - {(services.data && services.data.length > 0) ?services.data.map( srv => ):<> - - - Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config - - It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback - - Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs - - - - setOpen(true)} size="xl" radius="md" variant="filled"> - - - - - - - } - - - -} - -export default PortHijack; +import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import { BsPlusLg } from "react-icons/bs"; +import ServiceRow from '../../components/PortHijack/ServiceRow'; +import { porthijackServiceQuery } from '../../components/PortHijack/utils'; +import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; +import AddNewService from '../../components/PortHijack/AddNewService'; +import { useQueryClient } from '@tanstack/react-query'; +import { TbReload } from 'react-icons/tb'; +import { FaServer } from 'react-icons/fa'; +import { GrDirections } from 'react-icons/gr'; +import { DocsButton } from '../../components/DocsButton'; + + +function PortHijack() { + + const [open, setOpen] = useState(false); + const queryClient = useQueryClient() + const isMedium = isMediumScreen() + + const services = porthijackServiceQuery() + + useEffect(()=>{ + if(services.isError) + errorNotify("Porthijack Update failed!", getErrorMessage(services.error)) + },[services.isError]) + + const closeModal = () => {setOpen(false);} + + return <> + + + <ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy + {isMedium?:} + + Services: {services.isLoading?0:services.data?.length} + + + setOpen(true)} size="lg" radius="md" variant="filled"> + + + + queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled" + loading={services.isFetching}> + + + + + + + + + {(services.data && services.data.length > 0) ?services.data.map( srv => ):<> + + + Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config + + It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback + + Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs + + + + setOpen(true)} size="xl" radius="md" variant="filled"> + + + + + + + } + + + +} + +export default PortHijack; diff --git a/frontend/src/pages/TrafficViewer/index.tsx b/frontend/src/pages/TrafficViewer/index.tsx new file mode 100644 index 0000000..727790a --- /dev/null +++ b/frontend/src/pages/TrafficViewer/index.tsx @@ -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 ; + + const activeServices = services.data?.filter(s => s.status === 'active') || []; + const stoppedServices = services.data?.filter(s => s.status !== 'active') || []; + + return <> + + + <ThemeIcon radius="md" size="lg" variant='filled' color='cyan'> + <MdVisibility size={24} /> + </ThemeIcon> + <Space w="sm" /> + Traffic Viewer + + + Monitor live network traffic for all NFProxy services + + + + + + {services.data?.length === 0 ? ( + + + No NFProxy services found + + + + Create a service in the Netfilter Proxy section to start monitoring traffic + + + ) : ( + + {activeServices.length > 0 && ( + <> + + <Badge color="teal" size="lg" mr="xs">Active</Badge> + Running Services + + {activeServices.map(service => ( + + + + + + + +
+ {service.name} + + :{service.port} + {service.proto} + {service.ip_int} + +
+
+
+ + + + + {service.edited_packets} edited + +
+ + {service.blocked_packets} blocked + +
+ + navigate(`/nfproxy/${service.service_id}/traffic`)} + > + + + +
+
+
+
+ ))} + + + )} + + {stoppedServices.length > 0 && ( + <> + + <Badge color="red" size="lg" mr="xs">Stopped</Badge> + Inactive Services + + {stoppedServices.map(service => ( + + + + + + + +
+ {service.name} + + :{service.port} + {service.proto} + +
+
+
+ + + Start service to view traffic + + +
+
+ ))} + + )} +
+ )} + ; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ddfe92..53d6f94 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,22 +1,22 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": ["dom", "dom.iterable", "esnext"], - "types": ["vite/client", "vite-plugin-svgr/client", "node"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "ESNext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["src"] -} - +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["vite/client", "vite-plugin-svgr/client", "node"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} + diff --git a/run.py b/run.py index a7ffa90..3e067b5 100755 --- a/run.py +++ b/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}", diff --git a/tests/results/2.3.3-1T.csv b/tests/results/2.3.3-1T.csv index e571d8e..95e7f2f 100644 --- a/tests/results/2.3.3-1T.csv +++ b/tests/results/2.3.3-1T.csv @@ -1,51 +1,51 @@ -0,4090.616 -1,2211.62 -2,1165.45 -3,849.39 -4,828.635 -5,741.537 -6,632.721 -7,624.772 -8,529.234 -9,469.688 -10,336.33 -11,427.783 -12,400.662 -13,335.086 -14,342.042 -15,307.283 -16,239.694 -17,295.163 -18,285.787 -19,254.402 -20,250.553 -21,227.146 -22,238.747 -23,234.718 -24,210.484 -25,210.697 -26,205.943 -27,202.568 -28,194.341 -29,189.916 -30,154.228 -31,168.922 -32,173.623 -33,125.431 -34,162.154 -35,149.865 -36,150.088 -37,146.085 -38,137.182 -39,138.686 -40,136.302 -41,132.707 -42,100.928 -43,126.414 -44,125.271 -45,117.839 -46,89.494 -47,116.939 -48,112.517 -49,111.369 -50,108.568 +0,4090.616 +1,2211.62 +2,1165.45 +3,849.39 +4,828.635 +5,741.537 +6,632.721 +7,624.772 +8,529.234 +9,469.688 +10,336.33 +11,427.783 +12,400.662 +13,335.086 +14,342.042 +15,307.283 +16,239.694 +17,295.163 +18,285.787 +19,254.402 +20,250.553 +21,227.146 +22,238.747 +23,234.718 +24,210.484 +25,210.697 +26,205.943 +27,202.568 +28,194.341 +29,189.916 +30,154.228 +31,168.922 +32,173.623 +33,125.431 +34,162.154 +35,149.865 +36,150.088 +37,146.085 +38,137.182 +39,138.686 +40,136.302 +41,132.707 +42,100.928 +43,126.414 +44,125.271 +45,117.839 +46,89.494 +47,116.939 +48,112.517 +49,111.369 +50,108.568 diff --git a/tests/results/2.3.3-8T.csv b/tests/results/2.3.3-8T.csv index e4d3831..bfa6359 100644 --- a/tests/results/2.3.3-8T.csv +++ b/tests/results/2.3.3-8T.csv @@ -1,51 +1,51 @@ -0,3789.988 -1,2069.487 -2,1484.554 -3,956.972 -4,1052.873 -5,739.658 -6,534.722 -7,638.524 -8,573.833 -9,531.658 -10,476.167 -11,443.746 -12,406.027 -13,385.739 -14,341.563 -15,318.699 -16,303.722 -17,284.924 -18,284.336 -19,267.32 -20,202.74 -21,243.849 -22,226.082 -23,214.348 -24,216.8 -25,188.98 -26,158.68 -27,166.556 -28,148.287 -29,149.681 -30,177.043 -31,175.321 -32,165.312 -33,166.943 -34,159.026 -35,156.759 -36,150.216 -37,144.932 -38,146.088 -39,135.897 -40,136.99 -41,128.557 -42,100.307 -43,103.249 -44,123.49 -45,120.39 -46,118.055 -47,115.0 -48,112.593 -49,109.55 -50,109.512 +0,3789.988 +1,2069.487 +2,1484.554 +3,956.972 +4,1052.873 +5,739.658 +6,534.722 +7,638.524 +8,573.833 +9,531.658 +10,476.167 +11,443.746 +12,406.027 +13,385.739 +14,341.563 +15,318.699 +16,303.722 +17,284.924 +18,284.336 +19,267.32 +20,202.74 +21,243.849 +22,226.082 +23,214.348 +24,216.8 +25,188.98 +26,158.68 +27,166.556 +28,148.287 +29,149.681 +30,177.043 +31,175.321 +32,165.312 +33,166.943 +34,159.026 +35,156.759 +36,150.216 +37,144.932 +38,146.088 +39,135.897 +40,136.99 +41,128.557 +42,100.307 +43,103.249 +44,123.49 +45,120.39 +46,118.055 +47,115.0 +48,112.593 +49,109.55 +50,109.512 diff --git a/tests/results/2.4.0-1T.csv b/tests/results/2.4.0-1T.csv index b12df26..7fb6069 100644 --- a/tests/results/2.4.0-1T.csv +++ b/tests/results/2.4.0-1T.csv @@ -1,51 +1,51 @@ -0,4216.05 -1,4239.598 -2,2418.527 -3,2227.8 -4,2045.351 -5,2066.161 -6,2214.416 -7,2052.845 -8,2195.199 -9,2186.867 -10,2147.534 -11,2186.652 -12,2178.036 -13,2182.151 -14,2185.324 -15,1812.911 -16,2144.689 -17,2163.525 -18,2073.89 -19,2071.682 -20,2153.502 -21,2144.04 -22,2118.517 -23,2141.19 -24,2167.103 -25,2168.631 -26,2165.555 -27,2158.424 -28,2188.376 -29,2165.311 -30,2168.158 -31,2108.045 -32,2121.414 -33,2022.533 -34,1888.759 -35,2022.837 -36,2015.042 -37,1920.401 -38,2005.037 -39,2028.856 -40,2010.43 -41,1522.342 -42,1525.635 -43,1912.05 -44,1920.256 -45,1753.645 -46,1476.977 -47,1888.645 -48,1949.103 -49,1684.633 -50,1493.935 +0,4216.05 +1,4239.598 +2,2418.527 +3,2227.8 +4,2045.351 +5,2066.161 +6,2214.416 +7,2052.845 +8,2195.199 +9,2186.867 +10,2147.534 +11,2186.652 +12,2178.036 +13,2182.151 +14,2185.324 +15,1812.911 +16,2144.689 +17,2163.525 +18,2073.89 +19,2071.682 +20,2153.502 +21,2144.04 +22,2118.517 +23,2141.19 +24,2167.103 +25,2168.631 +26,2165.555 +27,2158.424 +28,2188.376 +29,2165.311 +30,2168.158 +31,2108.045 +32,2121.414 +33,2022.533 +34,1888.759 +35,2022.837 +36,2015.042 +37,1920.401 +38,2005.037 +39,2028.856 +40,2010.43 +41,1522.342 +42,1525.635 +43,1912.05 +44,1920.256 +45,1753.645 +46,1476.977 +47,1888.645 +48,1949.103 +49,1684.633 +50,1493.935 diff --git a/tests/results/2.4.0-8T.csv b/tests/results/2.4.0-8T.csv index e894d72..384540e 100644 --- a/tests/results/2.4.0-8T.csv +++ b/tests/results/2.4.0-8T.csv @@ -1,51 +1,51 @@ -0,4203.31 -1,4283.392 -2,2383.415 -3,2419.701 -4,2038.823 -5,2038.0 -6,2160.869 -7,2192.641 -8,2216.766 -9,2762.56 -10,2160.398 -11,2147.886 -12,2146.47 -13,2158.101 -14,2154.025 -15,1997.694 -16,2028.288 -17,2005.373 -18,2153.945 -19,2190.799 -20,2169.302 -21,2139.842 -22,2155.307 -23,2152.223 -24,2124.155 -25,2103.135 -26,2148.053 -27,2163.366 -28,2122.339 -29,2064.701 -30,2134.748 -31,1632.533 -32,2082.309 -33,1878.795 -34,2009.28 -35,1987.424 -36,1748.364 -37,1725.66 -38,1967.877 -39,1854.637 -40,1903.963 -41,1987.138 -42,1532.547 -43,1569.27 -44,1535.941 -45,1941.715 -46,2014.504 -47,2005.794 -48,2022.972 -49,1740.836 -50,1726.444 +0,4203.31 +1,4283.392 +2,2383.415 +3,2419.701 +4,2038.823 +5,2038.0 +6,2160.869 +7,2192.641 +8,2216.766 +9,2762.56 +10,2160.398 +11,2147.886 +12,2146.47 +13,2158.101 +14,2154.025 +15,1997.694 +16,2028.288 +17,2005.373 +18,2153.945 +19,2190.799 +20,2169.302 +21,2139.842 +22,2155.307 +23,2152.223 +24,2124.155 +25,2103.135 +26,2148.053 +27,2163.366 +28,2122.339 +29,2064.701 +30,2134.748 +31,1632.533 +32,2082.309 +33,1878.795 +34,2009.28 +35,1987.424 +36,1748.364 +37,1725.66 +38,1967.877 +39,1854.637 +40,1903.963 +41,1987.138 +42,1532.547 +43,1569.27 +44,1535.941 +45,1941.715 +46,2014.504 +47,2005.794 +48,2022.972 +49,1740.836 +50,1726.444 diff --git a/tests/results/2.5.1-1T-withload.csv b/tests/results/2.5.1-1T-withload.csv index fadffaa..bdc50c8 100644 --- a/tests/results/2.5.1-1T-withload.csv +++ b/tests/results/2.5.1-1T-withload.csv @@ -1,51 +1,51 @@ -0,710.619 -1,887.877 -2,981.431 -3,1081.412 -4,1038.514 -5,1029.805 -6,928.317 -7,1130.938 -8,1165.42 -9,925.632 -10,949.483 -11,1021.973 -12,903.878 -13,1001.53 -14,895.351 -15,1026.722 -16,634.727 -17,744.758 -18,978.59 -19,962.375 -20,997.471 -21,929.785 -22,1200.83 -23,1257.741 -24,772.729 -25,683.913 -26,1188.17 -27,919.961 -28,922.225 -29,1066.286 -30,979.399 -31,978.917 -32,988.415 -33,1061.523 -34,942.85 -35,1045.949 -36,883.941 -37,958.41 -38,989.523 -39,1001.121 -40,1080.079 -41,1151.938 -42,1221.644 -43,991.855 -44,1088.344 -45,973.641 -46,952.35 -47,1089.644 -48,939.615 -49,1258.419 -50,949.414 +0,710.619 +1,887.877 +2,981.431 +3,1081.412 +4,1038.514 +5,1029.805 +6,928.317 +7,1130.938 +8,1165.42 +9,925.632 +10,949.483 +11,1021.973 +12,903.878 +13,1001.53 +14,895.351 +15,1026.722 +16,634.727 +17,744.758 +18,978.59 +19,962.375 +20,997.471 +21,929.785 +22,1200.83 +23,1257.741 +24,772.729 +25,683.913 +26,1188.17 +27,919.961 +28,922.225 +29,1066.286 +30,979.399 +31,978.917 +32,988.415 +33,1061.523 +34,942.85 +35,1045.949 +36,883.941 +37,958.41 +38,989.523 +39,1001.121 +40,1080.079 +41,1151.938 +42,1221.644 +43,991.855 +44,1088.344 +45,973.641 +46,952.35 +47,1089.644 +48,939.615 +49,1258.419 +50,949.414 diff --git a/tests/results/2.5.1-1T.csv b/tests/results/2.5.1-1T.csv index 0f7535b..0001aab 100644 --- a/tests/results/2.5.1-1T.csv +++ b/tests/results/2.5.1-1T.csv @@ -1,51 +1,51 @@ -0,3245.763 -1,3283.646 -2,3741.157 -3,3691.206 -4,3365.134 -5,3691.457 -6,3354.807 -7,3526.728 -8,3252.62 -9,3551.086 -10,3561.506 -11,3525.577 -12,2776.064 -13,3541.86 -14,3501.34 -15,3692.092 -16,3637.166 -17,3617.031 -18,3700.092 -19,3176.831 -20,3368.038 -21,3716.577 -22,3452.917 -23,3617.604 -24,3651.796 -25,3552.053 -26,3843.18 -27,3720.406 -28,3431.1 -29,3578.973 -30,3561.994 -31,3524.566 -32,3567.537 -33,3626.767 -34,3498.361 -35,3621.396 -36,3297.839 -37,3541.207 -38,3560.364 -39,3589.746 -40,3686.673 -41,3463.811 -42,3428.408 -43,3753.139 -44,3368.89 -45,3324.876 -46,3614.895 -47,3245.942 -48,3257.925 -49,3200.585 -50,3321.55 +0,3245.763 +1,3283.646 +2,3741.157 +3,3691.206 +4,3365.134 +5,3691.457 +6,3354.807 +7,3526.728 +8,3252.62 +9,3551.086 +10,3561.506 +11,3525.577 +12,2776.064 +13,3541.86 +14,3501.34 +15,3692.092 +16,3637.166 +17,3617.031 +18,3700.092 +19,3176.831 +20,3368.038 +21,3716.577 +22,3452.917 +23,3617.604 +24,3651.796 +25,3552.053 +26,3843.18 +27,3720.406 +28,3431.1 +29,3578.973 +30,3561.994 +31,3524.566 +32,3567.537 +33,3626.767 +34,3498.361 +35,3621.396 +36,3297.839 +37,3541.207 +38,3560.364 +39,3589.746 +40,3686.673 +41,3463.811 +42,3428.408 +43,3753.139 +44,3368.89 +45,3324.876 +46,3614.895 +47,3245.942 +48,3257.925 +49,3200.585 +50,3321.55 diff --git a/tests/results/2.5.1-8T-withload.csv b/tests/results/2.5.1-8T-withload.csv index 5ff7b8f..a2e4aaf 100644 --- a/tests/results/2.5.1-8T-withload.csv +++ b/tests/results/2.5.1-8T-withload.csv @@ -1,51 +1,51 @@ -0,1790.382 -1,1933.881 -2,1941.564 -3,1926.518 -4,1945.295 -5,1734.462 -6,2009.994 -7,2007.538 -8,2004.825 -9,1848.551 -10,1836.558 -11,1977.19 -12,1987.207 -13,2007.422 -14,1994.914 -15,1982.997 -16,1955.828 -17,1705.883 -18,1983.501 -19,1951.311 -20,1921.772 -21,1956.908 -22,1948.865 -23,1929.387 -24,1814.539 -25,2084.284 -26,1830.901 -27,1946.713 -28,1958.238 -29,1906.573 -30,1895.341 -31,1986.09 -32,1943.785 -33,1879.917 -34,1946.029 -35,1858.958 -36,2009.44 -37,1876.749 -38,1967.254 -39,1968.595 -40,1846.438 -41,1955.897 -42,1986.446 -43,1965.143 -44,1963.016 -45,1890.88 -46,1998.801 -47,1682.048 -48,2023.688 -49,1982.952 -50,1993.641 +0,1790.382 +1,1933.881 +2,1941.564 +3,1926.518 +4,1945.295 +5,1734.462 +6,2009.994 +7,2007.538 +8,2004.825 +9,1848.551 +10,1836.558 +11,1977.19 +12,1987.207 +13,2007.422 +14,1994.914 +15,1982.997 +16,1955.828 +17,1705.883 +18,1983.501 +19,1951.311 +20,1921.772 +21,1956.908 +22,1948.865 +23,1929.387 +24,1814.539 +25,2084.284 +26,1830.901 +27,1946.713 +28,1958.238 +29,1906.573 +30,1895.341 +31,1986.09 +32,1943.785 +33,1879.917 +34,1946.029 +35,1858.958 +36,2009.44 +37,1876.749 +38,1967.254 +39,1968.595 +40,1846.438 +41,1955.897 +42,1986.446 +43,1965.143 +44,1963.016 +45,1890.88 +46,1998.801 +47,1682.048 +48,2023.688 +49,1982.952 +50,1993.641 diff --git a/tests/results/2.5.1-8T.csv b/tests/results/2.5.1-8T.csv index 16fff42..b388b64 100644 --- a/tests/results/2.5.1-8T.csv +++ b/tests/results/2.5.1-8T.csv @@ -1,51 +1,51 @@ -0,4007.679 -1,3963.986 -2,4222.243 -3,3640.707 -4,4388.553 -5,3636.047 -6,3644.611 -7,3547.39 -8,3412.162 -9,3632.367 -10,3536.655 -11,3820.019 -12,3677.177 -13,3366.323 -14,3353.031 -15,3392.423 -16,3330.368 -17,3363.272 -18,4027.34 -19,3467.982 -20,3607.754 -21,3767.614 -22,3340.544 -23,4086.612 -24,3784.164 -25,3496.518 -26,3543.808 -27,3453.934 -28,3546.188 -29,3458.804 -30,3728.609 -31,3697.624 -32,3698.191 -33,3673.973 -34,3690.046 -35,3663.799 -36,3540.004 -37,3857.604 -38,3426.215 -39,3704.176 -40,3796.133 -41,3604.623 -42,3650.508 -43,3501.861 -44,3685.992 -45,3623.404 -46,3728.601 -47,3844.994 -48,3820.046 -49,3680.976 -50,3797.432 +0,4007.679 +1,3963.986 +2,4222.243 +3,3640.707 +4,4388.553 +5,3636.047 +6,3644.611 +7,3547.39 +8,3412.162 +9,3632.367 +10,3536.655 +11,3820.019 +12,3677.177 +13,3366.323 +14,3353.031 +15,3392.423 +16,3330.368 +17,3363.272 +18,4027.34 +19,3467.982 +20,3607.754 +21,3767.614 +22,3340.544 +23,4086.612 +24,3784.164 +25,3496.518 +26,3543.808 +27,3453.934 +28,3546.188 +29,3458.804 +30,3728.609 +31,3697.624 +32,3698.191 +33,3673.973 +34,3690.046 +35,3663.799 +36,3540.004 +37,3857.604 +38,3426.215 +39,3704.176 +40,3796.133 +41,3604.623 +42,3650.508 +43,3501.861 +44,3685.992 +45,3623.404 +46,3728.601 +47,3844.994 +48,3820.046 +49,3680.976 +50,3797.432 diff --git a/tests/results/comparemark_nfproxy_1T.csv b/tests/results/comparemark_nfproxy_1T.csv index f2ee391..3f0df0d 100644 --- a/tests/results/comparemark_nfproxy_1T.csv +++ b/tests/results/comparemark_nfproxy_1T.csv @@ -1,101 +1,101 @@ -No filters,test data -1600.27,1772.897 -1486.257,1455.93 -1534.667,1403.539 -1244.374,1665.846 -1569.867,1627.449 -1522.719,1084.153 -1391.244,1259.783 -1528.465,1282.901 -1310.989,1275.515 -1675.138,1074.39 -1393.644,1359.139 -1639.889,1162.937 -1658.168,1239.767 -1477.156,1308.195 -1224.386,1298.007 -1420.7,1087.031 -1353.746,1090.502 -1759.778,1179.381 -1414.33,1222.86 -1475.981,1295.207 -1375.197,1327.8 -1265.015,1189.121 -1335.179,1594.98 -1191.896,1271.873 -1596.418,1100.372 -1433.755,1147.945 -1213.187,1312.989 -1157.99,1153.825 -1322.314,1184.481 -1262.974,1271.012 -1266.223,1350.519 -1192.275,1199.142 -1296.164,1189.432 -1245.501,1185.107 -1293.076,1374.689 -1260.554,1384.055 -1219.219,1420.395 -1132.234,1099.141 -1129.541,1101.805 -1273.171,1210.564 -1269.415,1184.094 -1370.586,1321.974 -1303.694,1317.074 -1413.705,1380.092 -1324.827,1142.097 -1124.399,1548.557 -1137.381,1029.353 -1419.146,1326.829 -1342.397,1270.316 -1546.898,1258.933 -1268.918,1062.23 -1239.877,1234.887 -1474.269,1181.184 -1289.763,1139.728 -1387.416,1125.734 -1128.784,1278.381 -1519.4,1243.597 -1343.003,1153.18 -1547.543,1117.816 -1582.958,1594.145 -1618.213,1358.087 -1449.399,1295.487 -1373.062,1174.153 -1211.207,1346.833 -1066.275,1417.633 -1203.659,1131.727 -1129.005,1351.061 -1200.245,1615.952 -1232.596,1250.436 -1262.319,1563.46 -1127.022,1651.89 -1736.368,1561.661 -1310.858,1459.713 -1351.455,1608.494 -1156.124,1440.379 -1220.053,1267.708 -1171.428,1300.284 -1149.242,1087.453 -1213.915,1081.207 -1092.869,1402.761 -1243.623,1321.907 -1216.257,1217.721 -1221.354,1263.695 -1242.771,1241.684 -1427.276,1322.01 -1328.502,1346.21 -1275.719,1269.909 -1372.075,1451.069 -1486.541,1532.56 -1577.036,1539.804 -1628.025,1372.806 -1415.623,1239.201 -1198.632,1095.849 -1170.341,1255.875 -1214.99,1424.292 -1356.431,1135.588 -1817.822,1212.386 -1745.199,1170.863 -1779.083,1145.458 -1544.934,1076.386 +No filters,test data +1600.27,1772.897 +1486.257,1455.93 +1534.667,1403.539 +1244.374,1665.846 +1569.867,1627.449 +1522.719,1084.153 +1391.244,1259.783 +1528.465,1282.901 +1310.989,1275.515 +1675.138,1074.39 +1393.644,1359.139 +1639.889,1162.937 +1658.168,1239.767 +1477.156,1308.195 +1224.386,1298.007 +1420.7,1087.031 +1353.746,1090.502 +1759.778,1179.381 +1414.33,1222.86 +1475.981,1295.207 +1375.197,1327.8 +1265.015,1189.121 +1335.179,1594.98 +1191.896,1271.873 +1596.418,1100.372 +1433.755,1147.945 +1213.187,1312.989 +1157.99,1153.825 +1322.314,1184.481 +1262.974,1271.012 +1266.223,1350.519 +1192.275,1199.142 +1296.164,1189.432 +1245.501,1185.107 +1293.076,1374.689 +1260.554,1384.055 +1219.219,1420.395 +1132.234,1099.141 +1129.541,1101.805 +1273.171,1210.564 +1269.415,1184.094 +1370.586,1321.974 +1303.694,1317.074 +1413.705,1380.092 +1324.827,1142.097 +1124.399,1548.557 +1137.381,1029.353 +1419.146,1326.829 +1342.397,1270.316 +1546.898,1258.933 +1268.918,1062.23 +1239.877,1234.887 +1474.269,1181.184 +1289.763,1139.728 +1387.416,1125.734 +1128.784,1278.381 +1519.4,1243.597 +1343.003,1153.18 +1547.543,1117.816 +1582.958,1594.145 +1618.213,1358.087 +1449.399,1295.487 +1373.062,1174.153 +1211.207,1346.833 +1066.275,1417.633 +1203.659,1131.727 +1129.005,1351.061 +1200.245,1615.952 +1232.596,1250.436 +1262.319,1563.46 +1127.022,1651.89 +1736.368,1561.661 +1310.858,1459.713 +1351.455,1608.494 +1156.124,1440.379 +1220.053,1267.708 +1171.428,1300.284 +1149.242,1087.453 +1213.915,1081.207 +1092.869,1402.761 +1243.623,1321.907 +1216.257,1217.721 +1221.354,1263.695 +1242.771,1241.684 +1427.276,1322.01 +1328.502,1346.21 +1275.719,1269.909 +1372.075,1451.069 +1486.541,1532.56 +1577.036,1539.804 +1628.025,1372.806 +1415.623,1239.201 +1198.632,1095.849 +1170.341,1255.875 +1214.99,1424.292 +1356.431,1135.588 +1817.822,1212.386 +1745.199,1170.863 +1779.083,1145.458 +1544.934,1076.386 diff --git a/tests/results/comparemark_nfproxy_8T.csv b/tests/results/comparemark_nfproxy_8T.csv index c720a17..dde841c 100644 --- a/tests/results/comparemark_nfproxy_8T.csv +++ b/tests/results/comparemark_nfproxy_8T.csv @@ -1,101 +1,101 @@ -No filters,test data -2098.666,2118.781 -2175.2,2086.957 -2177.653,1795.287 -1775.63,1745.066 -1827.78,2038.921 -1813.369,2179.81 -1988.859,2176.883 -1634.541,1704.071 -1878.829,1869.999 -1738.987,2024.959 -1920.502,1477.726 -1895.909,1732.832 -1812.012,1850.978 -1908.106,1902.953 -2112.837,1726.547 -1765.808,1710.915 -1918.121,1900.619 -1892.779,2054.93 -1852.952,2113.928 -1713.67,1770.379 -1873.637,2011.518 -1787.007,2104.061 -1764.704,2134.151 -2064.776,2073.226 -1838.23,1762.436 -1808.339,1792.41 -1756.516,1706.501 -1665.888,1610.771 -1682.272,1650.033 -1690.473,1563.995 -1997.801,1955.53 -1660.487,1669.25 -2023.106,1727.046 -1724.59,1686.137 -1697.656,1627.136 -1689.65,1571.13 -1628.35,1699.239 -1843.768,1825.739 -1715.158,1573.695 -1732.695,1875.656 -1902.818,1968.505 -1699.277,1919.737 -1618.75,2015.258 -1696.055,2014.261 -1792.486,1606.754 -1889.583,1625.965 -1716.951,1572.049 -1727.305,1649.502 -1747.618,2099.787 -1698.546,2153.363 -1723.117,1637.074 -1654.061,1721.968 -1735.332,1587.906 -1841.808,1565.797 -2006.973,1665.615 -1730.909,1883.505 -1681.954,1553.826 -1653.215,1849.824 -2072.138,1990.474 -1792.302,2176.718 -1679.381,2128.083 -1653.368,2078.013 -1399.58,2065.031 -1669.979,1815.553 -1677.346,1870.055 -1652.22,2010.441 -1870.35,1687.893 -1772.229,1857.193 -1743.552,1813.027 -1685.312,1466.505 -1863.269,1813.398 -1694.335,1889.661 -1739.016,1740.381 -1764.462,1752.725 -1702.134,2069.289 -1955.771,2176.617 -2046.117,2137.499 -1766.64,2177.955 -1733.26,2148.497 -1834.827,2161.573 -2087.089,2119.311 -2154.753,1679.596 -2073.729,1912.012 -2082.37,1841.045 -2160.86,1813.257 -1678.515,1894.864 -1758.394,1884.985 -1673.919,1732.373 -1666.474,1737.66 -1679.444,1463.082 -1684.006,2002.343 -1737.287,2026.394 -1811.305,2084.689 -2127.121,2117.391 -2139.884,1984.606 -1677.256,1770.76 -1698.544,1833.011 -1905.446,1734.777 -1913.257,1688.401 -2063.73,1667.27 +No filters,test data +2098.666,2118.781 +2175.2,2086.957 +2177.653,1795.287 +1775.63,1745.066 +1827.78,2038.921 +1813.369,2179.81 +1988.859,2176.883 +1634.541,1704.071 +1878.829,1869.999 +1738.987,2024.959 +1920.502,1477.726 +1895.909,1732.832 +1812.012,1850.978 +1908.106,1902.953 +2112.837,1726.547 +1765.808,1710.915 +1918.121,1900.619 +1892.779,2054.93 +1852.952,2113.928 +1713.67,1770.379 +1873.637,2011.518 +1787.007,2104.061 +1764.704,2134.151 +2064.776,2073.226 +1838.23,1762.436 +1808.339,1792.41 +1756.516,1706.501 +1665.888,1610.771 +1682.272,1650.033 +1690.473,1563.995 +1997.801,1955.53 +1660.487,1669.25 +2023.106,1727.046 +1724.59,1686.137 +1697.656,1627.136 +1689.65,1571.13 +1628.35,1699.239 +1843.768,1825.739 +1715.158,1573.695 +1732.695,1875.656 +1902.818,1968.505 +1699.277,1919.737 +1618.75,2015.258 +1696.055,2014.261 +1792.486,1606.754 +1889.583,1625.965 +1716.951,1572.049 +1727.305,1649.502 +1747.618,2099.787 +1698.546,2153.363 +1723.117,1637.074 +1654.061,1721.968 +1735.332,1587.906 +1841.808,1565.797 +2006.973,1665.615 +1730.909,1883.505 +1681.954,1553.826 +1653.215,1849.824 +2072.138,1990.474 +1792.302,2176.718 +1679.381,2128.083 +1653.368,2078.013 +1399.58,2065.031 +1669.979,1815.553 +1677.346,1870.055 +1652.22,2010.441 +1870.35,1687.893 +1772.229,1857.193 +1743.552,1813.027 +1685.312,1466.505 +1863.269,1813.398 +1694.335,1889.661 +1739.016,1740.381 +1764.462,1752.725 +1702.134,2069.289 +1955.771,2176.617 +2046.117,2137.499 +1766.64,2177.955 +1733.26,2148.497 +1834.827,2161.573 +2087.089,2119.311 +2154.753,1679.596 +2073.729,1912.012 +2082.37,1841.045 +2160.86,1813.257 +1678.515,1894.864 +1758.394,1884.985 +1673.919,1732.373 +1666.474,1737.66 +1679.444,1463.082 +1684.006,2002.343 +1737.287,2026.394 +1811.305,2084.689 +2127.121,2117.391 +2139.884,1984.606 +1677.256,1770.76 +1698.544,1833.011 +1905.446,1734.777 +1913.257,1688.401 +2063.73,1667.27 diff --git a/tests/results/comparemark_nfregex_1T.csv b/tests/results/comparemark_nfregex_1T.csv index fc2fd4d..bdde0da 100644 --- a/tests/results/comparemark_nfregex_1T.csv +++ b/tests/results/comparemark_nfregex_1T.csv @@ -1,101 +1,101 @@ -No filters,test data -3841.832,3177.356 -3369.899,3819.926 -3884.689,2843.759 -3391.267,3106.399 -3740.054,2899.246 -3754.086,3254.525 -3284.178,3180.96 -3293.044,3356.928 -3653.05,2925.883 -3830.609,2784.715 -3691.078,3283.715 -3551.286,3437.899 -3651.296,2759.088 -3726.295,3289.184 -3860.353,3067.069 -3910.997,3764.354 -3775.794,3182.171 -3824.719,3376.774 -3245.109,2954.582 -3705.489,4101.548 -3484.114,3155.55 -3742.727,3153.767 -3964.472,3624.241 -3747.219,2787.965 -3746.575,3518.095 -3903.7,2942.676 -3888.772,3222.041 -3854.913,2479.502 -3716.801,2876.082 -3919.146,2748.543 -3908.195,2742.45 -3894.436,3135.703 -3615.381,3411.222 -3807.51,3525.049 -3197.936,3515.207 -3817.654,3505.676 -3604.482,3749.862 -4054.217,3389.18 -4064.973,3110.13 -3828.174,3994.395 -3464.949,3706.928 -3458.833,3818.998 -3447.594,3354.733 -3148.49,2938.606 -3403.617,3000.615 -3619.143,3712.188 -3676.835,3294.72 -4020.2,3668.025 -3365.03,3288.992 -3395.001,3047.487 -3444.301,3644.15 -3258.341,3412.968 -3640.787,3028.915 -3523.975,2984.702 -3661.891,3124.492 -3802.303,3098.351 -3774.646,3486.505 -3622.705,1967.98 -3508.677,2629.166 -3566.014,2717.307 -3849.619,1697.053 -3315.839,1708.413 -3423.282,2104.829 -3750.536,2822.277 -3554.167,2610.241 -3826.747,3645.146 -3892.643,2795.429 -3832.114,2572.367 -3497.325,3586.324 -3348.139,3108.224 -3317.933,2944.826 -3605.83,2890.459 -3539.072,3132.536 -3121.903,3343.355 -2942.032,3478.153 -3445.076,3762.927 -3100.771,3377.621 -3189.105,3326.58 -3281.825,3443.852 -2678.243,3830.363 -2955.651,2863.628 -2696.034,3640.54 -3370.494,3203.94 -3300.628,3755.641 -3488.021,3931.192 -3330.963,2780.609 -3154.885,2986.501 -3375.716,3359.562 -2841.549,3077.406 -3404.81,3385.657 -3757.787,3352.594 -3717.258,3264.236 -3353.01,3659.337 -3190.808,3732.121 -3165.985,3380.969 -3797.661,3264.325 -3347.68,3711.328 -3604.306,3454.656 -3615.091,3547.976 -3291.287,3115.255 +No filters,test data +3841.832,3177.356 +3369.899,3819.926 +3884.689,2843.759 +3391.267,3106.399 +3740.054,2899.246 +3754.086,3254.525 +3284.178,3180.96 +3293.044,3356.928 +3653.05,2925.883 +3830.609,2784.715 +3691.078,3283.715 +3551.286,3437.899 +3651.296,2759.088 +3726.295,3289.184 +3860.353,3067.069 +3910.997,3764.354 +3775.794,3182.171 +3824.719,3376.774 +3245.109,2954.582 +3705.489,4101.548 +3484.114,3155.55 +3742.727,3153.767 +3964.472,3624.241 +3747.219,2787.965 +3746.575,3518.095 +3903.7,2942.676 +3888.772,3222.041 +3854.913,2479.502 +3716.801,2876.082 +3919.146,2748.543 +3908.195,2742.45 +3894.436,3135.703 +3615.381,3411.222 +3807.51,3525.049 +3197.936,3515.207 +3817.654,3505.676 +3604.482,3749.862 +4054.217,3389.18 +4064.973,3110.13 +3828.174,3994.395 +3464.949,3706.928 +3458.833,3818.998 +3447.594,3354.733 +3148.49,2938.606 +3403.617,3000.615 +3619.143,3712.188 +3676.835,3294.72 +4020.2,3668.025 +3365.03,3288.992 +3395.001,3047.487 +3444.301,3644.15 +3258.341,3412.968 +3640.787,3028.915 +3523.975,2984.702 +3661.891,3124.492 +3802.303,3098.351 +3774.646,3486.505 +3622.705,1967.98 +3508.677,2629.166 +3566.014,2717.307 +3849.619,1697.053 +3315.839,1708.413 +3423.282,2104.829 +3750.536,2822.277 +3554.167,2610.241 +3826.747,3645.146 +3892.643,2795.429 +3832.114,2572.367 +3497.325,3586.324 +3348.139,3108.224 +3317.933,2944.826 +3605.83,2890.459 +3539.072,3132.536 +3121.903,3343.355 +2942.032,3478.153 +3445.076,3762.927 +3100.771,3377.621 +3189.105,3326.58 +3281.825,3443.852 +2678.243,3830.363 +2955.651,2863.628 +2696.034,3640.54 +3370.494,3203.94 +3300.628,3755.641 +3488.021,3931.192 +3330.963,2780.609 +3154.885,2986.501 +3375.716,3359.562 +2841.549,3077.406 +3404.81,3385.657 +3757.787,3352.594 +3717.258,3264.236 +3353.01,3659.337 +3190.808,3732.121 +3165.985,3380.969 +3797.661,3264.325 +3347.68,3711.328 +3604.306,3454.656 +3615.091,3547.976 +3291.287,3115.255 diff --git a/tests/results/comparemark_nfregex_8T.csv b/tests/results/comparemark_nfregex_8T.csv index 76d89f9..48dc2e4 100644 --- a/tests/results/comparemark_nfregex_8T.csv +++ b/tests/results/comparemark_nfregex_8T.csv @@ -1,101 +1,101 @@ -No filters,test data -4244.309,3423.133 -4172.153,3839.874 -4318.167,3651.161 -4141.307,3886.542 -4153.546,3293.166 -4313.574,3639.47 -4212.2,3422.614 -3944.194,3928.898 -3470.867,3395.562 -3680.557,4233.545 -3639.904,3739.869 -3601.206,4331.278 -3602.268,3561.573 -4041.709,3360.442 -3326.243,3898.576 -3519.295,3710.73 -3421.704,3785.601 -3761.544,3720.579 -3849.834,3419.051 -3771.48,3525.297 -3477.096,3709.462 -3752.154,3410.653 -3828.539,3784.068 -3601.283,4371.022 -3550.535,3353.485 -3573.931,4326.953 -3989.022,3630.239 -3758.771,3187.932 -3764.081,3348.153 -3552.11,3210.788 -3624.703,3580.683 -3495.138,3702.232 -3679.786,3211.763 -3965.941,4386.728 -3481.692,4312.93 -3472.266,3638.52 -3902.087,4356.89 -4162.868,3770.82 -3556.674,3899.06 -3568.287,3768.694 -3813.52,3794.494 -3538.6,4233.813 -3583.165,3598.301 -3545.668,3574.602 -3498.538,3731.551 -4069.232,3732.176 -3488.875,4390.112 -3471.224,4308.19 -3487.893,3713.36 -3556.706,3783.748 -4134.049,4075.267 -3619.571,3616.779 -3880.411,4017.523 -3437.287,4024.127 -3571.923,4136.496 -3355.569,4297.359 -3621.019,3428.405 -3432.623,3962.733 -3541.66,3558.748 -3506.787,3874.117 -4124.636,3616.127 -3585.123,3360.593 -3572.09,3416.381 -3344.338,3861.743 -3540.41,3412.915 -3768.322,3490.888 -3865.742,3149.312 -3543.772,3438.211 -3649.759,3538.124 -3714.508,3298.845 -3989.119,3652.572 -4004.341,3688.486 -3942.733,3533.375 -3767.707,3692.636 -3854.87,3567.363 -3818.102,4325.471 -4326.545,3464.113 -3331.279,3346.4 -3782.928,3599.129 -3441.486,3571.214 -3688.115,3778.354 -3523.493,4268.157 -3350.288,3241.872 -3337.668,3405.69 -3467.795,3655.209 -3695.322,3161.427 -4111.114,3289.313 -3499.726,3157.723 -3731.525,3334.048 -4226.314,3315.567 -3430.903,3176.271 -3480.629,3296.73 -3930.84,3302.929 -3702.883,3251.164 -3839.087,3180.461 -3831.296,3215.8 -3615.657,3262.533 -3766.269,3446.736 -3556.331,4274.897 -3843.934,3370.384 +No filters,test data +4244.309,3423.133 +4172.153,3839.874 +4318.167,3651.161 +4141.307,3886.542 +4153.546,3293.166 +4313.574,3639.47 +4212.2,3422.614 +3944.194,3928.898 +3470.867,3395.562 +3680.557,4233.545 +3639.904,3739.869 +3601.206,4331.278 +3602.268,3561.573 +4041.709,3360.442 +3326.243,3898.576 +3519.295,3710.73 +3421.704,3785.601 +3761.544,3720.579 +3849.834,3419.051 +3771.48,3525.297 +3477.096,3709.462 +3752.154,3410.653 +3828.539,3784.068 +3601.283,4371.022 +3550.535,3353.485 +3573.931,4326.953 +3989.022,3630.239 +3758.771,3187.932 +3764.081,3348.153 +3552.11,3210.788 +3624.703,3580.683 +3495.138,3702.232 +3679.786,3211.763 +3965.941,4386.728 +3481.692,4312.93 +3472.266,3638.52 +3902.087,4356.89 +4162.868,3770.82 +3556.674,3899.06 +3568.287,3768.694 +3813.52,3794.494 +3538.6,4233.813 +3583.165,3598.301 +3545.668,3574.602 +3498.538,3731.551 +4069.232,3732.176 +3488.875,4390.112 +3471.224,4308.19 +3487.893,3713.36 +3556.706,3783.748 +4134.049,4075.267 +3619.571,3616.779 +3880.411,4017.523 +3437.287,4024.127 +3571.923,4136.496 +3355.569,4297.359 +3621.019,3428.405 +3432.623,3962.733 +3541.66,3558.748 +3506.787,3874.117 +4124.636,3616.127 +3585.123,3360.593 +3572.09,3416.381 +3344.338,3861.743 +3540.41,3412.915 +3768.322,3490.888 +3865.742,3149.312 +3543.772,3438.211 +3649.759,3538.124 +3714.508,3298.845 +3989.119,3652.572 +4004.341,3688.486 +3942.733,3533.375 +3767.707,3692.636 +3854.87,3567.363 +3818.102,4325.471 +4326.545,3464.113 +3331.279,3346.4 +3782.928,3599.129 +3441.486,3571.214 +3688.115,3778.354 +3523.493,4268.157 +3350.288,3241.872 +3337.668,3405.69 +3467.795,3655.209 +3695.322,3161.427 +4111.114,3289.313 +3499.726,3157.723 +3731.525,3334.048 +4226.314,3315.567 +3430.903,3176.271 +3480.629,3296.73 +3930.84,3302.929 +3702.883,3251.164 +3839.087,3180.461 +3831.296,3215.8 +3615.657,3262.533 +3766.269,3446.736 +3556.331,4274.897 +3843.934,3370.384