mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 21:42:05 +00:00
Merge pull request #33 from Sogoss/feat/tls-support
Add transparent TLS proxying and certificate management for NFProxy and NFRegex
This commit is contained in:
commit
7881da7128
46
.github/workflows/pypi-publish-fgex.yml
vendored
46
.github/workflows/pypi-publish-fgex.yml
vendored
@ -1,46 +0,0 @@
|
|||||||
# 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 }}
|
|
||||||
47
.github/workflows/pypi-publish.yml
vendored
47
.github/workflows/pypi-publish.yml
vendored
@ -1,47 +0,0 @@
|
|||||||
# 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 }}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
name: Create and publish Docker images
|
name: Test, build and publish release artifacts
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@ -10,17 +10,33 @@ env:
|
|||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# Gate for every publish target below: nothing gets pushed to Docker/PyPI unless this passes.
|
||||||
|
run_tests:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build and run firegex
|
||||||
|
run: python3 run.py start -P testpassword
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||||
|
|
||||||
docker_build:
|
docker_build:
|
||||||
|
needs: run_tests
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
arch: amd64
|
arch: amd64
|
||||||
run_tests: true
|
|
||||||
- os: ubuntu-24.04-arm
|
- os: ubuntu-24.04-arm
|
||||||
arch: arm64
|
arch: arm64
|
||||||
run_tests: true
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@ -33,14 +49,6 @@ jobs:
|
|||||||
id: lowercase
|
id: lowercase
|
||||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
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
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@master
|
uses: docker/setup-buildx-action@master
|
||||||
@ -51,23 +59,23 @@ jobs:
|
|||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: tag
|
id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Update version in setup.py
|
- name: Update version in setup.py
|
||||||
run: >-
|
run: >-
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
|
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/setup.py;
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
@ -102,43 +110,43 @@ jobs:
|
|||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: tag
|
id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create and push multi-platform manifest
|
- name: Create and push multi-platform manifest
|
||||||
run: |
|
run: |
|
||||||
# Create manifest list for specific tag
|
# Create manifest list for specific tag
|
||||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
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 }}-amd64 \
|
||||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
||||||
|
|
||||||
# Annotate the manifest with architecture info
|
# Annotate the manifest with architecture info
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
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 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||||
--arch amd64 --os linux
|
--arch amd64 --os linux
|
||||||
|
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
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 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
|
||||||
--arch arm64 --os linux
|
--arch arm64 --os linux
|
||||||
|
|
||||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||||
|
|
||||||
# Create manifest list for latest tag
|
# Create manifest list for latest tag
|
||||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
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-amd64 \
|
||||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
||||||
|
|
||||||
# Annotate the latest manifest with architecture info
|
# Annotate the latest manifest with architecture info
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||||
--arch amd64 --os linux
|
--arch amd64 --os linux
|
||||||
|
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||||
--arch arm64 --os linux
|
--arch arm64 --os linux
|
||||||
|
|
||||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||||
|
|
||||||
create-rootfs-assets:
|
create-rootfs-assets:
|
||||||
@ -208,3 +216,64 @@ jobs:
|
|||||||
echo "Assets uploaded successfully!"
|
echo "Assets uploaded successfully!"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
pypi_publish_firegex:
|
||||||
|
needs: run_tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
pypi_publish_fgex_alias:
|
||||||
|
needs: run_tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
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 }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -50,3 +50,4 @@
|
|||||||
**/npm-debug.log*
|
**/npm-debug.log*
|
||||||
**/yarn-debug.log*
|
**/yarn-debug.log*
|
||||||
**/yarn-error.log*
|
**/yarn-error.log*
|
||||||
|
.claude
|
||||||
87
CLAUDE.md
Normal file
87
CLAUDE.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
Firegex is a firewall built for CTF Attack-Defense competitions: it sits in front of a vulnerable service and blocks/mangles malicious traffic before it reaches it. It ships several independent filtering modules (regex-based, Python-filter-based, port hijacking, plain nftables rules, TLS decrypt-and-reinspect) behind a single FastAPI backend and React frontend.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Running the whole app (Docker or standalone)
|
||||||
|
```bash
|
||||||
|
python3 run.py # build+start (Docker if available, else standalone chroot on Linux)
|
||||||
|
python3 run.py --standalone # force standalone mode (no Docker)
|
||||||
|
python3 run.py stop [--clear] # stop (optionally wipe the DB volume/rootfs)
|
||||||
|
python3 run.py restart # restart WITHOUT rebuilding (stale image if source changed)
|
||||||
|
python3 run.py status
|
||||||
|
python3 run.py config --password [newpass] # change the running instance's password; omit the value to be prompted interactively
|
||||||
|
```
|
||||||
|
`run.py` has zero external dependencies (stdlib only) by design. `restart` does not rebuild the Docker image; after a source change use `stop` then the bare start command to force `docker compose up -d --build`.
|
||||||
|
|
||||||
|
### Frontend (`frontend/`)
|
||||||
|
```bash
|
||||||
|
bun install # bun.lock is canonical
|
||||||
|
bun run dev # vite dev server
|
||||||
|
bun run build # tsc && vite build
|
||||||
|
npx tsc --noEmit -p . # typecheck only (no separate lint script is configured)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend (`backend/`)
|
||||||
|
Needs Linux with `NET_ADMIN` (nftables/nfqueue) and root, so it's normally only run via `run.py` (Docker or standalone). Direct iteration on a Linux box/VM:
|
||||||
|
```bash
|
||||||
|
cd backend && DEBUG=1 python3 app.py DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests (`tests/`)
|
||||||
|
No pytest — plain scripts driven against a **live, already-running** firegex instance (`http://127.0.0.1:4444` by default), root/Linux required.
|
||||||
|
```bash
|
||||||
|
cd tests
|
||||||
|
./run_tests.sh [password] # full suite (defaults to "testpassword")
|
||||||
|
python3 nfregex_test.py -p <pw> -m tcp [--ipv6] [--tls]
|
||||||
|
python3 nfproxy_test.py -p <pw> [--ipv6] [--tls]
|
||||||
|
python3 ph_test.py -p <pw> -m tcp|udp [--ipv6]
|
||||||
|
python3 tls_test.py -p <pw> [--ipv6]
|
||||||
|
python3 api_test.py -p <pw>
|
||||||
|
python3 nginx_test.py # the only test that doesn't need a live instance
|
||||||
|
```
|
||||||
|
CI (`.github/workflows/docker-image.yml`) does exactly: `python3 run.py start -P testpassword` then `cd tests && ./run_tests.sh`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Three tiers
|
||||||
|
React (Vite + Mantine + react-router + TanStack Query + socket.io-client) → FastAPI backend → per-module C++ binaries that do the actual packet interception via nftables/NFQUEUE. The backend mostly orchestrates: SQLite-backed config, spawning/talking to the C++ processes over unix sockets, and deploying the built React app.
|
||||||
|
|
||||||
|
### Backend router auto-discovery (`backend/utils/loader.py`)
|
||||||
|
Every `.py` file in `backend/routers/` is auto-imported and mounted at `/api/<filename>` if it exposes a module-level `app` (a `fastapi.APIRouter`). Optional module-level `startup()`, `shutdown()`, `reset(params)` async functions are collected and called by the app's lifespan/`/api/reset` handlers. **Adding a new module = adding a new file here** — nothing else needs to register it.
|
||||||
|
|
||||||
|
### Per-module pattern
|
||||||
|
Each filtering module (`nfproxy`, `nfregex`, `porthijack`, `firewall`, `tls`) follows the same shape:
|
||||||
|
- `backend/routers/<mod>.py` — FastAPI endpoints + its own SQLite DB, via `utils.sqlite.SQLite`.
|
||||||
|
- `backend/modules/<mod>/models.py` — plain `Service`/filter model, `from_dict`.
|
||||||
|
- `backend/modules/<mod>/firewall.py` — `FirewallManager`/`ServiceManager` pair: `FirewallManager` tracks one `ServiceManager` per DB row (in-memory), `ServiceManager.next(status)` starts/stops the interceptor/nft rules for that one service.
|
||||||
|
- `backend/modules/<mod>/nftables.py` — builds/tears down the module's nftables rules; the manager class extends `utils.NFTableManager` (a `Singleton`), so there's exactly one nft-rule-set owner per module.
|
||||||
|
|
||||||
|
### nfproxy vs nfregex
|
||||||
|
Both intercept via NFQUEUE, but differently:
|
||||||
|
- **nfregex** (`backend/binsrc/nfregex.cpp` → `cppregex`): pure C++, matches PCRE2/hyperscan regexes against the stream entirely in the binary. Faster, but rules are just regex+mode.
|
||||||
|
- **nfproxy** (`backend/binsrc/nfproxy.cpp`, `binsrc/pyproxy/*`, embeds libpython → `cpproxy`): user-supplied Python filter code (via the `firegex` pip package, `fgex-lib/`) runs inside the C++ process through the embedded interpreter, talking back to the backend over a unix socket for stats/exceptions. Much more flexible, slower than nfregex.
|
||||||
|
|
||||||
|
### TLS Decrypt bridge (`backend/modules/tls/`)
|
||||||
|
A `tls_streams` DB row describes one public `ip:port` to decrypt. `ssl_port`/`clear_port` are two loopback ports **deterministically derived from a hash of `ip:port`** (`get_tls_ports()`), not stored arbitrarily. nginx (config generated by `modules/tls/nginx.py`) does the actual TLS work per active stream:
|
||||||
|
1. `listen loopback:ssl_port ssl` → terminates the public TLS connection, forwards **plaintext** to `loopback:clear_port`.
|
||||||
|
2. `listen loopback:clear_port` → re-encrypts (`proxy_ssl on`) and forwards to the real `ip:port`. The real backend is expected to also speak TLS (mirrors a CTF challenge service that natively does TLS) — this is not a generic "terminate TLS in front of a plaintext service" reverse proxy.
|
||||||
|
|
||||||
|
The only unencrypted hop is the loopback leg between `ssl_port` and `clear_port` — an nfproxy/nfregex service must attach there (`target_type="tls"`, `tls_stream_id=...`) to see decrypted content. Both modules' `nftables.py` has a `resolve_target(srv)` helper that swaps in `(loopback_ip, clear_port)` for `target_type="tls"` services instead of the service's own `ip_int`/`port` (which mirror the stream's real destination for display only). Stopping a TLS stream cascades to stop dependent filter services (and vice versa on start); deleting a stream still referenced by a service is rejected. This cross-module logic lives in `routers/tls.py`, which imports `routers.nfproxy`/`routers.nfregex` directly (one-directional — those modules only import `modules.tls.*`, never `routers.tls`).
|
||||||
|
|
||||||
|
### SQLite wrapper (`backend/utils/sqlite.py`)
|
||||||
|
Each DB file's schema is versioned by hashing the Python schema dict; on mismatch the **entire file is deleted and recreated** (not migrated) — any schema dict change wipes that DB file on next startup, including `db/firegex.db`'s `keys_values` table (password/JWT secret). `dump()`/`load()` back the export/import backup feature; `dump()` deliberately excludes `password`/`secret` from exported `keys_values` rows, so `/api/import` explicitly re-preserves the current password/secret around the import.
|
||||||
|
|
||||||
|
### Real-time frontend updates
|
||||||
|
One Socket.IO event, `"update"`, whose payload is a list of strings treated as a react-query `queryKey` prefix (e.g. `["nfproxy"]`, `["tls_streams"]`). `App.tsx` does `queryClient.invalidateQueries({ queryKey: data })` — prefix matching invalidates any query whose key starts with that array. Convention: each module's mutating endpoints emit the same tag string that module's frontend queries use as the first `queryKey` element. `staleTime: Infinity` is set globally, so this event is the **only** automatic refresh mechanism — a missing/mismatched tag means silent staleness for other clients/tabs.
|
||||||
|
|
||||||
|
### Frontend structure
|
||||||
|
`pages/<Module>/` (routes) + `components/<Module>/` (ServiceRow, AddEditService, `utils.ts` with query key + API wrapper) per module, mirroring the backend split. Detail drill-down lives at `pages/<Module>/ServiceDetails.tsx` under a nested `:srv` route (see `App.tsx`'s `PageRouting`). Theme tokens are CSS variables in `src/index.css`; Mantine `Card` defaults are set globally in `index.tsx`. Prefer Mantine `Group`/`Stack` with `gap` over the legacy `Box className="center-flex"` utility classes still present in older parts of the codebase.
|
||||||
|
|
||||||
|
### `fgex-lib/`
|
||||||
|
Separate, independently-published PyPI package (`pip install fgex`/`firegex`) — the library users write nfproxy filters against (`@pyfilter`, `RawPacket`/`HttpRequest`/etc.), plus the `fgex` CLI and a local `proxysim` for testing filters without a real firegex instance. Not part of the backend/frontend build.
|
||||||
@ -16,7 +16,7 @@ RUN bun run build
|
|||||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
||||||
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
||||||
libnfnetlink libmnl libcap-ng-utils nftables \
|
libnfnetlink libmnl libcap-ng-utils nftables \
|
||||||
vectorscan libtins python3-nftables libpcap && dnf clean all
|
vectorscan libtins python3-nftables libpcap nginx nginx-mod-stream && dnf clean all
|
||||||
|
|
||||||
RUN mkdir -p /execute/modules
|
RUN mkdir -p /execute/modules
|
||||||
WORKDIR /execute
|
WORKDIR /execute
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import utils
|
|||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import base64
|
||||||
from fastapi import FastAPI, HTTPException, Depends, APIRouter
|
from fastapi import FastAPI, HTTPException, Depends, APIRouter
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
@ -218,6 +219,62 @@ async def reset_firegex(form: ResetRequest):
|
|||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
|
||||||
|
@api.get('/export')
|
||||||
|
async def export_db():
|
||||||
|
"""Export all configuration databases as JSON"""
|
||||||
|
dbs = {}
|
||||||
|
if not os.path.exists('db'):
|
||||||
|
return dbs
|
||||||
|
for f in os.listdir('db'):
|
||||||
|
if f.endswith('.db'):
|
||||||
|
temp_db = SQLite(os.path.join('db', f))
|
||||||
|
dbs[f] = temp_db.dump()
|
||||||
|
|
||||||
|
# Export nfproxy filters
|
||||||
|
if os.path.exists('db/nfproxy_filters'):
|
||||||
|
dbs['nfproxy_filters'] = {}
|
||||||
|
for f in os.listdir('db/nfproxy_filters'):
|
||||||
|
if f.endswith('.py'):
|
||||||
|
with open(os.path.join('db/nfproxy_filters', f), 'rb') as script_file:
|
||||||
|
dbs['nfproxy_filters'][f] = base64.b64encode(script_file.read()).decode('utf-8')
|
||||||
|
return dbs
|
||||||
|
|
||||||
|
@api.post('/import', response_model=StatusMessageModel)
|
||||||
|
async def import_db(data: dict):
|
||||||
|
"""Import all configuration databases from JSON"""
|
||||||
|
if not os.path.exists('db'):
|
||||||
|
os.makedirs('db')
|
||||||
|
|
||||||
|
# Backups never contain the password/secret (export_db strips them), so preserve
|
||||||
|
# the current session's own values instead of losing them on import.
|
||||||
|
current_password = db.get("password")
|
||||||
|
current_secret = db.get("secret")
|
||||||
|
|
||||||
|
for db_file, db_data in data.items():
|
||||||
|
if db_file.endswith('.db'):
|
||||||
|
temp_db = SQLite(os.path.join('db', db_file))
|
||||||
|
# Just load the data, we assume schemas exist or will be matched
|
||||||
|
# We don't delete the whole file to preserve DB_VERSION and schemas
|
||||||
|
temp_db.load(db_data)
|
||||||
|
elif db_file == 'nfproxy_filters':
|
||||||
|
if not os.path.exists('db/nfproxy_filters'):
|
||||||
|
os.makedirs('db/nfproxy_filters')
|
||||||
|
for f, script_content in db_data.items():
|
||||||
|
if f.endswith('.py'):
|
||||||
|
with open(os.path.join('db/nfproxy_filters', f), 'wb') as script_file:
|
||||||
|
script_file.write(base64.b64decode(script_content))
|
||||||
|
|
||||||
|
if current_password is not None:
|
||||||
|
db.put("password", current_password)
|
||||||
|
if current_secret is not None:
|
||||||
|
db.put("secret", current_secret)
|
||||||
|
|
||||||
|
# Restart the application state
|
||||||
|
await shutdown_main()
|
||||||
|
await startup_main()
|
||||||
|
|
||||||
|
return {'status': 'ok'}
|
||||||
|
|
||||||
app.include_router(api)
|
app.include_router(api)
|
||||||
frontend_deploy(app)
|
frontend_deploy(app)
|
||||||
|
|
||||||
|
|||||||
@ -451,6 +451,7 @@ class NfQueue {
|
|||||||
// error code taken from the linux kernel:
|
// error code taken from the linux kernel:
|
||||||
// https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27
|
// https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27
|
||||||
#define ENOTSUPP 524 /* Operation is not supported */
|
#define ENOTSUPP 524 /* Operation is not supported */
|
||||||
|
#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
|
||||||
|
|
||||||
if (error_msg->error != -ENOTSUPP && error_msg->error != -EOPNOTSUPP) {
|
if (error_msg->error != -ENOTSUPP && error_msg->error != -EOPNOTSUPP) {
|
||||||
_clear();
|
_clear();
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
#define DEBUG
|
||||||
#define PY_SSIZE_T_CLEAN
|
#define PY_SSIZE_T_CLEAN
|
||||||
#include <Python.h>
|
#include <Python.h>
|
||||||
|
|
||||||
|
|||||||
@ -97,7 +97,7 @@ class PyProxyQueue: public NfQueue::ThreadNfQueue<PyProxyQueue> {
|
|||||||
pyq->pkt->drop();// This is needed because the callback has to take the updated pkt pointer!
|
pyq->pkt->drop();// This is needed because the callback has to take the updated pkt pointer!
|
||||||
}
|
}
|
||||||
|
|
||||||
void filter_action(NfQueue::PktRequest<PyProxyQueue>* pkt, Stream& stream, const string& data){
|
void filter_action(NfQueue::PktRequest<PyProxyQueue>* pkt, Stream& stream, const string& data, bool is_client){
|
||||||
auto stream_search = sctx.streams_ctx.find(pkt->sid);
|
auto stream_search = sctx.streams_ctx.find(pkt->sid);
|
||||||
pyfilter_ctx* stream_match;
|
pyfilter_ctx* stream_match;
|
||||||
if (stream_search == sctx.streams_ctx.end()){
|
if (stream_search == sctx.streams_ctx.end()){
|
||||||
@ -129,7 +129,7 @@ class PyProxyQueue: public NfQueue::ThreadNfQueue<PyProxyQueue> {
|
|||||||
stream_match = stream_search->second;
|
stream_match = stream_search->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = stream_match->handle_packet(pkt, data);
|
auto result = stream_match->handle_packet(pkt, data, is_client);
|
||||||
switch(result.action){
|
switch(result.action){
|
||||||
case PyFilterResponse::ACCEPT:
|
case PyFilterResponse::ACCEPT:
|
||||||
return pkt->accept();
|
return pkt->accept();
|
||||||
@ -169,21 +169,21 @@ class PyProxyQueue: public NfQueue::ThreadNfQueue<PyProxyQueue> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void on_data_recv(Stream& stream, PyProxyQueue* pyq, const string& data) {
|
static void on_data_recv(Stream& stream, PyProxyQueue* pyq, const string& data, bool is_client) {
|
||||||
pyq->pkt->fix_data_payload();
|
pyq->pkt->fix_data_payload();
|
||||||
pyq->filter_action(pyq->pkt, stream, data); //Only here the rebuilt_tcp_data is set
|
pyq->filter_action(pyq->pkt, stream, data, is_client); //Only here the rebuilt_tcp_data is set
|
||||||
}
|
}
|
||||||
|
|
||||||
//Input data filtering
|
//Input data filtering
|
||||||
static void on_client_data(Stream& stream, PyProxyQueue* pyq) {
|
static void on_client_data(Stream& stream, PyProxyQueue* pyq) {
|
||||||
auto data = stream.client_payload();
|
auto data = stream.client_payload();
|
||||||
on_data_recv(stream, pyq, string((char*)data.data(), data.size()));
|
on_data_recv(stream, pyq, string((char*)data.data(), data.size()), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Server data filtering
|
//Server data filtering
|
||||||
static void on_server_data(Stream& stream, PyProxyQueue* pyq) {
|
static void on_server_data(Stream& stream, PyProxyQueue* pyq) {
|
||||||
auto data = stream.server_payload();
|
auto data = stream.server_payload();
|
||||||
on_data_recv(stream, pyq, string((char*)data.data(), data.size()));
|
on_data_recv(stream, pyq, string((char*)data.data(), data.size()), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A stream was terminated. The second argument is the reason why it was terminated
|
// A stream was terminated. The second argument is the reason why it was terminated
|
||||||
|
|||||||
@ -105,7 +105,8 @@ struct pyfilter_ctx {
|
|||||||
|
|
||||||
py_filter_response handle_packet(
|
py_filter_response handle_packet(
|
||||||
NfQueue::PktRequest<PyProxyQueue>* pkt,
|
NfQueue::PktRequest<PyProxyQueue>* pkt,
|
||||||
const string& data
|
const string& data,
|
||||||
|
bool is_client
|
||||||
){
|
){
|
||||||
PyObject * packet_info = PyDict_New();
|
PyObject * packet_info = PyDict_New();
|
||||||
|
|
||||||
@ -113,7 +114,7 @@ struct pyfilter_ctx {
|
|||||||
set_item_to_dict(packet_info, "data", PyBytes_FromStringAndSize(data.c_str(), data.size()));
|
set_item_to_dict(packet_info, "data", PyBytes_FromStringAndSize(data.c_str(), data.size()));
|
||||||
set_item_to_dict(packet_info, "l4_size", PyLong_FromLong(pkt->data_size()));
|
set_item_to_dict(packet_info, "l4_size", PyLong_FromLong(pkt->data_size()));
|
||||||
set_item_to_dict(packet_info, "raw_packet", PyBytes_FromStringAndSize(pkt->packet.c_str(), pkt->packet.size()));
|
set_item_to_dict(packet_info, "raw_packet", PyBytes_FromStringAndSize(pkt->packet.c_str(), pkt->packet.size()));
|
||||||
set_item_to_dict(packet_info, "is_input", PyBool_FromLong(pkt->is_input));
|
set_item_to_dict(packet_info, "is_input", PyBool_FromLong(is_client));
|
||||||
set_item_to_dict(packet_info, "is_ipv6", PyBool_FromLong(pkt->is_ipv6));
|
set_item_to_dict(packet_info, "is_ipv6", PyBool_FromLong(pkt->is_ipv6));
|
||||||
set_item_to_dict(packet_info, "is_tcp", PyBool_FromLong(pkt->l4_proto == NfQueue::L4Proto::TCP));
|
set_item_to_dict(packet_info, "is_tcp", PyBool_FromLong(pkt->l4_proto == NfQueue::L4Proto::TCP));
|
||||||
|
|
||||||
@ -243,6 +244,7 @@ typedef map<stream_id, pyfilter_ctx*> matching_map;
|
|||||||
struct stream_ctx {
|
struct stream_ctx {
|
||||||
|
|
||||||
matching_map streams_ctx;
|
matching_map streams_ctx;
|
||||||
|
|
||||||
NfQueue::tcp_ack_map tcp_ack_ctx;
|
NfQueue::tcp_ack_map tcp_ack_ctx;
|
||||||
|
|
||||||
void clean_stream_by_id(stream_id sid){
|
void clean_stream_by_id(stream_id sid){
|
||||||
|
|||||||
@ -9,6 +9,15 @@ if [ -n "$SOCKET_DIR" ]; then
|
|||||||
chmod 755 "$SOCKET_DIR"
|
chmod 755 "$SOCKET_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Set host sysctls while running as root.
|
||||||
|
if [ -d "/sys_host" ]; then
|
||||||
|
echo "[*] Setting host sysctls..."
|
||||||
|
echo 1 > /sys_host/net.ipv4.conf.all.forwarding 2>/dev/null || true
|
||||||
|
echo 1 > /sys_host/net.ipv6.conf.all.forwarding 2>/dev/null || true
|
||||||
|
echo 1 > /sys_host/net.ipv4.conf.all.route_localnet 2>/dev/null || true
|
||||||
|
echo 1 > /sys_host/net.ipv4.ip_forward 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[*] Attempting to start with capabilities..."
|
echo "[*] Attempting to start with capabilities..."
|
||||||
|
|
||||||
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \
|
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \
|
||||||
@ -21,7 +30,8 @@ then
|
|||||||
--keep=1 \
|
--keep=1 \
|
||||||
--user=nobody \
|
--user=nobody \
|
||||||
--addamb=cap_net_admin,cap_sys_nice \
|
--addamb=cap_net_admin,cap_sys_nice \
|
||||||
-- -c "python3 /execute/app.py DOCKER"
|
--shell=/usr/bin/python3 \
|
||||||
|
-- /execute/app.py DOCKER
|
||||||
else
|
else
|
||||||
echo "[!] capsh failed, running with root user"
|
echo "[!] capsh failed, running with root user"
|
||||||
exec python3 /execute/app.py DOCKER
|
exec python3 /execute/app.py DOCKER
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import traceback
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
import time
|
import time
|
||||||
from utils import run_func
|
from utils import run_func
|
||||||
from utils import DEBUG
|
|
||||||
from utils import nicenessify
|
from utils import nicenessify
|
||||||
|
|
||||||
nft = FiregexTables()
|
nft = FiregexTables()
|
||||||
@ -65,8 +64,6 @@ class FiregexInterceptor:
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
out_data = (await self.process.stdout.read(1024*10)).decode(errors="ignore")
|
out_data = (await self.process.stdout.read(1024*10)).decode(errors="ignore")
|
||||||
if DEBUG:
|
|
||||||
print(out_data, end="")
|
|
||||||
except asyncio.exceptions.LimitOverrunError:
|
except asyncio.exceptions.LimitOverrunError:
|
||||||
self.outstrem_buffer = ""
|
self.outstrem_buffer = ""
|
||||||
continue
|
continue
|
||||||
@ -90,11 +87,11 @@ class FiregexInterceptor:
|
|||||||
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.STDOUT,
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
env={
|
env=dict(os.environ, **{
|
||||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
||||||
"FIREGEX_NFPROXY_SOCK": self.sock_path
|
"FIREGEX_NFPROXY_SOCK": self.sock_path
|
||||||
},
|
}),
|
||||||
)
|
)
|
||||||
nicenessify(-10, self.process.pid)
|
nicenessify(-10, self.process.pid)
|
||||||
self.outstrem_task = asyncio.create_task(self._stream_handler())
|
self.outstrem_task = asyncio.create_task(self._stream_handler())
|
||||||
@ -137,7 +134,6 @@ class FiregexInterceptor:
|
|||||||
raise HTTPException(status_code=500, detail="Can't read from nfq client") from e
|
raise HTTPException(status_code=500, detail="Can't read from nfq client") from e
|
||||||
if line.startswith("BLOCKED "):
|
if line.startswith("BLOCKED "):
|
||||||
filter_name = line.split()[1]
|
filter_name = line.split()[1]
|
||||||
print("BLOCKED", filter_name)
|
|
||||||
async with self.filter_map_lock:
|
async with self.filter_map_lock:
|
||||||
if filter_name in self.filter_map:
|
if filter_name in self.filter_map:
|
||||||
self.filter_map[filter_name].blocked_packets+=1
|
self.filter_map[filter_name].blocked_packets+=1
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from modules.nfproxy.firegex import FiregexInterceptor
|
|||||||
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
||||||
from modules.nfproxy.models import Service, PyFilter
|
from modules.nfproxy.models import Service, PyFilter
|
||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
|
from utils.sqlite import SQLite
|
||||||
from utils import run_func
|
from utils import run_func
|
||||||
|
|
||||||
class STATUS:
|
class STATUS:
|
||||||
@ -49,16 +50,17 @@ class ServiceManager:
|
|||||||
def __update_status_db(self, status):
|
def __update_status_db(self, status):
|
||||||
self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.srv.id)
|
self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.srv.id)
|
||||||
|
|
||||||
async def next(self,to):
|
async def next(self,to,persist:bool=True):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
if to == STATUS.STOP:
|
if to == STATUS.STOP:
|
||||||
await self.stop()
|
await self.stop(persist=persist)
|
||||||
if to == STATUS.ACTIVE:
|
if to == STATUS.ACTIVE:
|
||||||
await self.restart()
|
await self.restart()
|
||||||
|
|
||||||
def _set_status(self,status):
|
def _set_status(self,status,persist:bool=True):
|
||||||
self.status = status
|
self.status = status
|
||||||
self.__update_status_db(status)
|
if persist:
|
||||||
|
self.__update_status_db(status)
|
||||||
|
|
||||||
def read_outstrem_buffer(self):
|
def read_outstrem_buffer(self):
|
||||||
if self.interceptor:
|
if self.interceptor:
|
||||||
@ -73,12 +75,12 @@ class ServiceManager:
|
|||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
self._set_status(STATUS.ACTIVE)
|
self._set_status(STATUS.ACTIVE)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self,persist:bool=True):
|
||||||
nft.delete(self.srv)
|
nft.delete(self.srv)
|
||||||
if self.interceptor:
|
if self.interceptor:
|
||||||
await self.interceptor.stop()
|
await self.interceptor.stop()
|
||||||
self.interceptor = None
|
self.interceptor = None
|
||||||
self._set_status(STATUS.STOP)
|
self._set_status(STATUS.STOP,persist=persist)
|
||||||
|
|
||||||
async def restart(self):
|
async def restart(self):
|
||||||
await self.stop()
|
await self.stop()
|
||||||
@ -88,6 +90,8 @@ class ServiceManager:
|
|||||||
async with self.lock:
|
async with self.lock:
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FirewallManager:
|
class FirewallManager:
|
||||||
def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
|
def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -98,12 +102,16 @@ class FirewallManager:
|
|||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
for key in list(self.service_table.keys()):
|
for key in list(self.service_table.keys()):
|
||||||
await self.remove(key)
|
try:
|
||||||
|
await self.remove(key, persist=False)
|
||||||
|
except Exception:
|
||||||
|
# Don't let one broken service block shutdown of the others
|
||||||
|
self.service_table.pop(key, None)
|
||||||
|
|
||||||
async def remove(self,srv_id):
|
async def remove(self,srv_id,persist:bool=True):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
if srv_id in self.service_table:
|
if srv_id in self.service_table:
|
||||||
await self.service_table[srv_id].next(STATUS.STOP)
|
await self.service_table[srv_id].next(STATUS.STOP,persist=persist)
|
||||||
del self.service_table[srv_id]
|
del self.service_table[srv_id]
|
||||||
|
|
||||||
async def init(self):
|
async def init(self):
|
||||||
@ -112,12 +120,14 @@ class FirewallManager:
|
|||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
for srv in self.db.query('SELECT * FROM services;'):
|
services = self.db.query('SELECT * FROM services;')
|
||||||
srv = Service.from_dict(srv)
|
|
||||||
if srv.id in self.service_table:
|
for srv in services:
|
||||||
|
if srv["service_id"] in self.service_table:
|
||||||
continue
|
continue
|
||||||
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
srv_obj = Service.from_dict(srv)
|
||||||
await self.service_table[srv.id].next(srv.status)
|
self.service_table[srv_obj.id] = ServiceManager(srv_obj, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
||||||
|
await self.service_table[srv_obj.id].next(srv_obj.status)
|
||||||
|
|
||||||
def get(self,srv_id) -> ServiceManager:
|
def get(self,srv_id) -> ServiceManager:
|
||||||
if srv_id in self.service_table:
|
if srv_id in self.service_table:
|
||||||
|
|||||||
@ -3,11 +3,15 @@ class Service:
|
|||||||
self,
|
self,
|
||||||
service_id: str,
|
service_id: str,
|
||||||
status: str,
|
status: str,
|
||||||
port: int,
|
port: int | None,
|
||||||
name: str,
|
name: str,
|
||||||
proto: str,
|
proto: str,
|
||||||
ip_int: str,
|
ip_int: str | None,
|
||||||
fail_open: bool,
|
fail_open: bool,
|
||||||
|
target_type: str = "flow",
|
||||||
|
tls_stream_id: str | None = None,
|
||||||
|
tls_cert: str | None = None,
|
||||||
|
tls_key: str | None = None,
|
||||||
**other,
|
**other,
|
||||||
):
|
):
|
||||||
self.id = service_id
|
self.id = service_id
|
||||||
@ -17,6 +21,10 @@ class Service:
|
|||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.ip_int = ip_int
|
self.ip_int = ip_int
|
||||||
self.fail_open = fail_open
|
self.fail_open = fail_open
|
||||||
|
self.target_type = target_type
|
||||||
|
self.tls_stream_id = tls_stream_id
|
||||||
|
self.tls_cert = tls_cert
|
||||||
|
self.tls_key = tls_key
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, var: dict):
|
def from_dict(cls, var: dict):
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from modules.nfproxy.models import Service
|
from modules.nfproxy.models import Service
|
||||||
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
||||||
|
from modules.tls.manager import get_tls_ports, TLSManager
|
||||||
|
|
||||||
def convert_protocol_to_l4(proto:str):
|
def convert_protocol_to_l4(proto:str):
|
||||||
if proto == "tcp":
|
if proto == "tcp":
|
||||||
@ -9,6 +10,20 @@ def convert_protocol_to_l4(proto:str):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Invalid protocol")
|
raise Exception("Invalid protocol")
|
||||||
|
|
||||||
|
def resolve_target(srv: Service) -> tuple[str, int] | None:
|
||||||
|
"""Returns the (ip_int, port) a service's intercept rule should actually match.
|
||||||
|
For target_type=='tls' this is the loopback clear_port where nginx exposes the
|
||||||
|
decrypted plaintext, not the service's own (real backend) ip_int/port. Returns
|
||||||
|
None if the linked TLS stream can't be resolved (e.g. it was deleted)."""
|
||||||
|
if srv.target_type == "tls":
|
||||||
|
tls_stream = TLSManager().get_stream(srv.tls_stream_id)
|
||||||
|
if not tls_stream:
|
||||||
|
return None
|
||||||
|
_, clear_port = get_tls_ports(tls_stream["ip_int"], tls_stream["port"])
|
||||||
|
loopback_ip = "::1" if ip_family(tls_stream["ip_int"]) == "ip6" else "127.0.0.1"
|
||||||
|
return loopback_ip, clear_port
|
||||||
|
return srv.ip_int, srv.port
|
||||||
|
|
||||||
class FiregexFilter:
|
class FiregexFilter:
|
||||||
def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int):
|
def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int):
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -18,13 +33,25 @@ class FiregexFilter:
|
|||||||
self.ip_int = str(ip_int)
|
self.ip_int = str(ip_int)
|
||||||
|
|
||||||
def __eq__(self, o: object) -> bool:
|
def __eq__(self, o: object) -> bool:
|
||||||
if isinstance(o, FiregexFilter) or isinstance(o, Service):
|
if isinstance(o, FiregexFilter):
|
||||||
return self.port == o.port and self.proto == convert_protocol_to_l4(o.proto) and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
||||||
|
if isinstance(o, Service):
|
||||||
|
return self.matches(o)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def matches(self, srv: Service) -> bool:
|
||||||
|
if self.proto != convert_protocol_to_l4(srv.proto):
|
||||||
|
return False
|
||||||
|
target = resolve_target(srv)
|
||||||
|
if target is None:
|
||||||
|
return False
|
||||||
|
target_ip, target_port = target
|
||||||
|
return self.port == target_port and ip_parse(self.ip_int) == ip_parse(target_ip)
|
||||||
|
|
||||||
class FiregexTables(NFTableManager):
|
class FiregexTables(NFTableManager):
|
||||||
input_chain = "nfproxy_input"
|
input_chain = "nfproxy_input"
|
||||||
output_chain = "nfproxy_output"
|
output_chain = "nfproxy_output"
|
||||||
|
nat_chain = "nfproxy_nat"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__([
|
super().__init__([
|
||||||
@ -54,33 +81,39 @@ class FiregexTables(NFTableManager):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def add(self, srv:Service, queue_range):
|
def add(self, srv:Service, queue_range):
|
||||||
|
|
||||||
for ele in self.get():
|
for ele in self.get():
|
||||||
if ele.__eq__(srv):
|
if ele.matches(srv):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
target = resolve_target(srv)
|
||||||
|
if target is None:
|
||||||
|
return
|
||||||
|
target_ip, target_port = target
|
||||||
|
|
||||||
init, end = queue_range
|
init, end = queue_range
|
||||||
if init > end:
|
if init > end:
|
||||||
init, end = end, init
|
init, end = end, init
|
||||||
|
|
||||||
self.cmd(
|
self.cmd(
|
||||||
{ "insert":{ "rule": {
|
{ "insert":{ "rule": { # Send non-TLS outbound traffic to NFQUEUE (Client -> Server)
|
||||||
"family": "inet",
|
"family": "inet",
|
||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
"chain": self.output_chain,
|
"chain": self.output_chain,
|
||||||
"expr": [
|
"expr": [
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
{'match': {'left': {'payload': {'protocol': ip_family(target_ip), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(target_ip)}},
|
||||||
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "sport"}}, "op": "==", "right": int(srv.port)}},
|
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "dport"}}, "op": "==", "right": int(target_port)}},
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
]
|
]
|
||||||
}}},
|
}}},
|
||||||
{"insert":{"rule":{
|
{"insert":{"rule":{ # Send non-TLS inbound traffic to NFQUEUE (Server -> Client)
|
||||||
"family": "inet",
|
"family": "inet",
|
||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
"chain": self.input_chain,
|
"chain": self.input_chain,
|
||||||
"expr": [
|
"expr": [
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
{'match': {'left': {'payload': {'protocol': ip_family(target_ip), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(target_ip)}},
|
||||||
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "dport"}}, "op": "==", "right": int(srv.port)}},
|
{'match': {"left": { "payload": {"protocol": convert_protocol_to_l4(str(srv.proto)), "field": "sport"}}, "op": "==", "right": int(target_port)}},
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
]
|
]
|
||||||
@ -91,23 +124,26 @@ class FiregexTables(NFTableManager):
|
|||||||
def get(self) -> list[FiregexFilter]:
|
def get(self) -> list[FiregexFilter]:
|
||||||
res = []
|
res = []
|
||||||
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]):
|
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]):
|
||||||
ip_int = None
|
try:
|
||||||
if isinstance(filter["expr"][0]["match"]["right"],str):
|
ip_int = None
|
||||||
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
if isinstance(filter["expr"][0]["match"]["right"],str):
|
||||||
else:
|
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
||||||
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
else:
|
||||||
res.append(FiregexFilter(
|
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
||||||
target=filter["chain"],
|
res.append(FiregexFilter(
|
||||||
id=int(filter["handle"]),
|
target=filter["chain"],
|
||||||
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
id=int(filter["handle"]),
|
||||||
port=filter["expr"][1]["match"]["right"],
|
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
||||||
ip_int=ip_int
|
port=filter["expr"][1]["match"]["right"],
|
||||||
))
|
ip_int=ip_int
|
||||||
|
))
|
||||||
|
except (KeyError, TypeError, IndexError):
|
||||||
|
pass # Rule has unexpected structure (e.g., intermediate NAT rule), skip
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def delete(self, srv:Service):
|
def delete(self, srv:Service):
|
||||||
for filter in self.get():
|
for filter in self.get():
|
||||||
if filter.__eq__(srv):
|
if filter.matches(srv):
|
||||||
self.cmd({ "delete":{ "rule": {
|
self.cmd({ "delete":{ "rule": {
|
||||||
"family": "inet",
|
"family": "inet",
|
||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
|
|||||||
@ -101,11 +101,11 @@ class FiregexInterceptor:
|
|||||||
self.process = await asyncio.create_subprocess_exec(
|
self.process = await asyncio.create_subprocess_exec(
|
||||||
proxy_binary_path,
|
proxy_binary_path,
|
||||||
stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
|
||||||
env={
|
env=dict(os.environ, **{
|
||||||
"MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block",
|
"MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block",
|
||||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
||||||
},
|
}),
|
||||||
)
|
)
|
||||||
nicenessify(-10, self.process.pid)
|
nicenessify(-10, self.process.pid)
|
||||||
line_fut = self.process.stdout.readuntil()
|
line_fut = self.process.stdout.readuntil()
|
||||||
|
|||||||
@ -43,19 +43,20 @@ class ServiceManager:
|
|||||||
def __update_status_db(self, status):
|
def __update_status_db(self, status):
|
||||||
self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.srv.id)
|
self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.srv.id)
|
||||||
|
|
||||||
async def next(self,to):
|
async def next(self,to,persist:bool=True):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
if to == STATUS.STOP:
|
if to == STATUS.STOP:
|
||||||
await self.stop()
|
await self.stop(persist=persist)
|
||||||
if to == STATUS.ACTIVE:
|
if to == STATUS.ACTIVE:
|
||||||
await self.restart()
|
await self.restart()
|
||||||
|
|
||||||
def _stats_updater(self,filter:RegexFilter):
|
def _stats_updater(self,filter:RegexFilter):
|
||||||
self.db.query("UPDATE regexes SET blocked_packets = ? WHERE regex_id = ?;", filter.blocked, filter.id)
|
self.db.query("UPDATE regexes SET blocked_packets = ? WHERE regex_id = ?;", filter.blocked, filter.id)
|
||||||
|
|
||||||
def _set_status(self,status):
|
def _set_status(self,status,persist:bool=True):
|
||||||
self.status = status
|
self.status = status
|
||||||
self.__update_status_db(status)
|
if persist:
|
||||||
|
self.__update_status_db(status)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
if not self.interceptor:
|
if not self.interceptor:
|
||||||
@ -64,12 +65,12 @@ class ServiceManager:
|
|||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
self._set_status(STATUS.ACTIVE)
|
self._set_status(STATUS.ACTIVE)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self,persist:bool=True):
|
||||||
nft.delete(self.srv)
|
nft.delete(self.srv)
|
||||||
if self.interceptor:
|
if self.interceptor:
|
||||||
await self.interceptor.stop()
|
await self.interceptor.stop()
|
||||||
self.interceptor = None
|
self.interceptor = None
|
||||||
self._set_status(STATUS.STOP)
|
self._set_status(STATUS.STOP,persist=persist)
|
||||||
|
|
||||||
async def restart(self):
|
async def restart(self):
|
||||||
await self.stop()
|
await self.stop()
|
||||||
@ -79,6 +80,8 @@ class ServiceManager:
|
|||||||
async with self.lock:
|
async with self.lock:
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FirewallManager:
|
class FirewallManager:
|
||||||
def __init__(self, db:SQLite):
|
def __init__(self, db:SQLite):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -87,12 +90,16 @@ class FirewallManager:
|
|||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
for key in list(self.service_table.keys()):
|
for key in list(self.service_table.keys()):
|
||||||
await self.remove(key)
|
try:
|
||||||
|
await self.remove(key, persist=False)
|
||||||
|
except Exception:
|
||||||
|
# Don't let one broken service block shutdown of the others
|
||||||
|
self.service_table.pop(key, None)
|
||||||
|
|
||||||
async def remove(self,srv_id):
|
async def remove(self,srv_id,persist:bool=True):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
if srv_id in self.service_table:
|
if srv_id in self.service_table:
|
||||||
await self.service_table[srv_id].next(STATUS.STOP)
|
await self.service_table[srv_id].next(STATUS.STOP,persist=persist)
|
||||||
del self.service_table[srv_id]
|
del self.service_table[srv_id]
|
||||||
|
|
||||||
async def init(self):
|
async def init(self):
|
||||||
@ -101,12 +108,14 @@ class FirewallManager:
|
|||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
for srv in self.db.query('SELECT * FROM services;'):
|
services = self.db.query('SELECT * FROM services;')
|
||||||
srv = Service.from_dict(srv)
|
|
||||||
if srv.id in self.service_table:
|
for srv in services:
|
||||||
|
if srv["service_id"] in self.service_table:
|
||||||
continue
|
continue
|
||||||
self.service_table[srv.id] = ServiceManager(srv, self.db)
|
srv_obj = Service.from_dict(srv)
|
||||||
await self.service_table[srv.id].next(srv.status)
|
self.service_table[srv_obj.id] = ServiceManager(srv_obj, self.db)
|
||||||
|
await self.service_table[srv_obj.id].next(srv_obj.status)
|
||||||
|
|
||||||
def get(self,srv_id) -> ServiceManager:
|
def get(self,srv_id) -> ServiceManager:
|
||||||
if srv_id in self.service_table:
|
if srv_id in self.service_table:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, fail_open: bool, **other):
|
def __init__(self, service_id: str, status: str, port: int | None, name: str, proto: str, ip_int: str | None, fail_open: bool, target_type: str = "flow", tls_stream_id: str | None = None, tls_cert: str | None = None, tls_key: str | None = None, **other):
|
||||||
self.id = service_id
|
self.id = service_id
|
||||||
self.status = status
|
self.status = status
|
||||||
self.port = port
|
self.port = port
|
||||||
@ -9,6 +9,10 @@ class Service:
|
|||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.ip_int = ip_int
|
self.ip_int = ip_int
|
||||||
self.fail_open = fail_open
|
self.fail_open = fail_open
|
||||||
|
self.target_type = target_type
|
||||||
|
self.tls_stream_id = tls_stream_id
|
||||||
|
self.tls_cert = tls_cert
|
||||||
|
self.tls_key = tls_key
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, var: dict):
|
def from_dict(cls, var: dict):
|
||||||
|
|||||||
@ -1,5 +1,20 @@
|
|||||||
from modules.nfregex.models import Service
|
from modules.nfregex.models import Service
|
||||||
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json
|
||||||
|
from modules.tls.manager import get_tls_ports, TLSManager
|
||||||
|
|
||||||
|
def resolve_target(srv: Service) -> tuple[str, int] | None:
|
||||||
|
"""Returns the (ip_int, port) a service's intercept rule should actually match.
|
||||||
|
For target_type=='tls' this is the loopback clear_port where nginx exposes the
|
||||||
|
decrypted plaintext, not the service's own (real backend) ip_int/port. Returns
|
||||||
|
None if the linked TLS stream can't be resolved (e.g. it was deleted)."""
|
||||||
|
if srv.target_type == "tls":
|
||||||
|
tls_stream = TLSManager().get_stream(srv.tls_stream_id)
|
||||||
|
if not tls_stream:
|
||||||
|
return None
|
||||||
|
_, clear_port = get_tls_ports(tls_stream["ip_int"], tls_stream["port"])
|
||||||
|
loopback_ip = "::1" if ip_family(tls_stream["ip_int"]) == "ip6" else "127.0.0.1"
|
||||||
|
return loopback_ip, clear_port
|
||||||
|
return srv.ip_int, srv.port
|
||||||
|
|
||||||
class FiregexFilter:
|
class FiregexFilter:
|
||||||
def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int):
|
def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int):
|
||||||
@ -10,13 +25,25 @@ class FiregexFilter:
|
|||||||
self.ip_int = str(ip_int)
|
self.ip_int = str(ip_int)
|
||||||
|
|
||||||
def __eq__(self, o: object) -> bool:
|
def __eq__(self, o: object) -> bool:
|
||||||
if isinstance(o, FiregexFilter) or isinstance(o, Service):
|
if isinstance(o, FiregexFilter):
|
||||||
return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int)
|
||||||
|
if isinstance(o, Service):
|
||||||
|
return self.matches(o)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def matches(self, srv: Service) -> bool:
|
||||||
|
if self.proto != str(srv.proto):
|
||||||
|
return False
|
||||||
|
target = resolve_target(srv)
|
||||||
|
if target is None:
|
||||||
|
return False
|
||||||
|
target_ip, target_port = target
|
||||||
|
return self.port == target_port and ip_parse(self.ip_int) == ip_parse(target_ip)
|
||||||
|
|
||||||
class FiregexTables(NFTableManager):
|
class FiregexTables(NFTableManager):
|
||||||
input_chain = "nfregex_input"
|
input_chain = "nfregex_input"
|
||||||
output_chain = "nfregex_output"
|
output_chain = "nfregex_output"
|
||||||
|
nat_chain = "nfregex_nat"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__([
|
super().__init__([
|
||||||
@ -46,22 +73,28 @@ class FiregexTables(NFTableManager):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def add(self, srv:Service, queue_range):
|
def add(self, srv:Service, queue_range):
|
||||||
|
|
||||||
for ele in self.get():
|
for ele in self.get():
|
||||||
if ele.__eq__(srv):
|
if ele.matches(srv):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
target = resolve_target(srv)
|
||||||
|
if target is None:
|
||||||
|
return
|
||||||
|
target_ip, target_port = target
|
||||||
|
|
||||||
init, end = queue_range
|
init, end = queue_range
|
||||||
if init > end:
|
if init > end:
|
||||||
init, end = end, init
|
init, end = end, init
|
||||||
|
|
||||||
self.cmd(
|
self.cmd(
|
||||||
{ "insert":{ "rule": {
|
{ "insert":{ "rule": {
|
||||||
"family": "inet",
|
"family": "inet",
|
||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
"chain": self.output_chain,
|
"chain": self.output_chain,
|
||||||
"expr": [
|
"expr": [
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
{'match': {'left': {'payload': {'protocol': ip_family(target_ip), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(target_ip)}},
|
||||||
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}},
|
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(target_port)}},
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}},
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
]
|
]
|
||||||
@ -71,8 +104,8 @@ class FiregexTables(NFTableManager):
|
|||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
"chain": self.input_chain,
|
"chain": self.input_chain,
|
||||||
"expr": [
|
"expr": [
|
||||||
{'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}},
|
{'match': {'left': {'payload': {'protocol': ip_family(target_ip), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(target_ip)}},
|
||||||
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}},
|
{'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(target_port)}},
|
||||||
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
{"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}},
|
||||||
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
{"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}}
|
||||||
]
|
]
|
||||||
@ -83,27 +116,33 @@ class FiregexTables(NFTableManager):
|
|||||||
def get(self) -> list[FiregexFilter]:
|
def get(self) -> list[FiregexFilter]:
|
||||||
res = []
|
res = []
|
||||||
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]):
|
for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]):
|
||||||
ip_int = None
|
try:
|
||||||
if isinstance(filter["expr"][0]["match"]["right"],str):
|
ip_int = None
|
||||||
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
if isinstance(filter["expr"][0]["match"]["right"],str):
|
||||||
else:
|
ip_int = str(ip_parse(filter["expr"][0]["match"]["right"]))
|
||||||
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
else:
|
||||||
res.append(FiregexFilter(
|
ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}'
|
||||||
target=filter["chain"],
|
res.append(FiregexFilter(
|
||||||
id=int(filter["handle"]),
|
target=filter["chain"],
|
||||||
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
id=int(filter["handle"]),
|
||||||
port=filter["expr"][1]["match"]["right"],
|
proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"],
|
||||||
ip_int=ip_int
|
port=filter["expr"][1]["match"]["right"],
|
||||||
))
|
ip_int=ip_int
|
||||||
|
))
|
||||||
|
except (KeyError, TypeError, IndexError):
|
||||||
|
pass # Rule has unexpected structure (e.g., intermediate NAT rule), skip
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def delete(self, srv:Service):
|
def delete(self, srv:Service):
|
||||||
|
delete_cmds = []
|
||||||
for filter in self.get():
|
for filter in self.get():
|
||||||
if filter.__eq__(srv):
|
if filter.matches(srv):
|
||||||
self.cmd({ "delete":{ "rule": {
|
delete_cmds.append({ "delete":{ "rule": {
|
||||||
"family": "inet",
|
"family": "inet",
|
||||||
"table": self.table_name,
|
"table": self.table_name,
|
||||||
"chain": filter.target,
|
"chain": filter.target,
|
||||||
"handle": filter.id
|
"handle": filter.id
|
||||||
}}})
|
}}})
|
||||||
|
if delete_cmds:
|
||||||
|
self.cmd(*delete_cmds)
|
||||||
|
|
||||||
@ -22,20 +22,21 @@ class ServiceManager:
|
|||||||
nft.add(self.srv)
|
nft.add(self.srv)
|
||||||
self._set_status(True)
|
self._set_status(True)
|
||||||
|
|
||||||
async def disable(self):
|
async def disable(self,persist:bool=True):
|
||||||
if self.active:
|
if self.active:
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
nft.delete(self.srv)
|
nft.delete(self.srv)
|
||||||
self._set_status(False)
|
self._set_status(False,persist=persist)
|
||||||
|
|
||||||
async def refresh(self, srv:Service):
|
async def refresh(self, srv:Service):
|
||||||
self.srv = srv
|
self.srv = srv
|
||||||
if self.active:
|
if self.active:
|
||||||
await self.restart()
|
await self.restart()
|
||||||
|
|
||||||
def _set_status(self,active):
|
def _set_status(self,active,persist:bool=True):
|
||||||
self.active = active
|
self.active = active
|
||||||
self.db.query("UPDATE services SET active = ? WHERE service_id = ?;", active, self.srv.service_id)
|
if persist:
|
||||||
|
self.db.query("UPDATE services SET active = ? WHERE service_id = ?;", active, self.srv.service_id)
|
||||||
|
|
||||||
async def restart(self):
|
async def restart(self):
|
||||||
await self.disable()
|
await self.disable()
|
||||||
@ -49,12 +50,16 @@ class FirewallManager:
|
|||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
for key in list(self.service_table.keys()):
|
for key in list(self.service_table.keys()):
|
||||||
await self.remove(key)
|
try:
|
||||||
|
await self.remove(key, persist=False)
|
||||||
|
except Exception:
|
||||||
|
# Don't let one broken service block shutdown of the others
|
||||||
|
self.service_table.pop(key, None)
|
||||||
|
|
||||||
async def remove(self,srv_id):
|
async def remove(self,srv_id,persist:bool=True):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
if srv_id in self.service_table:
|
if srv_id in self.service_table:
|
||||||
await self.service_table[srv_id].disable()
|
await self.service_table[srv_id].disable(persist=persist)
|
||||||
del self.service_table[srv_id]
|
del self.service_table[srv_id]
|
||||||
|
|
||||||
async def init(self):
|
async def init(self):
|
||||||
|
|||||||
1
backend/modules/tls/__init__.py
Normal file
1
backend/modules/tls/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# TLS Decrypt Module
|
||||||
92
backend/modules/tls/manager.py
Normal file
92
backend/modules/tls/manager.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import hashlib
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
from utils.sqlite import SQLite
|
||||||
|
|
||||||
|
def ip_parse(ip: str) -> str:
|
||||||
|
try:
|
||||||
|
return str(ip_interface(ip).network)
|
||||||
|
except Exception:
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def get_tls_ports(ip: str, port: int) -> tuple[int, int]:
|
||||||
|
ip_addr = ip.split("/")[0]
|
||||||
|
key = f"{ip_addr}:{port}"
|
||||||
|
h = int.from_bytes(hashlib.sha256(key.encode('utf-8')).digest()[:4], 'big')
|
||||||
|
ssl_port = 10000 + (h % 10000)
|
||||||
|
clear_port = 20000 + (h % 10000)
|
||||||
|
return ssl_port, clear_port
|
||||||
|
|
||||||
|
TLS_SCHEMA = {
|
||||||
|
'tls_streams': {
|
||||||
|
'id': 'VARCHAR(100) PRIMARY KEY',
|
||||||
|
'name': 'VARCHAR(100) NOT NULL',
|
||||||
|
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||||
|
'port': 'INT NOT NULL',
|
||||||
|
'cert': 'TEXT NOT NULL',
|
||||||
|
'key': 'TEXT NOT NULL',
|
||||||
|
'status': 'VARCHAR(50) NOT NULL', # 'active' or 'stop'
|
||||||
|
'ssl_port': 'INT NOT NULL',
|
||||||
|
'clear_port': 'INT NOT NULL',
|
||||||
|
'UNIQUE': '(ip_int, port)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TLSManager:
|
||||||
|
def __init__(self, db_path='db/nft-tls.db'):
|
||||||
|
self.db = SQLite(db_path, schema=TLS_SCHEMA)
|
||||||
|
self.db.init()
|
||||||
|
|
||||||
|
def add_stream(self, stream_id: str, name: str, ip_int: str, port: int, cert: str, key: str, status: str = 'stop') -> None:
|
||||||
|
ip_int = ip_parse(ip_int)
|
||||||
|
ssl_port, clear_port = get_tls_ports(ip_int, port)
|
||||||
|
self.db.query(
|
||||||
|
"INSERT INTO tls_streams (id, name, ip_int, port, cert, key, status, ssl_port, clear_port) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||||
|
stream_id, name, ip_int, port, cert, key, status, ssl_port, clear_port
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_streams(self) -> list[dict]:
|
||||||
|
return self.db.query("SELECT * FROM tls_streams;")
|
||||||
|
|
||||||
|
def get_stream(self, stream_id: str) -> dict | None:
|
||||||
|
res = self.db.query("SELECT * FROM tls_streams WHERE id = ?;", stream_id)
|
||||||
|
return res[0] if res else None
|
||||||
|
|
||||||
|
def get_stream_by_ip_port(self, ip_int: str, port: int) -> dict | None:
|
||||||
|
ip_int = ip_parse(ip_int)
|
||||||
|
res = self.db.query("SELECT * FROM tls_streams WHERE ip_int = ? AND port = ?;", ip_int, port)
|
||||||
|
return res[0] if res else None
|
||||||
|
|
||||||
|
def update_status(self, stream_id: str, status: str) -> None:
|
||||||
|
self.db.query("UPDATE tls_streams SET status = ? WHERE id = ?;", status, stream_id)
|
||||||
|
|
||||||
|
def update_stream(self, stream_id: str, name: str | None = None, ip_int: str | None = None, port: int | None = None, cert: str | None = None, key: str | None = None) -> None:
|
||||||
|
stream = self.get_stream(stream_id)
|
||||||
|
if not stream:
|
||||||
|
return
|
||||||
|
fields: dict = {}
|
||||||
|
if name is not None:
|
||||||
|
fields["name"] = name
|
||||||
|
if cert is not None:
|
||||||
|
fields["cert"] = cert
|
||||||
|
if key is not None:
|
||||||
|
fields["key"] = key
|
||||||
|
new_ip = ip_parse(ip_int) if ip_int is not None else stream["ip_int"]
|
||||||
|
new_port = port if port is not None else stream["port"]
|
||||||
|
if ip_int is not None:
|
||||||
|
fields["ip_int"] = new_ip
|
||||||
|
if port is not None:
|
||||||
|
fields["port"] = new_port
|
||||||
|
if ip_int is not None or port is not None:
|
||||||
|
ssl_port, clear_port = get_tls_ports(new_ip, new_port)
|
||||||
|
fields["ssl_port"] = ssl_port
|
||||||
|
fields["clear_port"] = clear_port
|
||||||
|
if not fields:
|
||||||
|
return
|
||||||
|
set_clause = ", ".join(f"{k} = ?" for k in fields)
|
||||||
|
self.db.query(f"UPDATE tls_streams SET {set_clause} WHERE id = ?;", *fields.values(), stream_id)
|
||||||
|
|
||||||
|
def delete_stream(self, stream_id: str) -> None:
|
||||||
|
self.db.query("DELETE FROM tls_streams WHERE id = ?;", stream_id)
|
||||||
|
|
||||||
|
def get_active_streams(self) -> list[dict]:
|
||||||
|
return self.db.query("SELECT * FROM tls_streams WHERE status = 'active';")
|
||||||
43
backend/modules/tls/nftables.py
Normal file
43
backend/modules/tls/nftables.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from utils import NFTableManager, nftables_int_to_json, ip_family
|
||||||
|
from modules.tls.manager import TLSManager
|
||||||
|
|
||||||
|
class TLSTables(NFTableManager):
|
||||||
|
nat_chain = "tls_nat"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__([
|
||||||
|
{"add":{"chain":{ #NAT chain for DNAT
|
||||||
|
"family":"inet",
|
||||||
|
"table":self.table_name,
|
||||||
|
"name":self.nat_chain,
|
||||||
|
"type":"nat",
|
||||||
|
"hook":"prerouting",
|
||||||
|
"prio":-100,
|
||||||
|
"policy":"accept"
|
||||||
|
}}}
|
||||||
|
],[
|
||||||
|
{"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.nat_chain}}},
|
||||||
|
])
|
||||||
|
|
||||||
|
def reload(self):
|
||||||
|
self.init()
|
||||||
|
manager = TLSManager()
|
||||||
|
streams = manager.get_active_streams()
|
||||||
|
cmds = []
|
||||||
|
for srv in streams:
|
||||||
|
loopback_ip = "::1" if ip_family(srv["ip_int"]) == "ip6" else "127.0.0.1"
|
||||||
|
cmds.append({"insert":{ "rule": { # Redirect TLS traffic to local Nginx SSL proxy
|
||||||
|
"family": "inet",
|
||||||
|
"table": self.table_name,
|
||||||
|
"chain": self.nat_chain,
|
||||||
|
"expr": [
|
||||||
|
{'match': {'left': {'payload': {'protocol': ip_family(srv["ip_int"]), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv["ip_int"])}},
|
||||||
|
{'match': {'left': {'payload': {'protocol': 'tcp', 'field': 'dport'}}, 'op': '==', 'right': int(srv["port"])}},
|
||||||
|
{'match': {'left': {'meta': {'key': 'iifname'}}, 'op': '!=', 'right': 'lo'}},
|
||||||
|
{'dnat': {'family': ip_family(srv["ip_int"]), 'addr': loopback_ip, 'port': int(srv["ssl_port"])}}
|
||||||
|
]
|
||||||
|
}}})
|
||||||
|
if cmds:
|
||||||
|
self.cmd(*cmds)
|
||||||
|
|
||||||
|
tls_firewall = TLSTables()
|
||||||
83
backend/modules/tls/nginx.py
Normal file
83
backend/modules/tls/nginx.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
from modules.tls.manager import TLSManager
|
||||||
|
|
||||||
|
NGINX_CONF_PATH = "/tmp/firegex_nginx.conf"
|
||||||
|
NGINX_PID_PATH = "/tmp/firegex_nginx.pid"
|
||||||
|
NGINX_LOG_PATH = "/tmp/firegex_nginx_error.log"
|
||||||
|
|
||||||
|
def update_nginx() -> None:
|
||||||
|
manager = TLSManager()
|
||||||
|
streams = manager.get_active_streams()
|
||||||
|
|
||||||
|
if not streams:
|
||||||
|
subprocess.run(["nginx", "-c", NGINX_CONF_PATH, "-s", "stop", "-e", NGINX_LOG_PATH], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
# Clean up certificates, configuration, and pid
|
||||||
|
for pattern in ("/tmp/firegex_cert_*", "/tmp/firegex_key_*", NGINX_CONF_PATH, NGINX_PID_PATH, NGINX_LOG_PATH):
|
||||||
|
for path in (glob.glob(pattern) if "*" in pattern else [pattern]):
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
servers_config = []
|
||||||
|
for srv in streams:
|
||||||
|
srv_id = srv["id"]
|
||||||
|
cert_path = f"/tmp/firegex_cert_{srv_id}.crt"
|
||||||
|
key_path = f"/tmp/firegex_key_{srv_id}.key"
|
||||||
|
|
||||||
|
with open(cert_path, "w") as f:
|
||||||
|
f.write(srv.get("cert") or "")
|
||||||
|
with open(key_path, "w") as f:
|
||||||
|
f.write(srv.get("key") or "")
|
||||||
|
|
||||||
|
ssl_port = srv["ssl_port"]
|
||||||
|
clear_port = srv["clear_port"]
|
||||||
|
|
||||||
|
ip_addr = srv["ip_int"].split("/")[0]
|
||||||
|
is_ipv6 = ":" in ip_addr
|
||||||
|
loopback_ip = "[::1]" if is_ipv6 else "127.0.0.1"
|
||||||
|
dest_ip = f"[{ip_addr}]" if is_ipv6 else ip_addr
|
||||||
|
|
||||||
|
servers_config.append(
|
||||||
|
f" server {{\n"
|
||||||
|
f" listen {loopback_ip}:{ssl_port} ssl;\n"
|
||||||
|
f" proxy_pass {loopback_ip}:{clear_port};\n"
|
||||||
|
f" ssl_certificate {cert_path};\n"
|
||||||
|
f" ssl_certificate_key {key_path};\n"
|
||||||
|
f" ssl_protocols TLSv1.2 TLSv1.3;\n"
|
||||||
|
f" }}\n"
|
||||||
|
f" server {{\n"
|
||||||
|
f" listen {loopback_ip}:{clear_port};\n"
|
||||||
|
f" proxy_pass {dest_ip}:{srv['port']};\n"
|
||||||
|
f" proxy_ssl on;\n"
|
||||||
|
f" proxy_ssl_verify off;\n"
|
||||||
|
f" proxy_ssl_protocols TLSv1.2 TLSv1.3;\n"
|
||||||
|
f" }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
config_content = ""
|
||||||
|
if os.path.isdir("/usr/share/nginx/modules"):
|
||||||
|
config_content += "include /usr/share/nginx/modules/*.conf;\n"
|
||||||
|
|
||||||
|
config_content += (
|
||||||
|
f"pid {NGINX_PID_PATH};\n"
|
||||||
|
f"error_log {NGINX_LOG_PATH} info;\n\n"
|
||||||
|
f"events {{\n"
|
||||||
|
f" worker_connections 1024;\n"
|
||||||
|
f"}}\n\n"
|
||||||
|
f"stream {{\n"
|
||||||
|
+ "\n".join(servers_config) + "\n"
|
||||||
|
f"}}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(NGINX_CONF_PATH, "w") as f:
|
||||||
|
f.write(config_content)
|
||||||
|
|
||||||
|
# Try to reload. If Nginx is not running, start it.
|
||||||
|
res = subprocess.run(["nginx", "-c", NGINX_CONF_PATH, "-s", "reload", "-e", NGINX_LOG_PATH], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
if res.returncode != 0:
|
||||||
|
subprocess.run(["nginx", "-c", NGINX_CONF_PATH, "-e", NGINX_LOG_PATH], check=True)
|
||||||
22
backend/modules/tls/service.py
Normal file
22
backend/modules/tls/service.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from modules.tls.manager import TLSManager
|
||||||
|
from modules.tls.nginx import update_nginx
|
||||||
|
from modules.tls.nftables import tls_firewall
|
||||||
|
from utils import socketio_emit
|
||||||
|
|
||||||
|
async def reload_tls():
|
||||||
|
update_nginx()
|
||||||
|
tls_firewall.reload()
|
||||||
|
await socketio_emit(["tls_streams"])
|
||||||
|
|
||||||
|
async def activate_stream(stream_id: str | None) -> bool:
|
||||||
|
"""Activate a TLS stream if it exists and isn't already active. Returns False if the stream doesn't exist."""
|
||||||
|
if not stream_id:
|
||||||
|
return False
|
||||||
|
manager = TLSManager()
|
||||||
|
stream = manager.get_stream(stream_id)
|
||||||
|
if not stream:
|
||||||
|
return False
|
||||||
|
if stream["status"] != "active":
|
||||||
|
manager.update_status(stream_id, "active")
|
||||||
|
await reload_tls()
|
||||||
|
return True
|
||||||
@ -50,12 +50,9 @@ async def startup():
|
|||||||
await firewall.init()
|
await firewall.init()
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
keep_rules = firewall.keep_rules
|
if not firewall.keep_rules:
|
||||||
db.backup()
|
|
||||||
if not keep_rules:
|
|
||||||
await firewall.close()
|
await firewall.close()
|
||||||
db.disconnect()
|
db.disconnect()
|
||||||
db.restore()
|
|
||||||
|
|
||||||
async def refresh_frontend(additional:list[str]=[]):
|
async def refresh_frontend(additional:list[str]=[]):
|
||||||
await socketio_emit(["firewall"]+additional)
|
await socketio_emit(["firewall"]+additional)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from modules.nfproxy.nftables import FiregexTables
|
|||||||
from modules.nfproxy.firewall import STATUS, FirewallManager
|
from modules.nfproxy.firewall import STATUS, FirewallManager
|
||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
||||||
|
from modules.tls.service import activate_stream
|
||||||
from utils.models import ResetRequest, StatusMessageModel
|
from utils.models import ResetRequest, StatusMessageModel
|
||||||
import os
|
import os
|
||||||
from firegex.nfproxy.internals import get_filter_names
|
from firegex.nfproxy.internals import get_filter_names
|
||||||
@ -19,22 +20,27 @@ import utils
|
|||||||
class ServiceModel(BaseModel):
|
class ServiceModel(BaseModel):
|
||||||
service_id: str
|
service_id: str
|
||||||
status: str
|
status: str
|
||||||
port: PortType
|
port: PortType|None = None
|
||||||
name: str
|
name: str
|
||||||
proto: str
|
proto: str
|
||||||
ip_int: str
|
ip_int: str|None = None
|
||||||
n_filters: int
|
n_filters: int
|
||||||
edited_packets: int
|
edited_packets: int
|
||||||
blocked_packets: int
|
blocked_packets: int
|
||||||
fail_open: bool
|
fail_open: bool
|
||||||
|
target_type: str
|
||||||
|
tls_stream_id: str|None = None
|
||||||
|
|
||||||
class RenameForm(BaseModel):
|
class RenameForm(BaseModel):
|
||||||
name:str
|
name:str
|
||||||
|
|
||||||
class SettingsForm(BaseModel):
|
class SettingsForm(BaseModel):
|
||||||
port: PortType|None = None
|
port: PortType|None = None
|
||||||
|
proto: str|None = None
|
||||||
ip_int: str|None = None
|
ip_int: str|None = None
|
||||||
fail_open: bool|None = None
|
fail_open: bool|None = None
|
||||||
|
target_type: str|None = None
|
||||||
|
tls_stream_id: str|None = None
|
||||||
|
|
||||||
class PyFilterModel(BaseModel):
|
class PyFilterModel(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
@ -45,10 +51,12 @@ class PyFilterModel(BaseModel):
|
|||||||
|
|
||||||
class ServiceAddForm(BaseModel):
|
class ServiceAddForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
port: PortType
|
port: PortType|None = None
|
||||||
proto: str
|
proto: str
|
||||||
ip_int: str
|
ip_int: str|None = None
|
||||||
fail_open: bool = True
|
fail_open: bool = True
|
||||||
|
target_type: str = "flow"
|
||||||
|
tls_stream_id: str|None = None
|
||||||
|
|
||||||
class ServiceAddResponse(BaseModel):
|
class ServiceAddResponse(BaseModel):
|
||||||
status:str
|
status:str
|
||||||
@ -63,11 +71,13 @@ db = SQLite('db/nft-pyfilters.db', {
|
|||||||
'services': {
|
'services': {
|
||||||
'service_id': 'VARCHAR(100) PRIMARY KEY',
|
'service_id': 'VARCHAR(100) PRIMARY KEY',
|
||||||
'status': 'VARCHAR(100) NOT NULL',
|
'status': 'VARCHAR(100) NOT NULL',
|
||||||
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
'target_type': 'VARCHAR(10) NOT NULL CHECK(target_type IN ("flow", "tls")) DEFAULT "flow"',
|
||||||
|
'tls_stream_id': 'VARCHAR(100)',
|
||||||
|
'port': 'INT CHECK(port > 0 and port < 65536)',
|
||||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
||||||
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))',
|
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))',
|
||||||
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
||||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
'ip_int': 'VARCHAR(100)',
|
||||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
||||||
},
|
},
|
||||||
'pyfilter': {
|
'pyfilter': {
|
||||||
@ -115,10 +125,8 @@ async def startup():
|
|||||||
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
db.backup()
|
|
||||||
await firewall.close()
|
await firewall.close()
|
||||||
db.disconnect()
|
db.disconnect()
|
||||||
db.restore()
|
|
||||||
|
|
||||||
def gen_service_id():
|
def gen_service_id():
|
||||||
while True:
|
while True:
|
||||||
@ -138,7 +146,7 @@ firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exc
|
|||||||
@app.get('/services', response_model=list[ServiceModel])
|
@app.get('/services', response_model=list[ServiceModel])
|
||||||
async def get_service_list():
|
async def get_service_list():
|
||||||
"""Get the list of existent firegex services"""
|
"""Get the list of existent firegex services"""
|
||||||
return db.query("""
|
res = db.query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.service_id service_id,
|
s.service_id service_id,
|
||||||
s.status status,
|
s.status status,
|
||||||
@ -147,12 +155,15 @@ async def get_service_list():
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.target_type target_type,
|
||||||
|
s.tls_stream_id tls_stream_id,
|
||||||
COUNT(f.name) n_filters,
|
COUNT(f.name) n_filters,
|
||||||
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
||||||
COALESCE(SUM(f.edited_packets),0) edited_packets
|
COALESCE(SUM(f.edited_packets),0) edited_packets
|
||||||
FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id
|
FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id
|
||||||
GROUP BY s.service_id;
|
GROUP BY s.service_id;
|
||||||
""")
|
""")
|
||||||
|
return res
|
||||||
|
|
||||||
@app.get('/services/{service_id}', response_model=ServiceModel)
|
@app.get('/services/{service_id}', response_model=ServiceModel)
|
||||||
async def get_service_by_id(service_id: str):
|
async def get_service_by_id(service_id: str):
|
||||||
@ -166,6 +177,8 @@ async def get_service_by_id(service_id: str):
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.target_type target_type,
|
||||||
|
s.tls_stream_id tls_stream_id,
|
||||||
COUNT(f.name) n_filters,
|
COUNT(f.name) n_filters,
|
||||||
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
COALESCE(SUM(f.blocked_packets),0) blocked_packets,
|
||||||
COALESCE(SUM(f.edited_packets),0) edited_packets
|
COALESCE(SUM(f.edited_packets),0) edited_packets
|
||||||
@ -186,6 +199,10 @@ async def service_stop(service_id: str):
|
|||||||
@app.post('/services/{service_id}/start', response_model=StatusMessageModel)
|
@app.post('/services/{service_id}/start', response_model=StatusMessageModel)
|
||||||
async def service_start(service_id: str):
|
async def service_start(service_id: str):
|
||||||
"""Request the start of a specific service"""
|
"""Request the start of a specific service"""
|
||||||
|
srv = db.query("SELECT target_type, tls_stream_id FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if srv and srv[0]["target_type"] == "tls":
|
||||||
|
if not await activate_stream(srv[0]["tls_stream_id"]):
|
||||||
|
raise HTTPException(status_code=400, detail="Linked TLS stream not found")
|
||||||
await firewall.get(service_id).next(STATUS.ACTIVE)
|
await firewall.get(service_id).next(STATUS.ACTIVE)
|
||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
@ -218,6 +235,11 @@ async def service_rename(service_id: str, form: RenameForm):
|
|||||||
async def service_settings(service_id: str, form: SettingsForm):
|
async def service_settings(service_id: str, form: SettingsForm):
|
||||||
"""Request to change the settings of a specific service (will cause a restart)"""
|
"""Request to change the settings of a specific service (will cause a restart)"""
|
||||||
|
|
||||||
|
srv_check = db.query("SELECT ip_int, port, target_type, tls_stream_id FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if len(srv_check) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
old_srv = srv_check[0]
|
||||||
|
|
||||||
if form.port is not None and (form.port < 1 or form.port > 65535):
|
if form.port is not None and (form.port < 1 or form.port > 65535):
|
||||||
raise HTTPException(status_code=400, detail="Invalid port")
|
raise HTTPException(status_code=400, detail="Invalid port")
|
||||||
|
|
||||||
@ -226,6 +248,17 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
form.ip_int = ip_parse(form.ip_int)
|
form.ip_int = ip_parse(form.ip_int)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
|
||||||
|
new_ip = form.ip_int if form.ip_int is not None else old_srv["ip_int"]
|
||||||
|
new_port = form.port if form.port is not None else old_srv["port"]
|
||||||
|
|
||||||
|
new_target_type = form.target_type if form.target_type is not None else old_srv["target_type"]
|
||||||
|
new_tls_stream_id = form.tls_stream_id if form.tls_stream_id is not None else old_srv["tls_stream_id"]
|
||||||
|
|
||||||
|
if new_target_type == "tls" and not new_tls_stream_id:
|
||||||
|
raise HTTPException(status_code=400, detail="TLS stream ID is required when target type is tls")
|
||||||
|
if new_target_type == "flow" and (new_ip is None or new_port is None):
|
||||||
|
raise HTTPException(status_code=400, detail="IP and Port are required when target type is flow")
|
||||||
|
|
||||||
keys = []
|
keys = []
|
||||||
values = []
|
values = []
|
||||||
@ -233,10 +266,13 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
for key, value in form.model_dump(exclude_none=True).items():
|
for key, value in form.model_dump(exclude_none=True).items():
|
||||||
keys.append(key)
|
keys.append(key)
|
||||||
values.append(value)
|
values.append(value)
|
||||||
|
if key == "proto":
|
||||||
|
keys.append("l4_proto")
|
||||||
|
values.append(convert_protocol_to_l4(value))
|
||||||
|
|
||||||
if len(keys) == 0:
|
if len(keys) == 0:
|
||||||
raise HTTPException(status_code=400, detail="No settings to change provided")
|
raise HTTPException(status_code=400, detail="No settings to change provided")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
@ -244,12 +280,15 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
|
|
||||||
old_status = firewall.get(service_id).status
|
old_status = firewall.get(service_id).status
|
||||||
await firewall.remove(service_id)
|
await firewall.remove(service_id)
|
||||||
|
if old_status == STATUS.ACTIVE and new_target_type == "tls":
|
||||||
|
await activate_stream(new_tls_stream_id)
|
||||||
await firewall.reload()
|
await firewall.reload()
|
||||||
await firewall.get(service_id).next(old_status)
|
await firewall.get(service_id).next(old_status)
|
||||||
|
|
||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
|
||||||
|
|
||||||
@app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel])
|
@app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel])
|
||||||
async def get_service_pyfilter_list(service_id: str):
|
async def get_service_pyfilter_list(service_id: str):
|
||||||
"""Get the list of the pyfilters of a service"""
|
"""Get the list of the pyfilters of a service"""
|
||||||
@ -296,17 +335,25 @@ async def pyfilter_disable(service_id: str, filter_name: str):
|
|||||||
@app.post('/services', response_model=ServiceAddResponse)
|
@app.post('/services', response_model=ServiceAddResponse)
|
||||||
async def add_new_service(form: ServiceAddForm):
|
async def add_new_service(form: ServiceAddForm):
|
||||||
"""Add a new service"""
|
"""Add a new service"""
|
||||||
try:
|
if form.target_type == "flow":
|
||||||
form.ip_int = ip_parse(form.ip_int)
|
if form.ip_int is None or form.port is None:
|
||||||
except ValueError:
|
raise HTTPException(status_code=400, detail="IP and Port are required when target type is flow")
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
try:
|
||||||
|
form.ip_int = ip_parse(form.ip_int)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
elif form.target_type == "tls":
|
||||||
|
if not form.tls_stream_id:
|
||||||
|
raise HTTPException(status_code=400, detail="TLS stream ID is required when target type is tls")
|
||||||
|
|
||||||
if form.proto not in ["tcp", "http"]:
|
if form.proto not in ["tcp", "http"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
|
|
||||||
srv_id = None
|
srv_id = None
|
||||||
try:
|
try:
|
||||||
srv_id = gen_service_id()
|
srv_id = gen_service_id()
|
||||||
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open, l4_proto) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
db.query("INSERT INTO services (service_id, name, port, status, proto, ip_int, fail_open, l4_proto, target_type, tls_stream_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open, convert_protocol_to_l4(form.proto))
|
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open, convert_protocol_to_l4(form.proto), form.target_type, form.tls_stream_id)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
raise HTTPException(status_code=400, detail="This type of service already exists")
|
raise HTTPException(status_code=400, detail="This type of service already exists")
|
||||||
await firewall.reload()
|
await firewall.reload()
|
||||||
|
|||||||
@ -8,19 +8,22 @@ from modules.nfregex.nftables import FiregexTables
|
|||||||
from modules.nfregex.firewall import STATUS, FirewallManager
|
from modules.nfregex.firewall import STATUS, FirewallManager
|
||||||
from utils.sqlite import SQLite
|
from utils.sqlite import SQLite
|
||||||
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
from utils import ip_parse, refactor_name, socketio_emit, PortType
|
||||||
|
from modules.tls.service import activate_stream
|
||||||
from utils.models import ResetRequest, StatusMessageModel
|
from utils.models import ResetRequest, StatusMessageModel
|
||||||
from modules.nfregex.firegex import test_regex_validity
|
from modules.nfregex.firegex import test_regex_validity
|
||||||
|
|
||||||
class ServiceModel(BaseModel):
|
class ServiceModel(BaseModel):
|
||||||
status: str
|
status: str
|
||||||
service_id: str
|
service_id: str
|
||||||
port: PortType
|
port: PortType|None = None
|
||||||
name: str
|
name: str
|
||||||
proto: str
|
proto: str
|
||||||
ip_int: str
|
ip_int: str|None = None
|
||||||
n_regex: int
|
n_regex: int
|
||||||
n_packets: int
|
n_packets: int
|
||||||
fail_open: bool
|
fail_open: bool
|
||||||
|
target_type: str
|
||||||
|
tls_stream_id: str|None = None
|
||||||
|
|
||||||
class RenameForm(BaseModel):
|
class RenameForm(BaseModel):
|
||||||
name:str
|
name:str
|
||||||
@ -30,6 +33,8 @@ class SettingsForm(BaseModel):
|
|||||||
proto: str|None = None
|
proto: str|None = None
|
||||||
ip_int: str|None = None
|
ip_int: str|None = None
|
||||||
fail_open: bool|None = None
|
fail_open: bool|None = None
|
||||||
|
target_type: str|None = None
|
||||||
|
tls_stream_id: str|None = None
|
||||||
|
|
||||||
class RegexModel(BaseModel):
|
class RegexModel(BaseModel):
|
||||||
regex:str
|
regex:str
|
||||||
@ -49,10 +54,12 @@ class RegexAddForm(BaseModel):
|
|||||||
|
|
||||||
class ServiceAddForm(BaseModel):
|
class ServiceAddForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
port: PortType
|
port: PortType|None = None
|
||||||
proto: str
|
proto: str
|
||||||
ip_int: str
|
ip_int: str|None = None
|
||||||
fail_open: bool = False
|
fail_open: bool = False
|
||||||
|
target_type: str = "flow"
|
||||||
|
tls_stream_id: str|None = None
|
||||||
|
|
||||||
class ServiceAddResponse(BaseModel):
|
class ServiceAddResponse(BaseModel):
|
||||||
status:str
|
status:str
|
||||||
@ -64,11 +71,13 @@ db = SQLite('db/nft-regex.db', {
|
|||||||
'services': {
|
'services': {
|
||||||
'service_id': 'VARCHAR(100) PRIMARY KEY',
|
'service_id': 'VARCHAR(100) PRIMARY KEY',
|
||||||
'status': 'VARCHAR(100) NOT NULL',
|
'status': 'VARCHAR(100) NOT NULL',
|
||||||
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
'target_type': 'VARCHAR(10) NOT NULL CHECK(target_type IN ("flow", "tls")) DEFAULT "flow"',
|
||||||
|
'tls_stream_id': 'VARCHAR(100)',
|
||||||
|
'port': 'INT CHECK(port > 0 and port < 65536)',
|
||||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
||||||
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))',
|
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))',
|
||||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
'ip_int': 'VARCHAR(100)',
|
||||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1'
|
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
||||||
},
|
},
|
||||||
'regexes': {
|
'regexes': {
|
||||||
'regex': 'TEXT NOT NULL',
|
'regex': 'TEXT NOT NULL',
|
||||||
@ -113,10 +122,8 @@ async def startup():
|
|||||||
print("WARNING cannot start firewall:", e)
|
print("WARNING cannot start firewall:", e)
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
db.backup()
|
|
||||||
await firewall.close()
|
await firewall.close()
|
||||||
db.disconnect()
|
db.disconnect()
|
||||||
db.restore()
|
|
||||||
|
|
||||||
def gen_service_id():
|
def gen_service_id():
|
||||||
while True:
|
while True:
|
||||||
@ -130,7 +137,7 @@ firewall = FirewallManager(db)
|
|||||||
@app.get('/services', response_model=list[ServiceModel])
|
@app.get('/services', response_model=list[ServiceModel])
|
||||||
async def get_service_list():
|
async def get_service_list():
|
||||||
"""Get the list of existent firegex services"""
|
"""Get the list of existent firegex services"""
|
||||||
return db.query("""
|
res = db.query("""
|
||||||
SELECT
|
SELECT
|
||||||
s.service_id service_id,
|
s.service_id service_id,
|
||||||
s.status status,
|
s.status status,
|
||||||
@ -139,11 +146,14 @@ async def get_service_list():
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.target_type target_type,
|
||||||
|
s.tls_stream_id tls_stream_id,
|
||||||
COUNT(r.regex_id) n_regex,
|
COUNT(r.regex_id) n_regex,
|
||||||
COALESCE(SUM(r.blocked_packets),0) n_packets
|
COALESCE(SUM(r.blocked_packets),0) n_packets
|
||||||
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
||||||
GROUP BY s.service_id;
|
GROUP BY s.service_id;
|
||||||
""")
|
""")
|
||||||
|
return res
|
||||||
|
|
||||||
@app.get('/services/{service_id}', response_model=ServiceModel)
|
@app.get('/services/{service_id}', response_model=ServiceModel)
|
||||||
async def get_service_by_id(service_id: str):
|
async def get_service_by_id(service_id: str):
|
||||||
@ -157,6 +167,8 @@ async def get_service_by_id(service_id: str):
|
|||||||
s.proto proto,
|
s.proto proto,
|
||||||
s.ip_int ip_int,
|
s.ip_int ip_int,
|
||||||
s.fail_open fail_open,
|
s.fail_open fail_open,
|
||||||
|
s.target_type target_type,
|
||||||
|
s.tls_stream_id tls_stream_id,
|
||||||
COUNT(r.regex_id) n_regex,
|
COUNT(r.regex_id) n_regex,
|
||||||
COALESCE(SUM(r.blocked_packets),0) n_packets
|
COALESCE(SUM(r.blocked_packets),0) n_packets
|
||||||
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
|
||||||
@ -176,6 +188,10 @@ async def service_stop(service_id: str):
|
|||||||
@app.post('/services/{service_id}/start', response_model=StatusMessageModel)
|
@app.post('/services/{service_id}/start', response_model=StatusMessageModel)
|
||||||
async def service_start(service_id: str):
|
async def service_start(service_id: str):
|
||||||
"""Request the start of a specific service"""
|
"""Request the start of a specific service"""
|
||||||
|
srv = db.query("SELECT target_type, tls_stream_id FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if srv and srv[0]["target_type"] == "tls":
|
||||||
|
if not await activate_stream(srv[0]["tls_stream_id"]):
|
||||||
|
raise HTTPException(status_code=400, detail="Linked TLS stream not found")
|
||||||
await firewall.get(service_id).next(STATUS.ACTIVE)
|
await firewall.get(service_id).next(STATUS.ACTIVE)
|
||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
@ -206,9 +222,14 @@ async def service_rename(service_id: str, form: RenameForm):
|
|||||||
async def service_settings(service_id: str, form: SettingsForm):
|
async def service_settings(service_id: str, form: SettingsForm):
|
||||||
"""Request to change the settings of a specific service (will cause a restart)"""
|
"""Request to change the settings of a specific service (will cause a restart)"""
|
||||||
|
|
||||||
|
srv_check = db.query("SELECT proto, ip_int, port, target_type, tls_stream_id FROM services WHERE service_id = ?;", service_id)
|
||||||
|
if len(srv_check) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
old_srv = srv_check[0]
|
||||||
|
|
||||||
if form.proto is not None and form.proto not in ["tcp", "udp"]:
|
if form.proto is not None and form.proto not in ["tcp", "udp"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
|
|
||||||
if form.port is not None and (form.port < 1 or form.port > 65535):
|
if form.port is not None and (form.port < 1 or form.port > 65535):
|
||||||
raise HTTPException(status_code=400, detail="Invalid port")
|
raise HTTPException(status_code=400, detail="Invalid port")
|
||||||
|
|
||||||
@ -217,6 +238,22 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
form.ip_int = ip_parse(form.ip_int)
|
form.ip_int = ip_parse(form.ip_int)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
|
||||||
|
new_ip = form.ip_int if form.ip_int is not None else old_srv["ip_int"]
|
||||||
|
new_port = form.port if form.port is not None else old_srv["port"]
|
||||||
|
new_proto = form.proto if form.proto is not None else old_srv["proto"]
|
||||||
|
|
||||||
|
new_target_type = form.target_type if form.target_type is not None else old_srv["target_type"]
|
||||||
|
new_tls_stream_id = form.tls_stream_id if form.tls_stream_id is not None else old_srv["tls_stream_id"]
|
||||||
|
|
||||||
|
if new_target_type == "tls":
|
||||||
|
if new_proto != "tcp":
|
||||||
|
raise HTTPException(status_code=400, detail="TLS is only supported for TCP protocol")
|
||||||
|
if not new_tls_stream_id:
|
||||||
|
raise HTTPException(status_code=400, detail="TLS stream ID is required when target type is tls")
|
||||||
|
|
||||||
|
if new_target_type == "flow" and (new_ip is None or new_port is None):
|
||||||
|
raise HTTPException(status_code=400, detail="IP and Port are required when target type is flow")
|
||||||
|
|
||||||
keys = []
|
keys = []
|
||||||
values = []
|
values = []
|
||||||
@ -227,7 +264,7 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
|
|
||||||
if len(keys) == 0:
|
if len(keys) == 0:
|
||||||
raise HTTPException(status_code=400, detail="No settings to change provided")
|
raise HTTPException(status_code=400, detail="No settings to change provided")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
@ -235,9 +272,11 @@ async def service_settings(service_id: str, form: SettingsForm):
|
|||||||
|
|
||||||
old_status = firewall.get(service_id).status
|
old_status = firewall.get(service_id).status
|
||||||
await firewall.remove(service_id)
|
await firewall.remove(service_id)
|
||||||
|
if old_status == STATUS.ACTIVE and new_target_type == "tls":
|
||||||
|
await activate_stream(new_tls_stream_id)
|
||||||
await firewall.reload()
|
await firewall.reload()
|
||||||
await firewall.get(service_id).next(old_status)
|
await firewall.get(service_id).next(old_status)
|
||||||
|
|
||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
|
||||||
@ -316,23 +355,34 @@ async def add_new_regex(form: RegexAddForm):
|
|||||||
@app.post('/services', response_model=ServiceAddResponse)
|
@app.post('/services', response_model=ServiceAddResponse)
|
||||||
async def add_new_service(form: ServiceAddForm):
|
async def add_new_service(form: ServiceAddForm):
|
||||||
"""Add a new service"""
|
"""Add a new service"""
|
||||||
try:
|
if form.target_type == "flow":
|
||||||
form.ip_int = ip_parse(form.ip_int)
|
if form.ip_int is None or form.port is None:
|
||||||
except ValueError:
|
raise HTTPException(status_code=400, detail="IP and Port are required when target type is flow")
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
try:
|
||||||
|
form.ip_int = ip_parse(form.ip_int)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
elif form.target_type == "tls":
|
||||||
|
if not form.tls_stream_id:
|
||||||
|
raise HTTPException(status_code=400, detail="TLS stream ID is required when target type is tls")
|
||||||
|
|
||||||
if form.proto not in ["tcp", "udp"]:
|
if form.proto not in ["tcp", "udp"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
|
if form.target_type == "tls" and form.proto != "tcp":
|
||||||
|
raise HTTPException(status_code=400, detail="TLS is only supported for TCP protocol")
|
||||||
|
|
||||||
srv_id = None
|
srv_id = None
|
||||||
try:
|
try:
|
||||||
srv_id = gen_service_id()
|
srv_id = gen_service_id()
|
||||||
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open, target_type, tls_stream_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open)
|
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open, form.target_type, form.tls_stream_id)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
raise HTTPException(status_code=400, detail="This type of service already exists")
|
raise HTTPException(status_code=400, detail="This type of service already exists")
|
||||||
await firewall.reload()
|
await firewall.reload()
|
||||||
await refresh_frontend()
|
await refresh_frontend()
|
||||||
return {'status': 'ok', 'service_id': srv_id}
|
return {'status': 'ok', 'service_id': srv_id}
|
||||||
|
|
||||||
|
|
||||||
@app.get('/metrics', response_class = PlainTextResponse)
|
@app.get('/metrics', response_class = PlainTextResponse)
|
||||||
async def metrics():
|
async def metrics():
|
||||||
"""Aggregate metrics"""
|
"""Aggregate metrics"""
|
||||||
@ -355,3 +405,52 @@ async def metrics():
|
|||||||
metrics.append(f'firegex_blocked_packets{{{props}}} {stat["blocked_packets"]}')
|
metrics.append(f'firegex_blocked_packets{{{props}}} {stat["blocked_packets"]}')
|
||||||
metrics.append(f'firegex_active{{{props}}} {int(stat["active"] and stat["status"] == "active")}')
|
metrics.append(f'firegex_active{{{props}}} {int(stat["active"] and stat["status"] == "active")}')
|
||||||
return "\n".join(metrics)
|
return "\n".join(metrics)
|
||||||
|
|
||||||
|
@app.get('/services/{service_id}/export')
|
||||||
|
async def export_service_regexes(service_id: str):
|
||||||
|
"""Export all regexes for a specific service as JSON"""
|
||||||
|
if len(db.query('SELECT 1 FROM services WHERE service_id = ?;', service_id)) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
regexes = db.query("SELECT regex, mode, is_case_sensitive, active FROM regexes WHERE service_id = ?;", service_id)
|
||||||
|
return {"regexes": regexes}
|
||||||
|
|
||||||
|
@app.post('/services/{service_id}/import', response_model=StatusMessageModel)
|
||||||
|
async def import_service_regexes(service_id: str, data: dict):
|
||||||
|
"""Import regexes for a specific service from JSON"""
|
||||||
|
if len(db.query('SELECT 1 FROM services WHERE service_id = ?;', service_id)) == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
regexes = data.get("regexes", [])
|
||||||
|
if not isinstance(regexes, list):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid format")
|
||||||
|
|
||||||
|
for r in regexes:
|
||||||
|
regex = r.get("regex")
|
||||||
|
mode = r.get("mode")
|
||||||
|
is_case_sensitive = r.get("is_case_sensitive", False)
|
||||||
|
active = r.get("active", True)
|
||||||
|
|
||||||
|
if not regex or not mode:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.query('INSERT INTO regexes (regex, mode, service_id, is_case_sensitive, active) VALUES (?, ?, ?, ?, ?);',
|
||||||
|
regex, mode, service_id, int(is_case_sensitive), int(active))
|
||||||
|
|
||||||
|
# Since we inserted it directly, we need to notify the firewall to update its rules if it's active
|
||||||
|
if active and firewall.get(service_id).status == STATUS.ACTIVE:
|
||||||
|
# Easiest way to sync is to restart the service rule engine
|
||||||
|
await firewall.get(service_id).next(STATUS.ACTIVE)
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
# Skip duplicates
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Force reload of all rules for this service
|
||||||
|
old_status = firewall.get(service_id).status
|
||||||
|
await firewall.remove(service_id)
|
||||||
|
await firewall.reload()
|
||||||
|
await firewall.get(service_id).next(old_status)
|
||||||
|
|
||||||
|
await refresh_frontend()
|
||||||
|
return {'status': 'ok'}
|
||||||
|
|||||||
@ -67,10 +67,8 @@ async def startup():
|
|||||||
await firewall.init()
|
await firewall.init()
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
db.backup()
|
|
||||||
await firewall.close()
|
await firewall.close()
|
||||||
db.disconnect()
|
db.disconnect()
|
||||||
db.restore()
|
|
||||||
|
|
||||||
async def refresh_frontend(additional:list[str]=[]):
|
async def refresh_frontend(additional:list[str]=[]):
|
||||||
await socketio_emit(["porthijack"]+additional)
|
await socketio_emit(["porthijack"]+additional)
|
||||||
|
|||||||
177
backend/routers/tls.py
Normal file
177
backend/routers/tls.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
import subprocess
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from modules.tls.manager import TLSManager, ip_parse
|
||||||
|
from modules.tls.service import reload_tls
|
||||||
|
from utils.models import StatusMessageModel
|
||||||
|
import secrets
|
||||||
|
import routers.nfproxy as nfproxy_router
|
||||||
|
import routers.nfregex as nfregex_router
|
||||||
|
|
||||||
|
def gen_service_id():
|
||||||
|
while True:
|
||||||
|
res = secrets.token_hex(8)
|
||||||
|
if not manager.get_stream(res):
|
||||||
|
break
|
||||||
|
return res
|
||||||
|
|
||||||
|
class TLSStreamAddForm(BaseModel):
|
||||||
|
name: str
|
||||||
|
ip_int: str
|
||||||
|
port: int
|
||||||
|
cert: str
|
||||||
|
key: str
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
class TLSStreamEditForm(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
ip_int: str | None = None
|
||||||
|
port: int | None = None
|
||||||
|
cert: str | None = None
|
||||||
|
key: str | None = None
|
||||||
|
|
||||||
|
class TLSStreamResponse(BaseModel):
|
||||||
|
status: str
|
||||||
|
stream_id: str | None = None
|
||||||
|
|
||||||
|
app = APIRouter()
|
||||||
|
manager = TLSManager()
|
||||||
|
|
||||||
|
# (module, human name) pairs of the routers that can depend on a TLS stream
|
||||||
|
DEPENDENT_ROUTERS = (("nfproxy", nfproxy_router), ("nfregex", nfregex_router))
|
||||||
|
|
||||||
|
def get_dependent_services(stream_id: str) -> list[tuple[str, "object", str, str]]:
|
||||||
|
"""Returns (module_name, router_module, service_id, status) for every nfproxy/nfregex service linked to a TLS stream."""
|
||||||
|
deps = []
|
||||||
|
for module_name, router_mod in DEPENDENT_ROUTERS:
|
||||||
|
rows = router_mod.db.query(
|
||||||
|
"SELECT service_id, status FROM services WHERE target_type = 'tls' AND tls_stream_id = ?;", stream_id
|
||||||
|
)
|
||||||
|
for row in rows:
|
||||||
|
deps.append((module_name, router_mod, row["service_id"], row["status"]))
|
||||||
|
return deps
|
||||||
|
|
||||||
|
async def sync_dependent_services_address(stream_id: str, new_ip: str, new_port: int):
|
||||||
|
"""After a stream's ip/port changed, mirror it onto dependent services and rebind their live nft rules."""
|
||||||
|
touched_modules = set()
|
||||||
|
for module_name, router_mod, srv_id, status in get_dependent_services(stream_id):
|
||||||
|
router_mod.db.query("UPDATE services SET ip_int = ?, port = ? WHERE service_id = ?;", new_ip, new_port, srv_id)
|
||||||
|
await router_mod.firewall.remove(srv_id)
|
||||||
|
await router_mod.firewall.reload()
|
||||||
|
await router_mod.firewall.get(srv_id).next(status)
|
||||||
|
touched_modules.add(module_name)
|
||||||
|
for module_name, router_mod in DEPENDENT_ROUTERS:
|
||||||
|
if module_name in touched_modules:
|
||||||
|
await router_mod.refresh_frontend()
|
||||||
|
|
||||||
|
async def cascade_stop_dependents(stream_id: str):
|
||||||
|
"""When a TLS stream is stopped, stop every active service that depends on it."""
|
||||||
|
touched_modules = set()
|
||||||
|
for module_name, router_mod, srv_id, status in get_dependent_services(stream_id):
|
||||||
|
if status == "active":
|
||||||
|
await router_mod.firewall.get(srv_id).next("stop")
|
||||||
|
touched_modules.add(module_name)
|
||||||
|
for module_name, router_mod in DEPENDENT_ROUTERS:
|
||||||
|
if module_name in touched_modules:
|
||||||
|
await router_mod.refresh_frontend()
|
||||||
|
|
||||||
|
async def startup():
|
||||||
|
await reload_tls()
|
||||||
|
|
||||||
|
async def shutdown():
|
||||||
|
subprocess.run(["nginx", "-c", "/tmp/firegex_nginx.conf", "-s", "stop", "-e", "/tmp/firegex_nginx_error.log"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
async def reset(form):
|
||||||
|
if form.delete:
|
||||||
|
manager.db.delete()
|
||||||
|
manager.db.init()
|
||||||
|
await reload_tls()
|
||||||
|
|
||||||
|
@app.get('/streams')
|
||||||
|
async def get_streams():
|
||||||
|
return manager.get_streams()
|
||||||
|
|
||||||
|
@app.post('/streams', response_model=TLSStreamResponse)
|
||||||
|
async def add_stream(form: TLSStreamAddForm):
|
||||||
|
try:
|
||||||
|
ip_int = ip_parse(form.ip_int)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
|
||||||
|
if manager.get_stream_by_ip_port(ip_int, form.port):
|
||||||
|
raise HTTPException(status_code=400, detail="A TLS stream for this IP and Port already exists")
|
||||||
|
|
||||||
|
stream_id = gen_service_id()
|
||||||
|
try:
|
||||||
|
manager.add_stream(stream_id, form.name, ip_int, form.port, form.cert, form.key, status="active" if form.enabled else "stop")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
await reload_tls()
|
||||||
|
return {"status": "ok", "stream_id": stream_id}
|
||||||
|
|
||||||
|
@app.put('/streams/{stream_id}', response_model=StatusMessageModel)
|
||||||
|
async def edit_stream(stream_id: str, form: TLSStreamEditForm):
|
||||||
|
stream = manager.get_stream(stream_id)
|
||||||
|
if not stream:
|
||||||
|
raise HTTPException(status_code=404, detail="TLS stream not found")
|
||||||
|
|
||||||
|
new_ip_int = None
|
||||||
|
if form.ip_int is not None:
|
||||||
|
try:
|
||||||
|
new_ip_int = ip_parse(form.ip_int)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid address")
|
||||||
|
|
||||||
|
if form.port is not None and (form.port < 1 or form.port > 65535):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid port")
|
||||||
|
|
||||||
|
check_ip = new_ip_int if new_ip_int is not None else stream["ip_int"]
|
||||||
|
check_port = form.port if form.port is not None else stream["port"]
|
||||||
|
address_changed = check_ip != stream["ip_int"] or check_port != stream["port"]
|
||||||
|
|
||||||
|
if address_changed:
|
||||||
|
existing = manager.get_stream_by_ip_port(check_ip, check_port)
|
||||||
|
if existing and existing["id"] != stream_id:
|
||||||
|
raise HTTPException(status_code=400, detail="A TLS stream for this IP and Port already exists")
|
||||||
|
|
||||||
|
try:
|
||||||
|
manager.update_stream(stream_id, name=form.name, ip_int=new_ip_int, port=form.port, cert=form.cert, key=form.key)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
if address_changed:
|
||||||
|
await sync_dependent_services_address(stream_id, check_ip, check_port)
|
||||||
|
|
||||||
|
await reload_tls()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@app.delete('/streams/{stream_id}', response_model=StatusMessageModel)
|
||||||
|
async def delete_stream(stream_id: str):
|
||||||
|
stream = manager.get_stream(stream_id)
|
||||||
|
if not stream:
|
||||||
|
raise HTTPException(status_code=404, detail="TLS stream not found")
|
||||||
|
|
||||||
|
if get_dependent_services(stream_id):
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot delete this TLS stream because it is actively used by NFProxy or NFRegex services.")
|
||||||
|
|
||||||
|
manager.delete_stream(stream_id)
|
||||||
|
await reload_tls()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@app.post('/streams/{stream_id}/start', response_model=StatusMessageModel)
|
||||||
|
async def start_stream(stream_id: str):
|
||||||
|
if not manager.get_stream(stream_id):
|
||||||
|
raise HTTPException(status_code=404, detail="TLS stream not found")
|
||||||
|
manager.update_status(stream_id, "active")
|
||||||
|
await reload_tls()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@app.post('/streams/{stream_id}/stop', response_model=StatusMessageModel)
|
||||||
|
async def stop_stream(stream_id: str):
|
||||||
|
if not manager.get_stream(stream_id):
|
||||||
|
raise HTTPException(status_code=404, detail="TLS stream not found")
|
||||||
|
manager.update_status(stream_id, "stop")
|
||||||
|
await reload_tls()
|
||||||
|
await cascade_stop_dependents(stream_id)
|
||||||
|
return {"status": "ok"}
|
||||||
@ -15,7 +15,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
_localhost_env = os.getenv("LOCALHOST_IP", "127.0.0.1")
|
||||||
|
if _localhost_env == "None":
|
||||||
|
_localhost_env = "127.0.0.1"
|
||||||
|
LOCALHOST_IP = socket.gethostbyname(_localhost_env)
|
||||||
|
|
||||||
socketio:AsyncServer = None
|
socketio:AsyncServer = None
|
||||||
sid_list:set = set()
|
sid_list:set = set()
|
||||||
@ -25,9 +28,12 @@ ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
|||||||
ON_DOCKER = "DOCKER" in sys.argv
|
ON_DOCKER = "DOCKER" in sys.argv
|
||||||
DEBUG = "DEBUG" in sys.argv
|
DEBUG = "DEBUG" in sys.argv
|
||||||
NORELOAD = "NORELOAD" in sys.argv
|
NORELOAD = "NORELOAD" in sys.argv
|
||||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
_port_env = os.getenv("PORT", "4444")
|
||||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
FIREGEX_PORT = int(_port_env) if _port_env and _port_env != "None" else 4444
|
||||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
_host_env = os.getenv("HOST", "0.0.0.0")
|
||||||
|
FIREGEX_HOST = _host_env if _host_env and _host_env != "None" else "0.0.0.0"
|
||||||
|
_socket_dir_env = os.getenv("SOCKET_DIR", None)
|
||||||
|
FIREGEX_SOCKET_DIR = None if _socket_dir_env == "None" else _socket_dir_env
|
||||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||||
JWT_ALGORITHM: str = "HS256"
|
JWT_ALGORITHM: str = "HS256"
|
||||||
def _get_version():
|
def _get_version():
|
||||||
@ -305,6 +311,8 @@ def nicenessify(priority:int, pid:int|None=None):
|
|||||||
ps = psutil.Process(pid)
|
ps = psutil.Process(pid)
|
||||||
if os.name == 'posix':
|
if os.name == 'posix':
|
||||||
ps.nice(priority)
|
ps.nice(priority)
|
||||||
|
except (psutil.AccessDenied, PermissionError) as e:
|
||||||
|
print(f"Permission denied setting priority (pid={pid}, priority={priority}): {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -125,3 +125,44 @@ class SQLite():
|
|||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key, value):
|
||||||
return self.put(key, value)
|
return self.put(key, value)
|
||||||
|
|
||||||
|
def dump(self) -> dict:
|
||||||
|
self.connect()
|
||||||
|
res = {}
|
||||||
|
tables = self.query("SELECT name FROM sqlite_master WHERE type='table';")
|
||||||
|
for table in tables:
|
||||||
|
tname = table["name"]
|
||||||
|
if tname == 'keys_values':
|
||||||
|
res[tname] = self.query(f"SELECT * FROM {tname} WHERE key NOT IN ('password', 'secret');")
|
||||||
|
else:
|
||||||
|
res[tname] = self.query(f"SELECT * FROM {tname};")
|
||||||
|
return res
|
||||||
|
|
||||||
|
def load(self, data: dict):
|
||||||
|
self.connect()
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute("BEGIN")
|
||||||
|
for table_name, rows in data.items():
|
||||||
|
# Verify table exists to prevent SQL injection or schema mismatches
|
||||||
|
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?;", (table_name,))
|
||||||
|
if len(cur.fetchall()) == 0:
|
||||||
|
continue
|
||||||
|
cur.execute(f"DELETE FROM {table_name};")
|
||||||
|
if rows:
|
||||||
|
cols = list(rows[0].keys())
|
||||||
|
placeholders = ", ".join(["?"] * len(cols))
|
||||||
|
col_names = ", ".join(cols)
|
||||||
|
query = f"INSERT INTO {table_name} ({col_names}) VALUES ({placeholders})"
|
||||||
|
for row in rows:
|
||||||
|
cur.execute(query, [row[c] for c in cols])
|
||||||
|
cur.execute("COMMIT")
|
||||||
|
except Exception as e:
|
||||||
|
cur.execute("ROLLBACK")
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
try:
|
||||||
|
self.conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 298 KiB |
@ -5,35 +5,35 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "firegex-frontend",
|
"name": "firegex-frontend",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "latest",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@mantine/code-highlight": "latest",
|
"@mantine/code-highlight": "^9.3.2",
|
||||||
"@mantine/core": "latest",
|
"@mantine/core": "^9.3.2",
|
||||||
"@mantine/form": "latest",
|
"@mantine/form": "^9.3.2",
|
||||||
"@mantine/hooks": "latest",
|
"@mantine/hooks": "^9.3.2",
|
||||||
"@mantine/modals": "latest",
|
"@mantine/modals": "^9.3.2",
|
||||||
"@mantine/notifications": "latest",
|
"@mantine/notifications": "^9.3.2",
|
||||||
"@tanstack/react-query": "latest",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@vitejs/plugin-react-swc": "latest",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"buffer": "latest",
|
"@vitejs/plugin-react-swc": "^4.3.1",
|
||||||
"react": "latest",
|
"buffer": "^6.0.3",
|
||||||
"react-dom": "latest",
|
"react": "^19.2.7",
|
||||||
"react-icons": "latest",
|
"react-dom": "^19.2.7",
|
||||||
"react-router": "latest",
|
"react-icons": "^5.6.0",
|
||||||
"shiki": "latest",
|
"react-router": "^8.0.1",
|
||||||
"socket.io-client": "latest",
|
"shiki": "^4.2.0",
|
||||||
"typescript": "latest",
|
"socket.io-client": "^4.8.3",
|
||||||
"zustand": "latest",
|
"typescript": "^6.0.3",
|
||||||
|
"zustand": "^5.0.14",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/react-query-devtools": "latest",
|
"@tanstack/react-query-devtools": "^5.101.0",
|
||||||
"@types/jest": "latest",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "latest",
|
"@types/node": "^26.0.0",
|
||||||
"@types/react": "latest",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "latest",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "latest",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"vite": "latest",
|
"vite": "^8.0.16",
|
||||||
"vite-plugin-svgr": "latest",
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
"vite-tsconfig-paths": "latest",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -126,6 +126,10 @@
|
|||||||
|
|
||||||
"@mantine/store": ["@mantine/store@9.3.2", "", { "peerDependencies": { "react": "^19.2.0" } }, "sha512-uNmvjkwfUHZJ5bvzQroLFvubuUAHjBM0rh69RBRsUCG5FR5K4f8y4SX8+lch0wS6UJuuvodsSIK8H5jvZuFSgQ=="],
|
"@mantine/store": ["@mantine/store@9.3.2", "", { "peerDependencies": { "react": "^19.2.0" } }, "sha512-uNmvjkwfUHZJ5bvzQroLFvubuUAHjBM0rh69RBRsUCG5FR5K4f8y4SX8+lch0wS6UJuuvodsSIK8H5jvZuFSgQ=="],
|
||||||
|
|
||||||
|
"@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="],
|
||||||
|
|
||||||
|
"@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
|
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
|
||||||
@ -272,6 +276,8 @@
|
|||||||
|
|
||||||
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
|
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
|
||||||
|
|
||||||
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||||
@ -342,6 +348,8 @@
|
|||||||
|
|
||||||
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||||
|
|
||||||
|
"dompurify": ["dompurify@3.4.8", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ=="],
|
||||||
|
|
||||||
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
|
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.90", "", {}, "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.90", "", {}, "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug=="],
|
||||||
@ -374,8 +382,6 @@
|
|||||||
|
|
||||||
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||||
|
|
||||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
@ -448,6 +454,8 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="],
|
||||||
|
|
||||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
|
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
|
||||||
|
|
||||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||||
@ -460,6 +468,8 @@
|
|||||||
|
|
||||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
|
"monaco-editor": ["monaco-editor@0.56.0", "", { "dependencies": { "dompurify": "3.4.8", "marked": "14.0.0" } }, "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.14", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ=="],
|
"nanoid": ["nanoid@3.3.14", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ=="],
|
||||||
@ -554,6 +564,8 @@
|
|||||||
|
|
||||||
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
||||||
|
|
||||||
|
"state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="],
|
||||||
|
|
||||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
@ -570,8 +582,6 @@
|
|||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
"tsconfck": ["tsconfck@3.1.4", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ=="],
|
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="],
|
"type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="],
|
||||||
@ -606,8 +616,6 @@
|
|||||||
|
|
||||||
"vite-plugin-svgr": ["vite-plugin-svgr@5.2.0", "", { "dependencies": { "@rollup/pluginutils": "^5.3.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=3.0.0" } }, "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ=="],
|
"vite-plugin-svgr": ["vite-plugin-svgr@5.2.0", "", { "dependencies": { "@rollup/pluginutils": "^5.3.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=3.0.0" } }, "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ=="],
|
||||||
|
|
||||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
|
||||||
|
|
||||||
"ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
"ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"@mantine/hooks": "^9.3.2",
|
"@mantine/hooks": "^9.3.2",
|
||||||
"@mantine/modals": "^9.3.2",
|
"@mantine/modals": "^9.3.2",
|
||||||
"@mantine/notifications": "^9.3.2",
|
"@mantine/notifications": "^9.3.2",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"@vitejs/plugin-react-swc": "^4.3.1",
|
"@vitejs/plugin-react-swc": "^4.3.1",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
@ -39,7 +40,6 @@
|
|||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"vite": "^8.0.16",
|
"vite": "^8.0.16",
|
||||||
"vite-plugin-svgr": "^5.2.0",
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^26.0.0",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { Button, Group, Loader, LoadingOverlay, Notification, Space, PasswordInput, Title, Box } from '@mantine/core';
|
import { Button, Card, Group, Image, Loader, LoadingOverlay, Notification, Space, Stack, PasswordInput, Title, Text, Code, ThemeIcon, Box } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ImCross } from 'react-icons/im';
|
import { ImCross } from 'react-icons/im';
|
||||||
|
import { MdCloudOff } from 'react-icons/md';
|
||||||
import { Outlet, Route, Routes } from 'react-router';
|
import { Outlet, Route, Routes } from 'react-router';
|
||||||
import MainLayout from './components/MainLayout';
|
import MainLayout from './components/MainLayout';
|
||||||
import { PasswordSend, ServerStatusResponse } from './js/models';
|
import { PasswordSend, ServerStatusResponse } from './js/models';
|
||||||
@ -15,17 +16,64 @@ import NFProxy from './pages/NFProxy';
|
|||||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||||
import { useAuthStore, useSystemStore } from './js/store';
|
import { useAuthStore, useSystemStore } from './js/store';
|
||||||
|
|
||||||
|
const AuthShell = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<Box style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: "40px 20px" }}>
|
||||||
|
<Card withBorder radius="lg" p="xl" shadow="md" style={{ width: "100%", maxWidth: 440, borderColor: 'var(--fourth_color)', backgroundColor: 'var(--third_color)', boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
|
||||||
|
<Stack align="center" gap={2} mb="lg">
|
||||||
|
<Image src="/header-logo.png" alt="Firegex logo" w={56} />
|
||||||
|
<Title order={2} ta="center" style={{ fontFamily: 'Hanken Grotesk', color: 'var(--accent-color)' }}>[Fi]*regex</Title>
|
||||||
|
</Stack>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
|
const StatusCard = ({ title, description, color, children }: { title: string, description: string, color: string, children?: React.ReactNode }) => (
|
||||||
|
<Box style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: "40px 20px" }}>
|
||||||
|
<Card withBorder radius="lg" p="xl" shadow="md" style={{ width: "100%", maxWidth: 480, borderColor: 'var(--fourth_color)', backgroundColor: 'var(--third_color)', boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<ThemeIcon size={64} radius="xl" color={color} variant="light">
|
||||||
|
<MdCloudOff size={32} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={2} ta="center">{title}</Title>
|
||||||
|
<Text c="dimmed" ta="center">{description}</Text>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [systemStatus, setSystemStatus] = useState<ServerStatusResponse>({status:"", loggined:false})
|
const [systemStatus, setSystemStatus] = useState<ServerStatusResponse>({ status: "", loggined: false })
|
||||||
const [reqError, setReqError] = useState<undefined|string>()
|
const [reqError, setReqError] = useState<undefined | string>()
|
||||||
const [error, setError] = useState<string|null>()
|
const [error, setError] = useState<string | null>()
|
||||||
const [loadinBtn, setLoadingBtn] = useState(false);
|
const [loadinBtn, setLoadingBtn] = useState(false);
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { access_token } = useAuthStore()
|
const { access_token } = useAuthStore()
|
||||||
const { setVersion } = useSystemStore()
|
const { setVersion } = useSystemStore()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socketio.auth = { token: access_token || "" }
|
||||||
|
socketio.connect()
|
||||||
|
getStatus()
|
||||||
|
socketio.on("update", (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: data })
|
||||||
|
})
|
||||||
|
socketio.on("connect_error", (err) => {
|
||||||
|
if (access_token) {
|
||||||
|
errorNotify("Socket.Io connection failed! ", `Error message: [${err.message}]`)
|
||||||
|
}
|
||||||
|
getStatus()
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
socketio.off("update")
|
||||||
|
socketio.off("connect_error")
|
||||||
|
socketio.disconnect()
|
||||||
|
}
|
||||||
|
}, [access_token])
|
||||||
|
|
||||||
const getStatus = () => {
|
const getStatus = () => {
|
||||||
getstatus().then(res => {
|
getstatus().then(res => {
|
||||||
setSystemStatus(res)
|
setSystemStatus(res)
|
||||||
@ -44,146 +92,106 @@ function App() {
|
|||||||
getStatus()
|
getStatus()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (access_token && systemStatus.loggined) {
|
|
||||||
socketio.auth = { token: access_token }
|
|
||||||
if (!socketio.connected) {
|
|
||||||
socketio.connect()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (socketio.connected) {
|
|
||||||
socketio.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [access_token, systemStatus.loggined])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
socketio.on("update", (data) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: data })
|
|
||||||
})
|
|
||||||
socketio.on("connect_error", (err) => {
|
|
||||||
if (err.message === "Unauthorized") {
|
|
||||||
useAuthStore.getState().clearAccessToken()
|
|
||||||
getStatus()
|
|
||||||
} else if (access_token && systemStatus.loggined) {
|
|
||||||
errorNotify("Socket.Io connection failed! ", `Error message: [${err.message}]`)
|
|
||||||
getStatus()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
socketio.off("update")
|
|
||||||
socketio.off("connect_error")
|
|
||||||
}
|
|
||||||
}, [access_token, systemStatus.loggined])
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
password:"",
|
password: "",
|
||||||
},
|
},
|
||||||
validate:{
|
validate: {
|
||||||
password: (value) => value !== "" ? null : "Password is required",
|
password: (value) => value !== "" ? null : "Password is required",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (loading){
|
if (loading) {
|
||||||
return <LoadingOverlay visible/>
|
return <LoadingOverlay visible />
|
||||||
}else if (reqError){
|
} else if (reqError) {
|
||||||
return <Box className='center-flex-row' style={{padding:"100px"}}>
|
return <StatusCard title="Can't reach Firegex 🔥" description="The backend isn't responding. Retrying automatically..." color="red">
|
||||||
<Title order={1} style={{textAlign:"center"}}>Error launching Firegex! 🔥</Title>
|
<Code block style={{ width: "100%", wordBreak: "break-word", backgroundColor: 'var(--secondary_color)' }}>{reqError}</Code>
|
||||||
<Space h="md" />
|
<Loader size="sm" />
|
||||||
<Title order={4} style={{textAlign:"center"}}>Error communicating with backend</Title>
|
</StatusCard>
|
||||||
<Space h="md" />
|
} else if (systemStatus.status === "init") {
|
||||||
Error: {reqError}
|
|
||||||
<Space h="xl" />
|
const submitRequest = async (values: PasswordSend) => {
|
||||||
<Loader />
|
|
||||||
</Box>
|
|
||||||
}else if (systemStatus.status === "init"){
|
|
||||||
|
|
||||||
const submitRequest = async (values:PasswordSend) => {
|
|
||||||
setLoadingBtn(true)
|
setLoadingBtn(true)
|
||||||
await setpassword(values).then(res => {
|
await setpassword(values).then(res => {
|
||||||
if(!res){
|
if (!res) {
|
||||||
setSystemStatus({loggined:true, status:"run"})
|
setSystemStatus({ loggined: true, status: "run" })
|
||||||
}else{
|
} else {
|
||||||
setError(res)
|
setError(res)
|
||||||
}
|
}
|
||||||
}).catch( err => setError(err.toString()))
|
}).catch(err => setError(err.toString()))
|
||||||
setLoadingBtn(false)
|
setLoadingBtn(false)
|
||||||
form.reset()
|
form.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Box className='center-flex-row' style={{padding:"100px"}}>
|
|
||||||
<Title order={3} style={{textAlign:"center"}}>Setup: Choose the password for access to the firewall 🔒</Title>
|
|
||||||
<Space h="xl" />
|
|
||||||
<form onSubmit={form.onSubmit(submitRequest)} style={{width:"80%"}}>
|
|
||||||
<PasswordInput
|
|
||||||
label="Password"
|
|
||||||
placeholder="$3cr3t"
|
|
||||||
{...form.getInputProps('password')}
|
|
||||||
/>
|
|
||||||
<Group align="right" mt="md">
|
|
||||||
<Button loading={loadinBtn} type="submit">Set Password</Button>
|
|
||||||
</Group>
|
|
||||||
</form>
|
|
||||||
<Space h="xl" />
|
|
||||||
{error?<>
|
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
|
||||||
Error: {error}
|
|
||||||
</Notification><Space h="md" /></>:null}
|
|
||||||
</Box>
|
|
||||||
}else if (systemStatus.status === "run" && !systemStatus.loggined){
|
|
||||||
const submitRequest = async (values:PasswordSend) => {
|
|
||||||
setLoadingBtn(true)
|
|
||||||
await login(values).then(res => {
|
|
||||||
if(!res){
|
|
||||||
queryClient.invalidateQueries()
|
|
||||||
setSystemStatus({...systemStatus, loggined:true})
|
|
||||||
}else{
|
|
||||||
setError("Login failed")
|
|
||||||
}
|
|
||||||
}).catch( err => setError(err.toString()))
|
|
||||||
setLoadingBtn(false)
|
|
||||||
form.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <Box className='center-flex-row' style={{padding:"100px"}}>
|
return <AuthShell>
|
||||||
<Title order={2} style={{textAlign:"center"}}>Welcome to Firegex 🔥</Title>
|
<Title order={5} ta="center" style={{ color: 'var(--text-secondary)', fontWeight: 400 }}>Choose the password for access to the firewall 🔒</Title>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<Title order={2} style={{textAlign:"center"}}>Before you use the firewall, insert the password 🔒</Title>
|
<form onSubmit={form.onSubmit(submitRequest)} style={{ width: "100%" }}>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="$3cr3t"
|
||||||
|
{...form.getInputProps('password')}
|
||||||
|
/>
|
||||||
|
<Group mt="xl">
|
||||||
|
<Button loading={loadinBtn} type="submit" variant="light" color="cyan" fullWidth>Set Password</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
{error && <>
|
||||||
|
<Space h="lg" />
|
||||||
|
<Notification icon={<ImCross size={14} />} color="red" onClose={() => { setError(null) }}>
|
||||||
|
Error: {error}
|
||||||
|
</Notification></>}
|
||||||
|
</AuthShell>
|
||||||
|
} else if (systemStatus.status === "run" && !systemStatus.loggined) {
|
||||||
|
const submitRequest = async (values: PasswordSend) => {
|
||||||
|
setLoadingBtn(true)
|
||||||
|
await login(values).then(res => {
|
||||||
|
if (!res) {
|
||||||
|
queryClient.invalidateQueries()
|
||||||
|
setSystemStatus({ ...systemStatus, loggined: true })
|
||||||
|
} else {
|
||||||
|
setError("Login failed")
|
||||||
|
}
|
||||||
|
}).catch(err => setError(err.toString()))
|
||||||
|
setLoadingBtn(false)
|
||||||
|
form.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <AuthShell>
|
||||||
|
<Title order={5} ta="center" style={{ color: 'var(--text-secondary)', fontWeight: 400 }}>Insert the password to unlock the firewall 🔒</Title>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<form onSubmit={form.onSubmit(submitRequest)} style={{width:"80%"}}>
|
<form onSubmit={form.onSubmit(submitRequest)} style={{ width: "100%" }}>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label="Password"
|
||||||
placeholder="$3cr3t"
|
placeholder="$3cr3t"
|
||||||
{...form.getInputProps('password')}
|
{...form.getInputProps('password')}
|
||||||
/>
|
/>
|
||||||
<Group align="right" mt="md">
|
<Group mt="xl">
|
||||||
<Button loading={loadinBtn} type="submit">Login</Button>
|
<Button loading={loadinBtn} type="submit" variant="light" color="cyan" fullWidth>Login</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
<Space h="xl" />
|
{error && <>
|
||||||
{error?<>
|
<Space h="lg" />
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={() => { setError(null) }}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" /></>:null}
|
</Notification></>}
|
||||||
</Box>
|
</AuthShell>
|
||||||
}else if (systemStatus.status === "run" && systemStatus.loggined){
|
} else if (systemStatus.status === "run" && systemStatus.loggined) {
|
||||||
return <PageRouting getStatus={getStatus} />
|
return <PageRouting getStatus={getStatus} />
|
||||||
}else{
|
} else {
|
||||||
return <Box className='center-flex-row' style={{padding:"100px"}}>
|
return <StatusCard title="Unexpected error 🔥" description="Firegex returned an unexpected status. Try reloading the page." color="orange" />
|
||||||
<Title order={1} style={{textAlign:"center"}}>Error launching Firegex! 🔥</Title>
|
|
||||||
<Space h="md" />
|
|
||||||
<Title order={4} style={{textAlign:"center"}}>Error communicating with backend</Title>
|
|
||||||
</Box>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
import TLSDecrypt from './pages/TLSDecrypt';
|
||||||
|
|
||||||
|
const PageRouting = ({ getStatus }: { getStatus: () => void }) => {
|
||||||
|
|
||||||
return <Routes>
|
return <Routes>
|
||||||
<Route element={<MainLayout><Outlet /></MainLayout>}>
|
<Route element={<MainLayout><Outlet /></MainLayout>}>
|
||||||
|
<Route path="tls-decrypt" element={<TLSDecrypt />} />
|
||||||
<Route path="nfregex" element={<NFRegex><Outlet /></NFRegex>} >
|
<Route path="nfregex" element={<NFRegex><Outlet /></NFRegex>} >
|
||||||
<Route path=":srv" element={<ServiceDetailsNFRegex />} />
|
<Route path=":srv" element={<ServiceDetailsNFRegex />} />
|
||||||
</Route>
|
</Route>
|
||||||
@ -192,9 +200,9 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
|||||||
</Route>
|
</Route>
|
||||||
<Route path="firewall" element={<Firewall />} />
|
<Route path="firewall" element={<Firewall />} />
|
||||||
<Route path="porthijack" element={<PortHijack />} />
|
<Route path="porthijack" element={<PortHijack />} />
|
||||||
<Route path="*" element={<HomeRedirector />} />
|
<Route path="*" element={<HomeRedirector />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Card, Text, Title, SegmentedControl, Box } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RegexAddForm } from '../js/models';
|
import { RegexAddForm } from '../js/models';
|
||||||
@ -68,48 +68,73 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="lg" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Regex"
|
label="Regex"
|
||||||
placeholder="[A-Z0-9]{31}="
|
placeholder="[A-Z0-9]{31}="
|
||||||
|
size="md"
|
||||||
|
data-autofocus
|
||||||
{...form.getInputProps('regex')}
|
{...form.getInputProps('regex')}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
|
||||||
<Switch
|
<Space h="xl" />
|
||||||
label="Case insensitive"
|
<Group justify="space-between" align="center">
|
||||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
<Box>
|
||||||
/>
|
<Text fw={500} size="sm">Packet Direction</Text>
|
||||||
<Space h="md" />
|
<Text size="xs" c="dimmed">Choose which traffic this regex should filter</Text>
|
||||||
<Switch
|
</Box>
|
||||||
label="Deactivate"
|
<SegmentedControl
|
||||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
value={form.values.mode}
|
||||||
/>
|
onChange={(val) => form.setFieldValue('mode', val)}
|
||||||
<Space h="md" />
|
data={[
|
||||||
<Select
|
{ label: 'Client → Server', value: 'C' },
|
||||||
data={[
|
{ label: 'Bidirectional', value: 'B' },
|
||||||
{ value: 'C', label: 'Client -> Server' },
|
{ label: 'Server → Client', value: 'S' },
|
||||||
{ value: 'S', label: 'Server -> Client' },
|
]}
|
||||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
color="cyan"
|
||||||
]}
|
size="sm"
|
||||||
label="Choose the source of the packets to filter"
|
/>
|
||||||
variant="filled"
|
</Group>
|
||||||
{...form.getInputProps('mode')}
|
|
||||||
/>
|
<Space h="xl" />
|
||||||
<Group align="right" mt="md">
|
<Card withBorder radius="md" p="md" bg="transparent">
|
||||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Text fw={500} size="sm">Case Insensitive</Text>
|
||||||
|
<Switch
|
||||||
|
color="cyan"
|
||||||
|
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" c="dimmed" mb="md">
|
||||||
|
Ignore letter casing when matching this regex against packets.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Text fw={500} size="sm">Start Deactivated</Text>
|
||||||
|
<Switch
|
||||||
|
color="orange"
|
||||||
|
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Create the filter but keep it disabled initially.
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="xl">
|
||||||
|
<Button variant="default" onClick={close}>Cancel</Button>
|
||||||
|
<Button loading={submitLoading} type="submit" color="cyan">Add Filter</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Space h="md" />
|
|
||||||
|
|
||||||
{error?<>
|
{error?<>
|
||||||
|
<Space h="md" />
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" /></>:null}
|
</Notification></>:null}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddNewRegex;
|
export default AddNewRegex;
|
||||||
|
|||||||
@ -1,27 +1,26 @@
|
|||||||
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
|
import { SegmentedControl, SegmentedControlProps } from "@mantine/core";
|
||||||
import { RuleMode, Table } from "./utils";
|
import { RuleMode, Table } from "./utils";
|
||||||
import { table } from "console";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const ModeSelector = (props:Omit<SegmentedControlProps, "data"> & { table: Table }) => {
|
export const ModeSelector = (props: Omit<SegmentedControlProps, "data"> & { table: Table }) => {
|
||||||
const isFilterTable = props.table == Table.FILTER
|
const isFilterTable = props.table == Table.FILTER
|
||||||
return <SegmentedControl
|
return <SegmentedControl
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
value: RuleMode.IN,
|
value: RuleMode.IN,
|
||||||
label: isFilterTable?'IN':'PREROUTING',
|
label: isFilterTable ? 'IN' : 'PREROUTING',
|
||||||
},
|
},
|
||||||
...(isFilterTable?[{
|
...(isFilterTable ? [{
|
||||||
value: RuleMode.FORWARD,
|
value: RuleMode.FORWARD,
|
||||||
label: 'FWD',
|
label: 'FWD',
|
||||||
}]:[]),
|
}] : []),
|
||||||
{
|
{
|
||||||
value: RuleMode.OUT,
|
value: RuleMode.OUT,
|
||||||
label: isFilterTable?'OUT':'POSTROUTING',
|
label: isFilterTable ? 'OUT' : 'POSTROUTING',
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
size={props.size?props.size:"xs"}
|
size={props.size ? props.size : "xs"}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -1,14 +1,21 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip, ActionIcon } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip, ActionIcon, Title, Tabs, Text, UnstyledButton, Card, Divider } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
import { okNotify, regex_ipv4, regex_ipv6, postapi } from '../../js/utils';
|
||||||
|
import TLSServiceFields from '../TLSServiceFields';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfproxy, Service } from './utils';
|
import { nfproxy, Service } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||||
|
import { TbNetwork, TbShieldLock, TbSettings, TbTarget } from "react-icons/tb";
|
||||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||||
|
|
||||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
type ServiceAddForm = ServiceAddFormOriginal & {
|
||||||
|
autostart: boolean,
|
||||||
|
tls_enabled: boolean,
|
||||||
|
tls_cert: string,
|
||||||
|
tls_key: string
|
||||||
|
}
|
||||||
|
|
||||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||||
|
|
||||||
@ -18,7 +25,11 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
ip_int:edit?.ip_int??"",
|
ip_int:edit?.ip_int??"",
|
||||||
proto:edit?.proto??"tcp",
|
proto:edit?.proto??"tcp",
|
||||||
fail_open: edit?.fail_open??false,
|
fail_open: edit?.fail_open??false,
|
||||||
autostart: true
|
autostart: true,
|
||||||
|
tls_enabled: edit?.target_type === 'tls',
|
||||||
|
tls_stream_id: edit?.tls_stream_id ?? undefined,
|
||||||
|
tls_cert: "",
|
||||||
|
tls_key: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -38,6 +49,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
}
|
}
|
||||||
}, [opened])
|
}, [opened])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
@ -47,26 +60,81 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
const submitRequest = async (values: ServiceAddForm) => {
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
|
let streamId = values.tls_stream_id;
|
||||||
|
|
||||||
if (edit){
|
if (edit){
|
||||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
if (values.tls_enabled && values.tls_cert && values.tls_key) {
|
||||||
|
try {
|
||||||
|
const res = await postapi('tls/streams', {
|
||||||
|
name: values.name,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
port: values.port,
|
||||||
|
cert: values.tls_cert,
|
||||||
|
key: values.tls_key
|
||||||
|
});
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamId = res.stream_id;
|
||||||
|
} catch (err: any) {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream: " + err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nfproxy.settings(edit.service_id, { port: values.port, proto: values.proto, ip_int: values.ip_int, fail_open: values.fail_open, target_type: values.tls_enabled ? "tls" : "flow", tls_stream_id: streamId }).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
okNotify(`Service ${values.name} settings updated`, `Successfully updated settings for service ${values.name}`)
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
if (values.tls_enabled && values.tls_cert && values.tls_key) {
|
||||||
|
try {
|
||||||
|
const res = await postapi('tls/streams', {
|
||||||
|
name: values.name,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
port: values.port,
|
||||||
|
cert: values.tls_cert,
|
||||||
|
key: values.tls_key
|
||||||
|
});
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamId = res.stream_id;
|
||||||
|
} catch (err: any) {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream: " + err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: values.name,
|
||||||
|
port: values.port,
|
||||||
|
proto: values.proto,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
fail_open: values.fail_open,
|
||||||
|
target_type: values.tls_enabled ? "tls" : "flow",
|
||||||
|
tls_stream_id: streamId,
|
||||||
|
}
|
||||||
|
nfproxy.servicesadd(payload).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) nfproxy.servicestart(res.service_id)
|
if (values.autostart) nfproxy.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
okNotify(`Service ${values.name} has been added`, `Successfully added service with port ${values.port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
@ -79,66 +147,131 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered styles={{ body: { maxHeight: 'calc(100vh - 150px)', overflowY: 'auto' } }}>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
{!edit?<TextInput
|
<Tabs defaultValue="general" color="cyan" variant="outline" radius="md">
|
||||||
label="Service name"
|
<Tabs.List>
|
||||||
placeholder="Challenge 01"
|
<Tabs.Tab value="general" leftSection={<TbSettings size={16} />}>
|
||||||
{...form.getInputProps('name')}
|
General
|
||||||
/>:null}
|
</Tabs.Tab>
|
||||||
<Space h="md" />
|
<Tabs.Tab value="advanced" leftSection={<TbShieldLock size={16} />}>
|
||||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
Advanced
|
||||||
<Space h="md" />
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
<Box className='center-flex'>
|
<Tabs.Panel value="general" pt="xl" pb="md">
|
||||||
<Box>
|
{!edit ? (
|
||||||
{!edit?<Switch
|
<TextInput
|
||||||
label="Auto-Start Service"
|
label="Service name"
|
||||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
placeholder="Challenge 01"
|
||||||
/>:null}
|
size="md"
|
||||||
<Space h="sm" />
|
{...form.getInputProps('name')}
|
||||||
<Group gap="xs" wrap="nowrap" align="center">
|
|
||||||
<Switch
|
|
||||||
label="Enable fail-open nfqueue"
|
|
||||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
) : null}
|
||||||
label={<>
|
<Space h="md" />
|
||||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
<Title order={6} mb="xs">Protocol</Title>
|
||||||
</>}
|
<SegmentedControl
|
||||||
zIndex={1000000}
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
color="cyan"
|
||||||
|
data={[
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'HTTP', value: 'http' },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps('proto')}
|
||||||
|
/>
|
||||||
|
<Space h="xl" />
|
||||||
|
<Divider mb="xl" label="Target Setup" labelPosition="center" />
|
||||||
|
|
||||||
|
<Title order={6} mb="xs">Target Type</Title>
|
||||||
|
<Group grow mb="xl">
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => form.setFieldValue('tls_enabled', false)}
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: form.values.tls_enabled ? '1px solid var(--fourth_color)' : '2px solid var(--accent-color)',
|
||||||
|
backgroundColor: form.values.tls_enabled ? 'transparent' : 'rgba(0, 229, 255, 0.05)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<Group wrap="nowrap">
|
||||||
variant="subtle"
|
<TbNetwork size={32} color={!form.values.tls_enabled ? "var(--accent-color)" : "gray"} />
|
||||||
color="gray"
|
<div>
|
||||||
size="sm"
|
<Text fw={600}>Direct Flow</Text>
|
||||||
onClick={(e: any) => { e.preventDefault(); e.stopPropagation(); }}
|
<Text size="xs" c="dimmed">Forward traffic directly without decryption</Text>
|
||||||
>
|
</div>
|
||||||
<IoMdInformationCircleOutline size={18} />
|
</Group>
|
||||||
</ActionIcon>
|
</UnstyledButton>
|
||||||
</Tooltip>
|
<UnstyledButton
|
||||||
|
onClick={() => form.setFieldValue('tls_enabled', true)}
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: !form.values.tls_enabled ? '1px solid var(--fourth_color)' : '2px solid var(--accent-color)',
|
||||||
|
backgroundColor: !form.values.tls_enabled ? 'transparent' : 'rgba(0, 229, 255, 0.05)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group wrap="nowrap">
|
||||||
|
<TbShieldLock size={32} color={form.values.tls_enabled ? "var(--accent-color)" : "gray"} />
|
||||||
|
<div>
|
||||||
|
<Text fw={600}>TLS Decrypted</Text>
|
||||||
|
<Text size="xs" c="dimmed">Decrypt TLS traffic for deep inspection</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
|
||||||
<Box className="flex-spacer"></Box>
|
|
||||||
{edit?null:<SegmentedControl
|
|
||||||
data={[
|
|
||||||
{ label: 'TCP', value: 'tcp' },
|
|
||||||
{ label: 'HTTP', value: 'http' },
|
|
||||||
]}
|
|
||||||
{...form.getInputProps('proto')}
|
|
||||||
/>}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
{!form.values.tls_enabled ? (
|
||||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||||
</Group>
|
) : (
|
||||||
|
<TLSServiceFields form={form} />
|
||||||
|
)}
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
{error?<>
|
<Tabs.Panel value="advanced" pt="xl" pb="md">
|
||||||
|
<Card withBorder padding="lg" radius="md" bg="transparent">
|
||||||
|
{!edit ? (
|
||||||
|
<Box mb="xl">
|
||||||
|
<Text fw={500} mb="xs">Auto-Start Service</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">Automatically start this service as soon as it is created.</Text>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
color="cyan"
|
||||||
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw={500} mb="xs">Enable fail-open nfqueue</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
Firegex uses nfqueue internally to handle packets. Enabling this option will allow packets to pass through the firewall automatically in case the regex engine is overloaded or crashes.
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
color="cyan"
|
||||||
|
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{error ? <>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={() => { setError(null) }}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" />
|
</Notification>
|
||||||
</>:null}
|
</> : null}
|
||||||
|
|
||||||
|
<Group justify='flex-end' mt="xl" pt="md" style={{ borderTop: '1px solid var(--fourth_color)' }}>
|
||||||
|
<Button variant="default" onClick={close}>Cancel</Button>
|
||||||
|
<Button loading={submitLoading} type="submit" color="cyan" disabled={edit ? !form.isDirty() : false}>
|
||||||
|
{edit ? "Save Changes" : "Create Service"}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
125
frontend/src/components/NFProxy/EditFilterModal.tsx
Normal file
125
frontend/src/components/NFProxy/EditFilterModal.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { Button, FileButton, Group, Modal, Notification, Space, Textarea, ActionIcon, Tooltip } from "@mantine/core";
|
||||||
|
import { nfproxy, Service } from "./utils";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ImCross } from "react-icons/im";
|
||||||
|
import { okNotify } from "../../js/utils";
|
||||||
|
import { MdUploadFile, MdSave } from "react-icons/md";
|
||||||
|
import { TbCode } from "react-icons/tb";
|
||||||
|
import Editor from "@monaco-editor/react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
|
||||||
|
export const EditFilterModal = ({ opened, onClose, service }: { opened: boolean, onClose: () => void, service?: Service }) => {
|
||||||
|
const close = () => {
|
||||||
|
onClose()
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
const [code, setCode] = useState<string>("");
|
||||||
|
|
||||||
|
// Fetch code on open
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened && service) {
|
||||||
|
nfproxy.getpyfilterscode(service.service_id).then(fetchedCode => {
|
||||||
|
setCode(fetchedCode || "");
|
||||||
|
}).catch(err => {
|
||||||
|
setError("Error fetching code: " + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [opened, service])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!service) return;
|
||||||
|
setSubmitLoading(true);
|
||||||
|
nfproxy.setpyfilterscode(service.service_id, code).then(res => {
|
||||||
|
if (!res) {
|
||||||
|
okNotify(`Service ${service.name} code updated`, `Successfully updated python code for proxy filter.`);
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
setError("Error: " + res);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
setError("Error: " + err);
|
||||||
|
}).finally(() => {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileUpload = (file: File | null) => {
|
||||||
|
if (file) {
|
||||||
|
file.text().then(text => {
|
||||||
|
setCode(text);
|
||||||
|
okNotify("File loaded", "Python code loaded into editor. Remember to save changes.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened && service != null}
|
||||||
|
onClose={close}
|
||||||
|
title={
|
||||||
|
<Group gap="xs">
|
||||||
|
<TbCode size={20} color="var(--accent-color)" />
|
||||||
|
Python Script Editor
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
size="80%"
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
centered
|
||||||
|
styles={{ body: { display: 'flex', flexDirection: 'column', height: '70vh' } }}
|
||||||
|
>
|
||||||
|
<Box style={{ flexGrow: 1, border: '1px solid var(--fourth_color)', borderRadius: 8, overflow: 'hidden' }}>
|
||||||
|
<Editor
|
||||||
|
height="100%"
|
||||||
|
language="python"
|
||||||
|
theme="vs-dark"
|
||||||
|
value={code}
|
||||||
|
onChange={(value) => setCode(value || "")}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'JetBrains Mono, monospace',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
smoothScrolling: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Space h="md" />
|
||||||
|
|
||||||
|
<Group justify="space-between" mt="auto">
|
||||||
|
<FileButton onChange={handleFileUpload} accept=".py" multiple={false}>
|
||||||
|
{(props) => (
|
||||||
|
<Button variant="light" color="gray" leftSection={<MdUploadFile size={16}/>} {...props}>
|
||||||
|
Load from .py file
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button variant="default" onClick={close}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
color="cyan"
|
||||||
|
leftSection={<MdSave size={16} />}
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={submitLoading}
|
||||||
|
>
|
||||||
|
Save Code
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Notification icon={<ImCross size={14} />} color="red" onClose={() => setError(null)}>
|
||||||
|
{error}
|
||||||
|
</Notification>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,21 +1,23 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip, Card, Group, Button, Text } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
import { FaPlay, FaStop, FaTrash } from 'react-icons/fa';
|
||||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
|
||||||
import YesNoModal from '../../YesNoModal';
|
import YesNoModal from '../../YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||||
import { BsTrashFill } from 'react-icons/bs';
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi';
|
||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbPlugConnected } from "react-icons/tb";
|
import { TbPlugConnected, TbShieldLock, TbHexagon } from "react-icons/tb";
|
||||||
import { FaFilter } from "react-icons/fa";
|
import { FaFilter } from "react-icons/fa";
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../AddEditService';
|
import AddEditService from '../AddEditService';
|
||||||
import { FaPencilAlt } from "react-icons/fa";
|
import { FaPencilAlt } from "react-icons/fa";
|
||||||
import { ExceptionWarning } from '../ExceptionWarning';
|
import { ExceptionWarning } from '../ExceptionWarning';
|
||||||
|
import { MdChevronRight } from "react-icons/md";
|
||||||
|
import TLSAssociationBadge from '../../TLSAssociationBadge';
|
||||||
|
|
||||||
|
|
||||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||||
|
|
||||||
@ -77,72 +79,87 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Box className='firegex__nfregex__rowbox'>
|
<Card
|
||||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
withBorder
|
||||||
<Box>
|
radius="md"
|
||||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
p="md"
|
||||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
w="100%"
|
||||||
<Title className="firegex__nfregex__name" ml="xs">
|
bg="transparent"
|
||||||
{service.name}
|
className="firegex__clickable_row"
|
||||||
</Title>
|
style={{ borderColor: 'var(--fourth_color)' }}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center" wrap={isMedium ? "nowrap" : "wrap"}>
|
||||||
|
<Group wrap="nowrap" align="flex-start">
|
||||||
|
<Box style={{
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: status_color === 'teal' ? 'rgba(32, 201, 151, 0.1)' : 'rgba(250, 82, 82, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: status_color === 'teal' ? 'var(--mantine-color-teal-filled)' : 'var(--mantine-color-red-filled)'
|
||||||
|
}}>
|
||||||
|
<TbHexagon size={24} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
<Box>
|
||||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
<Group gap="xs" align="center">
|
||||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
<Text fw={600} size="md">{service.name}</Text>
|
||||||
:{service.port}
|
<Badge color={status_color} variant="light" size="xs" radius="sm">
|
||||||
</Badge>
|
{service.status.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
{service.target_type === 'tls' && <TLSAssociationBadge tlsStreamId={service.tls_stream_id} />}
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" mt={4}>
|
||||||
|
<Text size="xs" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
||||||
|
PORT: :{service.port}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
||||||
|
• {service.target_type === 'tls' ? 'decrypted traffic to ' : ''}{service.ip_int} ON {service.proto.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
</Group>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Box className="center-flex-row">
|
<Group gap="xs" onClick={(e) => e.stopPropagation()}>
|
||||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
|
||||||
<Space h="xs" />
|
|
||||||
<Box className='center-flex'>
|
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
|
||||||
<Box className="center-flex">
|
|
||||||
<ExceptionWarning service_id={service.service_id} />
|
<ExceptionWarning service_id={service.service_id} />
|
||||||
<Space w="sm"/>
|
{service.status === "stop" ? (
|
||||||
<MenuDropDownWithButton>
|
<Button variant="default" size="xs" leftSection={<FaPlay size={10} />} onClick={startService} loading={buttonLoading}>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
Start
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
</Button>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
) : (
|
||||||
<Divider />
|
<Button variant="default" size="xs" leftSection={<FaStop size={10} />} onClick={stopService} loading={buttonLoading}>
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
Stop
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
</Button>
|
||||||
</MenuDropDownWithButton>
|
)}
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
<Menu>
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
<Menu.Target>
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
<Button variant="default" size="xs" leftSection={<IoSettingsSharp size={10} />}>
|
||||||
disabled={service.status === "stop"}
|
Options
|
||||||
aria-describedby="tooltip-stop-id">
|
</Button>
|
||||||
<FaStop size="20px" />
|
</Menu.Target>
|
||||||
</ActionIcon>
|
<Menu.Dropdown>
|
||||||
</Tooltip>
|
<Menu.Label>Actions</Menu.Label>
|
||||||
<Space w="md"/>
|
<Menu.Item leftSection={<BiRename size={14} />} onClick={() => setRenameModal(true)}>Rename Service</Menu.Item>
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
<Menu.Item leftSection={<IoSettingsSharp size={14} />} onClick={() => setEditModal(true)}>Settings</Menu.Item>
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
</Menu.Dropdown>
|
||||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
</Menu>
|
||||||
<FaPlay size="20px" />
|
|
||||||
</ActionIcon>
|
<Button variant="default" size="xs" leftSection={<FaTrash size={10} />} onClick={() => setDeleteModal(true)}>
|
||||||
</Tooltip>
|
Delete
|
||||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
</Button>
|
||||||
{onClick?<Box className='firegex__service_forward_btn'>
|
</Group>
|
||||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
<Tooltip label="View details" position="left">
|
||||||
</Box>:null}
|
<Box style={{ display: 'flex', alignItems: 'center', color: 'var(--text-secondary)' }}>
|
||||||
</Box>
|
<MdChevronRight size={22} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Tooltip>
|
||||||
</Box>
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ export const UploadFilterModal = ({ opened, onClose, service }: { opened: boolea
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened && file){
|
if (opened && file){
|
||||||
file.bytes().then( code => {
|
file.bytes().then( code => {
|
||||||
console.log(code.toString())
|
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
nfproxy.setpyfilterscode(service?.service_id??"",code.toString()).then( res => {
|
nfproxy.setpyfilterscode(service?.service_id??"",code.toString()).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
|
|||||||
@ -13,20 +13,34 @@ export type Service = {
|
|||||||
edited_packets:number,
|
edited_packets:number,
|
||||||
blocked_packets:number,
|
blocked_packets:number,
|
||||||
fail_open:boolean,
|
fail_open:boolean,
|
||||||
|
target_type:string,
|
||||||
|
tls_stream_id:string|null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TLSConfig = {
|
||||||
|
tls_enabled: boolean,
|
||||||
|
tls_cert: string | null,
|
||||||
|
tls_key: string | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type ServiceAddForm = {
|
export type ServiceAddForm = {
|
||||||
name:string,
|
name:string,
|
||||||
port:number,
|
port:number,
|
||||||
proto:string,
|
proto:string,
|
||||||
ip_int:string,
|
ip_int:string,
|
||||||
fail_open: boolean,
|
fail_open: boolean,
|
||||||
|
target_type?: string,
|
||||||
|
tls_stream_id?: string | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceSettings = {
|
export type ServiceSettings = {
|
||||||
port?:number,
|
port?:number,
|
||||||
|
proto?:string,
|
||||||
ip_int?:string,
|
ip_int?:string,
|
||||||
fail_open?: boolean,
|
fail_open?: boolean,
|
||||||
|
target_type?: string,
|
||||||
|
tls_stream_id?: string | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddResponse = {
|
export type ServiceAddResponse = {
|
||||||
@ -88,6 +102,10 @@ export const nfproxy = {
|
|||||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
|
updatetlsconfig: async (service_id:string, data:TLSConfig) => {
|
||||||
|
const { status } = await putapi(`nfproxy/services/${service_id}/tls-config`,data) as ServerResponse;
|
||||||
|
return status === "ok"?undefined:status
|
||||||
|
},
|
||||||
getpyfilterscode: async (service_id:string) => {
|
getpyfilterscode: async (service_id:string) => {
|
||||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,14 +1,21 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip, ActionIcon } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip, ActionIcon, Title, Tabs, Text, UnstyledButton, Badge, Card, Divider } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
import { okNotify, regex_ipv4, regex_ipv6, postapi } from '../../js/utils';
|
||||||
|
import TLSServiceFields from '../TLSServiceFields';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfregex, Service } from './utils';
|
import { nfregex, Service } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||||
|
import { TbNetwork, TbShieldLock, TbSettings, TbTarget } from "react-icons/tb";
|
||||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||||
|
|
||||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
type ServiceAddForm = ServiceAddFormOriginal & {
|
||||||
|
autostart: boolean,
|
||||||
|
tls_enabled: boolean,
|
||||||
|
tls_cert: string,
|
||||||
|
tls_key: string
|
||||||
|
}
|
||||||
|
|
||||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||||
|
|
||||||
@ -18,7 +25,11 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
ip_int:edit?.ip_int??"",
|
ip_int:edit?.ip_int??"",
|
||||||
proto:edit?.proto??"tcp",
|
proto:edit?.proto??"tcp",
|
||||||
fail_open: edit?.fail_open??false,
|
fail_open: edit?.fail_open??false,
|
||||||
autostart: true
|
autostart: true,
|
||||||
|
tls_enabled: edit?.target_type === 'tls',
|
||||||
|
tls_stream_id: edit?.tls_stream_id ?? undefined,
|
||||||
|
tls_cert: "",
|
||||||
|
tls_key: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -38,6 +49,8 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
}
|
}
|
||||||
}, [opened])
|
}, [opened])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
@ -47,26 +60,81 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
const submitRequest = async (values: ServiceAddForm) => {
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
|
let streamId = values.tls_stream_id;
|
||||||
|
|
||||||
if (edit){
|
if (edit){
|
||||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
if (values.tls_enabled && values.tls_cert && values.tls_key) {
|
||||||
|
try {
|
||||||
|
const res = await postapi('tls/streams', {
|
||||||
|
name: values.name,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
port: values.port,
|
||||||
|
cert: values.tls_cert,
|
||||||
|
key: values.tls_key
|
||||||
|
});
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamId = res.stream_id;
|
||||||
|
} catch (err: any) {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream: " + err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nfregex.settings(edit.service_id, { port: values.port, proto: values.proto, ip_int: values.ip_int, fail_open: values.fail_open, target_type: values.tls_enabled ? "tls" : "flow", tls_stream_id: streamId }).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
okNotify(`Service ${values.name} settings updated`, `Successfully updated settings for service ${values.name}`)
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
if (values.tls_enabled && values.tls_cert && values.tls_key) {
|
||||||
|
try {
|
||||||
|
const res = await postapi('tls/streams', {
|
||||||
|
name: values.name,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
port: values.port,
|
||||||
|
cert: values.tls_cert,
|
||||||
|
key: values.tls_key
|
||||||
|
});
|
||||||
|
if (res.status !== "ok") {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamId = res.stream_id;
|
||||||
|
} catch (err: any) {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
setError("Failed to create TLS Stream: " + err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: values.name,
|
||||||
|
port: values.port,
|
||||||
|
proto: values.proto,
|
||||||
|
ip_int: values.ip_int,
|
||||||
|
fail_open: values.fail_open,
|
||||||
|
target_type: values.tls_enabled ? "tls" : "flow",
|
||||||
|
tls_stream_id: streamId,
|
||||||
|
}
|
||||||
|
nfregex.servicesadd(payload).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) nfregex.servicestart(res.service_id)
|
if (values.autostart) nfregex.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
okNotify(`Service ${values.name} has been added`, `Successfully added service with port ${values.port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
@ -79,66 +147,134 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered styles={{ body: { maxHeight: 'calc(100vh - 150px)', overflowY: 'auto' } }}>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
{!edit?<TextInput
|
<Tabs defaultValue="general" color="cyan" variant="outline" radius="md">
|
||||||
label="Service name"
|
<Tabs.List>
|
||||||
placeholder="Challenge 01"
|
<Tabs.Tab value="general" leftSection={<TbSettings size={16} />}>
|
||||||
{...form.getInputProps('name')}
|
General
|
||||||
/>:null}
|
</Tabs.Tab>
|
||||||
<Space h="md" />
|
<Tabs.Tab value="advanced" leftSection={<TbShieldLock size={16} />}>
|
||||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
Advanced
|
||||||
<Space h="md" />
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
<Box className='center-flex'>
|
<Tabs.Panel value="general" pt="xl" pb="md">
|
||||||
<Box>
|
{!edit ? (
|
||||||
{!edit?<Switch
|
<TextInput
|
||||||
label="Auto-Start Service"
|
label="Service name"
|
||||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
placeholder="Challenge 01"
|
||||||
/>:null}
|
size="md"
|
||||||
<Space h="sm" />
|
{...form.getInputProps('name')}
|
||||||
<Group gap="xs" wrap="nowrap" align="center">
|
|
||||||
<Switch
|
|
||||||
label="Enable fail-open nfqueue"
|
|
||||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
) : null}
|
||||||
label={<>
|
<Space h="md" />
|
||||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
<Title order={6} mb="xs">Protocol</Title>
|
||||||
</>}
|
<SegmentedControl
|
||||||
zIndex={1000000}
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
color="cyan"
|
||||||
|
data={[
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'UDP', value: 'udp' },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps('proto')}
|
||||||
|
disabled={edit !== undefined}
|
||||||
|
/>
|
||||||
|
<Space h="xl" />
|
||||||
|
<Divider mb="xl" label="Target Setup" labelPosition="center" />
|
||||||
|
|
||||||
|
<Title order={6} mb="xs">Target Type</Title>
|
||||||
|
<Group grow mb="xl">
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => form.setFieldValue('tls_enabled', false)}
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: form.values.tls_enabled ? '1px solid var(--fourth_color)' : '2px solid var(--accent-color)',
|
||||||
|
backgroundColor: form.values.tls_enabled ? 'transparent' : 'rgba(0, 229, 255, 0.05)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<Group wrap="nowrap">
|
||||||
variant="subtle"
|
<TbNetwork size={32} color={!form.values.tls_enabled ? "var(--accent-color)" : "gray"} />
|
||||||
color="gray"
|
<div>
|
||||||
size="sm"
|
<Text fw={600}>Direct Flow</Text>
|
||||||
onClick={(e: any) => { e.preventDefault(); e.stopPropagation(); }}
|
<Text size="xs" c="dimmed">Forward traffic directly without decryption</Text>
|
||||||
>
|
</div>
|
||||||
<IoMdInformationCircleOutline size={18} />
|
</Group>
|
||||||
</ActionIcon>
|
</UnstyledButton>
|
||||||
</Tooltip>
|
<UnstyledButton
|
||||||
|
onClick={() => { if (form.values.proto === 'tcp') form.setFieldValue('tls_enabled', true) }}
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: !form.values.tls_enabled ? '1px solid var(--fourth_color)' : '2px solid var(--accent-color)',
|
||||||
|
backgroundColor: !form.values.tls_enabled ? 'transparent' : 'rgba(0, 229, 255, 0.05)',
|
||||||
|
opacity: form.values.proto !== 'tcp' && !form.values.tls_enabled ? 0.5 : 1,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
cursor: form.values.proto !== 'tcp' ? 'not-allowed' : 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group wrap="nowrap">
|
||||||
|
<TbShieldLock size={32} color={form.values.tls_enabled ? "var(--accent-color)" : "gray"} />
|
||||||
|
<div>
|
||||||
|
<Text fw={600}>TLS Decrypted</Text>
|
||||||
|
<Text size="xs" c="dimmed">Decrypt TLS traffic for deep inspection</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
|
||||||
<Box className="flex-spacer"></Box>
|
|
||||||
<SegmentedControl
|
|
||||||
data={[
|
|
||||||
{ label: 'TCP', value: 'tcp' },
|
|
||||||
{ label: 'UDP', value: 'udp' },
|
|
||||||
]}
|
|
||||||
{...form.getInputProps('proto')}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
{!form.values.tls_enabled ? (
|
||||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||||
</Group>
|
) : (
|
||||||
|
<TLSServiceFields form={form} disabled={form.values.proto !== 'tcp'} />
|
||||||
|
)}
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
{error?<>
|
<Tabs.Panel value="advanced" pt="xl" pb="md">
|
||||||
|
<Card withBorder padding="lg" radius="md" bg="transparent">
|
||||||
|
{!edit ? (
|
||||||
|
<Box mb="xl">
|
||||||
|
<Text fw={500} mb="xs">Auto-Start Service</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">Automatically start this service as soon as it is created.</Text>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
color="cyan"
|
||||||
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw={500} mb="xs">Enable fail-open nfqueue</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
Firegex uses nfqueue internally to handle packets. Enabling this option will allow packets to pass through the firewall automatically in case the regex engine is overloaded or crashes.
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
color="cyan"
|
||||||
|
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{error ? <>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={() => { setError(null) }}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" />
|
</Notification>
|
||||||
</>:null}
|
</> : null}
|
||||||
|
|
||||||
|
<Group justify='flex-end' mt="xl" pt="md" style={{ borderTop: '1px solid var(--fourth_color)' }}>
|
||||||
|
<Button variant="default" onClick={close}>Cancel</Button>
|
||||||
|
<Button loading={submitLoading} type="submit" color="cyan" disabled={edit ? !form.isDirty() : false}>
|
||||||
|
{edit ? "Save Changes" : "Create Service"}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip, Card, Group, Button, Text } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
import { FaPlay, FaStop, FaTrash } from 'react-icons/fa';
|
||||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
|
||||||
import YesNoModal from '../../YesNoModal';
|
import YesNoModal from '../../YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||||
import { BsTrashFill } from 'react-icons/bs';
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi';
|
||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
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 { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../AddEditService';
|
import AddEditService from '../AddEditService';
|
||||||
|
import { TbShieldLock, TbHexagon } from "react-icons/tb";
|
||||||
|
import { MdChevronRight } from "react-icons/md";
|
||||||
|
import TLSAssociationBadge from '../../TLSAssociationBadge';
|
||||||
|
|
||||||
|
|
||||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||||
|
|
||||||
@ -75,68 +76,86 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Box className='firegex__nfregex__rowbox'>
|
<Card
|
||||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
withBorder
|
||||||
<Box>
|
radius="md"
|
||||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
p="md"
|
||||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
w="100%"
|
||||||
<Title className="firegex__nfregex__name" ml="xs">
|
bg="transparent"
|
||||||
{service.name}
|
className="firegex__clickable_row"
|
||||||
</Title>
|
style={{ borderColor: 'var(--fourth_color)' }}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center" wrap={isMedium ? "nowrap" : "wrap"}>
|
||||||
|
<Group wrap="nowrap" align="flex-start">
|
||||||
|
<Box style={{
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: status_color === 'teal' ? 'rgba(32, 201, 151, 0.1)' : 'rgba(250, 82, 82, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: status_color === 'teal' ? 'var(--mantine-color-teal-filled)' : 'var(--mantine-color-red-filled)'
|
||||||
|
}}>
|
||||||
|
<TbHexagon size={24} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
<Box>
|
||||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
<Group gap="xs" align="center">
|
||||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
<Text fw={600} size="md">{service.name}</Text>
|
||||||
:{service.port}
|
<Badge color={status_color} variant="light" size="xs" radius="sm">
|
||||||
</Badge>
|
{service.status.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
{service.target_type === 'tls' && <TLSAssociationBadge tlsStreamId={service.tls_stream_id} />}
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" mt={4}>
|
||||||
|
<Text size="xs" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
||||||
|
PORT: :{service.port}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
||||||
|
• {service.target_type === 'tls' ? 'decrypted traffic to ' : ''}{service.ip_int} ON {service.proto.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
</Group>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Box className="center-flex-row">
|
<Group gap="xs" onClick={(e) => e.stopPropagation()}>
|
||||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
{service.status === "stop" ? (
|
||||||
<Space h="xs" />
|
<Button variant="default" size="xs" leftSection={<FaPlay size={10} />} onClick={startService} loading={buttonLoading}>
|
||||||
<Box className='center-flex'>
|
Start
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
</Button>
|
||||||
<Space w="xs" />
|
) : (
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
<Button variant="default" size="xs" leftSection={<FaStop size={10} />} onClick={stopService} loading={buttonLoading}>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu>
|
||||||
|
<Menu.Target>
|
||||||
|
<Button variant="default" size="xs" leftSection={<IoSettingsSharp size={10} />}>
|
||||||
|
Options
|
||||||
|
</Button>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Label>Actions</Menu.Label>
|
||||||
|
<Menu.Item leftSection={<BiRename size={14} />} onClick={() => setRenameModal(true)}>Rename Service</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<IoSettingsSharp size={14} />} onClick={() => setEditModal(true)}>Settings</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<Button variant="default" size="xs" leftSection={<FaTrash size={10} />} onClick={() => setDeleteModal(true)}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Tooltip label="View details" position="left">
|
||||||
|
<Box style={{ display: 'flex', alignItems: 'center', color: 'var(--text-secondary)' }}>
|
||||||
|
<MdChevronRight size={22} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Tooltip>
|
||||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
</Group>
|
||||||
<Box className="center-flex">
|
</Group>
|
||||||
<MenuDropDownWithButton>
|
</Card>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
|
||||||
<Divider />
|
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
|
||||||
</MenuDropDownWithButton>
|
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
|
||||||
disabled={service.status === "stop"}
|
|
||||||
aria-describedby="tooltip-stop-id">
|
|
||||||
<FaStop size="20px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
|
||||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
|
||||||
<FaPlay size="20px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
|
||||||
{onClick?<Box className='firegex__service_forward_btn'>
|
|
||||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
|
||||||
</Box>:null}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ export type Service = {
|
|||||||
n_packets:number,
|
n_packets:number,
|
||||||
n_regex:number,
|
n_regex:number,
|
||||||
fail_open:boolean,
|
fail_open:boolean,
|
||||||
|
target_type:string,
|
||||||
|
tls_stream_id:string|null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddForm = {
|
export type ServiceAddForm = {
|
||||||
@ -21,6 +23,8 @@ export type ServiceAddForm = {
|
|||||||
proto:string,
|
proto:string,
|
||||||
ip_int:string,
|
ip_int:string,
|
||||||
fail_open: boolean,
|
fail_open: boolean,
|
||||||
|
target_type?: string,
|
||||||
|
tls_stream_id?: string | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceSettings = {
|
export type ServiceSettings = {
|
||||||
@ -28,6 +32,8 @@ export type ServiceSettings = {
|
|||||||
proto?:string,
|
proto?:string,
|
||||||
ip_int?:string,
|
ip_int?:string,
|
||||||
fail_open?: boolean,
|
fail_open?: boolean,
|
||||||
|
target_type?: string,
|
||||||
|
tls_stream_id?: string | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddResponse = {
|
export type ServiceAddResponse = {
|
||||||
@ -92,4 +98,8 @@ export const nfregex = {
|
|||||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
|
updatetlsconfig: async (service_id: string, data: { tls_enabled: boolean, tls_cert: string | null, tls_key: string | null }) => {
|
||||||
|
const { status } = await putapi(`nfregex/services/${service_id}/tls-config`, data) as ServerResponse;
|
||||||
|
return status === "ok" ? undefined : status;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@ -2,11 +2,13 @@ import { Divider, Group, MantineColor, ScrollArea, Text, ThemeIcon, Title, Unsty
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbPlugConnected } from "react-icons/tb";
|
import { TbPlugConnected } from "react-icons/tb";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { GrDirections } from "react-icons/gr";
|
import { GrDirections } from "react-icons/gr";
|
||||||
import { PiWallLight } from "react-icons/pi";
|
import { PiWallLight } from "react-icons/pi";
|
||||||
import { useNavbarStore } from "../../js/store";
|
import { useNavbarStore } from "../../js/store";
|
||||||
import { getMainPath } from "../../js/utils";
|
import { getMainPath, getapi, postapi, okNotify, errorNotify } from "../../js/utils";
|
||||||
import { BsRegex } from "react-icons/bs";
|
import { BsRegex } from "react-icons/bs";
|
||||||
|
import { MdDownload, MdUpload } from "react-icons/md";
|
||||||
|
|
||||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||||
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||||
@ -29,6 +31,7 @@ function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick
|
|||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
const [toggle, setToggleState] = useState(false);
|
const [toggle, setToggleState] = useState(false);
|
||||||
const { navOpened, closeNav } = useNavbarStore()
|
const { navOpened, closeNav } = useNavbarStore()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return <AppShell.Navbar p="md" hidden={!navOpened}>
|
return <AppShell.Navbar p="md" hidden={!navOpened}>
|
||||||
<Box px="xs" mt="xs">
|
<Box px="xs" mt="xs">
|
||||||
@ -40,11 +43,48 @@ export default function NavBar() {
|
|||||||
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
||||||
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
||||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||||
{/* <Box px="xs" mt="lg">
|
<NavBarButton navigate="tls-decrypt" closeNav={closeNav} name="TLS Decryption" color="cyan" icon={<TbPlugConnected size={19} />} />
|
||||||
<Title order={5}>Experimental Features 🧪</Title>
|
<Box px="xs" mt="lg">
|
||||||
|
<Title order={5}>Backup & Restore</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Text></Text>
|
<Divider my="xs" />
|
||||||
<Divider my="xs" /> */}
|
<NavBarButton closeNav={closeNav} name="Export Backup" color="yellow" icon={<MdDownload size={19} />} onClick={() => {
|
||||||
|
getapi('export').then(data => {
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `firegex_backup_${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
okNotify("Backup Exported", "Your configuration backup has been downloaded.");
|
||||||
|
}).catch(err => errorNotify("Export Failed", err.toString()));
|
||||||
|
}} />
|
||||||
|
<NavBarButton closeNav={closeNav} name="Import Backup" color="orange" icon={<MdUpload size={19} />} onClick={() => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target?.result as string);
|
||||||
|
postapi('import', data).then(() => {
|
||||||
|
okNotify("Backup Imported", "Configuration has been restored successfully.");
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
}).catch(err => errorNotify("Import Failed", err.toString()));
|
||||||
|
} catch (err: any) {
|
||||||
|
errorNotify("Invalid JSON", err.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Card, Divider, Tabs, Title } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { porthijack } from './utils';
|
import { porthijack } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
|
import { TbSettings, TbShieldLock } from 'react-icons/tb';
|
||||||
|
|
||||||
type ServiceAddForm = {
|
type ServiceAddForm = {
|
||||||
name:string,
|
name:string,
|
||||||
@ -66,36 +67,68 @@ function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void })
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
<TextInput
|
<Tabs defaultValue="general" color="cyan" variant="outline" radius="md">
|
||||||
label="Service name"
|
<Tabs.List>
|
||||||
placeholder="Challenge 01"
|
<Tabs.Tab value="general" leftSection={<TbSettings size={16} />}>
|
||||||
{...form.getInputProps('name')}
|
General
|
||||||
/>
|
</Tabs.Tab>
|
||||||
<Space h="md" />
|
<Tabs.Tab value="advanced" leftSection={<TbShieldLock size={16} />}>
|
||||||
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
Advanced
|
||||||
<Space h="md" />
|
</Tabs.Tab>
|
||||||
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
</Tabs.List>
|
||||||
<Space h="md" />
|
|
||||||
|
|
||||||
<Box className='center-flex'>
|
<Tabs.Panel value="general" pt="xl" pb="md">
|
||||||
<Switch
|
<TextInput
|
||||||
label="Auto-Start Service"
|
label="Service name"
|
||||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
placeholder="Challenge 01"
|
||||||
/>
|
size="md"
|
||||||
<Box className="flex-spacer" />
|
{...form.getInputProps('name')}
|
||||||
<SegmentedControl
|
/>
|
||||||
data={[
|
<Space h="md" />
|
||||||
{ label: 'TCP', value: 'tcp' },
|
<Title order={6} mb="xs">Protocol</Title>
|
||||||
{ label: 'UDP', value: 'udp' },
|
<SegmentedControl
|
||||||
]}
|
fullWidth
|
||||||
{...form.getInputProps('proto')}
|
size="md"
|
||||||
/>
|
color="cyan"
|
||||||
</Box>
|
data={[
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'UDP', value: 'udp' },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps('proto')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Space h="xl" />
|
||||||
|
<Divider mb="xl" label="Interface & Port Setup" labelPosition="center" />
|
||||||
|
|
||||||
|
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
||||||
|
<Space h="md" />
|
||||||
|
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
<Tabs.Panel value="advanced" pt="xl" pb="md">
|
||||||
<Button loading={submitLoading} type="submit">Add Service</Button>
|
<Card withBorder padding="lg" radius="md" bg="transparent">
|
||||||
|
<Box>
|
||||||
|
<Group justify="space-between" mb="xs">
|
||||||
|
<Title order={6}>Auto-Start Service</Title>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
color="cyan"
|
||||||
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Title order={6} size="sm" c="dimmed" style={{ fontWeight: "normal" }}>
|
||||||
|
Automatically start this service as soon as it is created.
|
||||||
|
</Title>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Group justify='flex-end' mt="xl" mb="sm">
|
||||||
|
<Button variant="default" onClick={close}>Cancel</Button>
|
||||||
|
<Button loading={submitLoading} type="submit" color="cyan">Add Service</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{error?<>
|
{error?<>
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip, Card, Group, Button, Text } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
import { FaPlay, FaStop, FaTrash } from 'react-icons/fa';
|
||||||
import { porthijack, Service } from '../utils';
|
import { porthijack, Service } from '../utils';
|
||||||
import YesNoModal from '../../YesNoModal';
|
import YesNoModal from '../../YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
||||||
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi';
|
||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
import ChangeDestination from './ChangeDestination';
|
import ChangeDestination from './ChangeDestination';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
import { MdDoubleArrow } from "react-icons/md";
|
import { TbHexagon } from "react-icons/tb";
|
||||||
|
|
||||||
export default function ServiceRow({ service }:{ service:Service }) {
|
export default function ServiceRow({ service }:{ service:Service }) {
|
||||||
|
|
||||||
@ -69,67 +69,80 @@ export default function ServiceRow({ service }:{ service:Service }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Box className='firegex__nfregex__rowbox'>
|
<Card
|
||||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
withBorder
|
||||||
<Box>
|
radius="md"
|
||||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
p="md"
|
||||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
w="100%"
|
||||||
<Title className="firegex__nfregex__name" ml="xs">
|
bg="transparent"
|
||||||
{service.name}
|
style={{
|
||||||
</Title>
|
borderColor: 'var(--fourth_color)',
|
||||||
|
transition: 'border-color 0.2s ease',
|
||||||
|
'&:hover': { borderColor: 'var(--mantine-color-dark-4)' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center" wrap={isMedium ? "nowrap" : "wrap"}>
|
||||||
|
<Group wrap="nowrap" align="flex-start">
|
||||||
|
<Box style={{
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: status_color === 'teal' ? 'rgba(32, 201, 151, 0.1)' : 'rgba(250, 82, 82, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: status_color === 'teal' ? 'var(--mantine-color-teal-filled)' : 'var(--mantine-color-red-filled)'
|
||||||
|
}}>
|
||||||
|
<TbHexagon size={24} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
<Box>
|
||||||
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
|
<Group gap="xs" align="center">
|
||||||
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
|
<Text fw={600} size="md">{service.name}</Text>
|
||||||
{service.proto}
|
<Badge color={status_color} variant="light" size="xs" radius="sm">
|
||||||
</Badge>
|
{service.active ? "ENABLED" : "DISABLED"}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" mt={4}>
|
||||||
|
<Text size="xs" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
||||||
|
FROM {service.ip_src}:{service.public_port}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
||||||
|
• TO {service.ip_dst}:{service.proxy_port} ON {service.proto.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
</Group>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
<Group gap="xs" onClick={(e) => e.stopPropagation()}>
|
||||||
<Box className="center-flex-row">
|
{service.active ? (
|
||||||
<Badge color="lime" radius="sm" size="lg" variant="filled">
|
<Button variant="default" size="xs" leftSection={<FaStop size={10} />} onClick={stopService} loading={buttonLoading}>
|
||||||
FROM {service.ip_src} :{service.public_port}
|
Stop
|
||||||
</Badge>
|
</Button>
|
||||||
<Space h="sm" />
|
) : (
|
||||||
<Badge color="blue" radius="sm" size="lg" variant="filled">
|
<Button variant="default" size="xs" leftSection={<FaPlay size={10} />} onClick={startService} loading={buttonLoading}>
|
||||||
<Box className="center-flex">
|
Start
|
||||||
TO {service.ip_dst} :{service.proxy_port}
|
</Button>
|
||||||
</Box>
|
)}
|
||||||
</Badge>
|
|
||||||
</Box>
|
<Menu>
|
||||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
<Menu.Target>
|
||||||
<Box className="center-flex">
|
<Button variant="default" size="xs" leftSection={<BsArrowRepeat size={10} />}>
|
||||||
<MenuDropDownWithButton>
|
Options
|
||||||
<Menu.Label><b>Rename service</b></Menu.Label>
|
</Button>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
</Menu.Target>
|
||||||
<Menu.Label><b>Change destination</b></Menu.Label>
|
<Menu.Dropdown>
|
||||||
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
<Menu.Label>Actions</Menu.Label>
|
||||||
<Divider />
|
<Menu.Item leftSection={<BiRename size={14} />} onClick={()=>setRenameModal(true)}>Rename service</Menu.Item>
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Item leftSection={<BsArrowRepeat size={14} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
</Menu.Dropdown>
|
||||||
</MenuDropDownWithButton>
|
</Menu>
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
<Button variant="default" size="xs" leftSection={<FaTrash size={10} />} onClick={() => setDeleteModal(true)}>
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
Delete
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
</Button>
|
||||||
disabled={!service.active}
|
</Group>
|
||||||
aria-describedby="tooltip-stop-id">
|
</Group>
|
||||||
<FaStop size="20px" />
|
</Card>
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
|
||||||
variant="filled" disabled={service.active}>
|
|
||||||
<FaPlay size="20px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
import { Text, Badge, Space, ActionIcon, Tooltip, Box, Card, Group } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PyFilter } from '../../js/models';
|
import { PyFilter } from '../../js/models';
|
||||||
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||||
@ -21,24 +21,43 @@ export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
|
|||||||
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
|
return (
|
||||||
|
<Card
|
||||||
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
|
withBorder
|
||||||
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
|
shadow="sm"
|
||||||
{filterInfo.name}
|
radius="md"
|
||||||
<Box className='flex-spacer' />
|
p="sm"
|
||||||
<Space w="xs" />
|
mb="sm"
|
||||||
{isMedium?<>
|
bg="transparent"
|
||||||
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
|
style={{ borderColor: 'var(--fourth_color)', transition: 'border-color 0.2s ease' }}
|
||||||
<Space w="xs" />
|
>
|
||||||
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
<Space w="lg" />
|
<Group gap="sm" align="center">
|
||||||
</>:null}
|
<Badge size="xs" circle color={filterInfo.active ? "lime" : "red"} variant="filled" />
|
||||||
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
|
<Text fw={600} style={{ fontFamily: 'monospace' }}>{filterInfo.name}</Text>
|
||||||
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
|
</Group>
|
||||||
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
</Box>
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<Badge size="sm" color="yellow" variant="light" leftSection={<FaFilter size={10} style={{ marginTop: 2 }} />}>
|
||||||
|
{filterInfo.blocked_packets}
|
||||||
|
</Badge>
|
||||||
|
<Badge size="sm" color="orange" variant="light" leftSection={<FaPencilAlt size={10} style={{ marginTop: 2 }} />}>
|
||||||
|
{filterInfo.edited_packets}
|
||||||
|
</Badge>
|
||||||
|
<Space w="xs" />
|
||||||
|
<Tooltip label={filterInfo.active ? "Deactivate" : "Activate"} color={filterInfo.active ? "orange" : "teal"}>
|
||||||
|
<ActionIcon
|
||||||
|
color={filterInfo.active ? "orange" : "teal"}
|
||||||
|
onClick={changeRegexStatus}
|
||||||
|
size="lg"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
{filterInfo.active ? <FaPause size={16} /> : <FaPlay size={16} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box, Card, Code, Group } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RegexFilter } from '../../js/models';
|
import { RegexFilter } from '../../js/models';
|
||||||
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||||
@ -41,45 +41,93 @@ function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
|||||||
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box className="firegex__regexview__box">
|
return (
|
||||||
<Box>
|
<Card
|
||||||
<Box className='center-flex' style={{width: "100%"}}>
|
withBorder
|
||||||
<Box className="firegex__regexview__outer_regex_text">
|
shadow="sm"
|
||||||
<Text className="firegex__regexview__regex_text" onClick={()=>{
|
radius="md"
|
||||||
|
p="sm"
|
||||||
|
mb="sm"
|
||||||
|
bg="transparent"
|
||||||
|
style={{ borderColor: 'var(--fourth_color)', transition: 'border-color 0.2s ease' }}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Box style={{ flexGrow: 1, overflow: 'hidden' }}>
|
||||||
|
<Tooltip label="Click to copy regex" position="top-start">
|
||||||
|
<Code
|
||||||
|
block
|
||||||
|
color="dark.8"
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflowX: 'auto',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
padding: '12px 16px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid var(--fourth_color)',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.2)'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
clipboard.copy(regex_expr)
|
clipboard.copy(regex_expr)
|
||||||
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
|
okNotify("Regex copied to clipboard!", `The regex '${regex_expr}' has been copied to the clipboard!`)
|
||||||
}}>{regex_expr}</Text>
|
}}
|
||||||
</Box>
|
>
|
||||||
<Space w="xs" />
|
{regex_expr}
|
||||||
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
|
</Code>
|
||||||
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
</Tooltip>
|
||||||
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Tooltip label="Delete regex" zIndex={0} color="red" >
|
|
||||||
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
|
|
||||||
<BsTrashFill size={22} /></ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Box display="flex" mt="sm" ml="xs">
|
|
||||||
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<YesNoModal
|
|
||||||
title='Are you sure to delete this regex?'
|
<Group gap="sm" wrap="nowrap" ml="md">
|
||||||
description={`You are going to delete the regex '${regex_expr}'.`}
|
<Tooltip label={regexInfo.active ? "Deactivate" : "Activate"} color={regexInfo.active ? "orange" : "teal"}>
|
||||||
onClose={()=>setDeleteModal(false)}
|
<ActionIcon
|
||||||
action={deleteRegex}
|
color={regexInfo.active ? "orange" : "teal"}
|
||||||
opened={deleteModal}
|
onClick={changeRegexStatus}
|
||||||
/>
|
size="xl"
|
||||||
|
radius="md"
|
||||||
</Box>
|
variant="light"
|
||||||
|
>
|
||||||
|
{regexInfo.active ? <FaPause size={18} /> : <FaPlay size={18} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Delete regex" color="red">
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
onClick={() => setDeleteModal(true)}
|
||||||
|
size="xl"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
<BsTrashFill size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="sm" mt="md">
|
||||||
|
<Badge size="md" color="yellow" variant="light" leftSection={<FaFilter size={12} style={{ marginTop: 2 }} />}>
|
||||||
|
{regexInfo.n_packets} packets
|
||||||
|
</Badge>
|
||||||
|
<Badge size="md" color={regexInfo.active ? "lime" : "red"} variant="light">
|
||||||
|
{regexInfo.active ? "ACTIVE" : "DISABLED"}
|
||||||
|
</Badge>
|
||||||
|
<Badge size="md" color={regexInfo.is_case_sensitive ? "grape" : "pink"} variant="light">
|
||||||
|
{regexInfo.is_case_sensitive ? "Strict" : "Loose"}
|
||||||
|
</Badge>
|
||||||
|
<Badge size="md" color="blue" variant="light">
|
||||||
|
{mode_string}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<YesNoModal
|
||||||
|
title='Are you sure to delete this regex?'
|
||||||
|
description={`You are going to delete the regex '${regex_expr}'.`}
|
||||||
|
onClose={() => setDeleteModal(false)}
|
||||||
|
action={deleteRegex}
|
||||||
|
opened={deleteModal}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RegexView;
|
export default RegexView;
|
||||||
|
|||||||
38
frontend/src/components/TLSAssociationBadge.tsx
Normal file
38
frontend/src/components/TLSAssociationBadge.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Badge, Tooltip } from '@mantine/core';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { getapi } from '../js/utils';
|
||||||
|
import { TbShieldLock } from 'react-icons/tb';
|
||||||
|
|
||||||
|
interface TLSStream {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ip_int: string;
|
||||||
|
port: number;
|
||||||
|
status: string;
|
||||||
|
ssl_port: number;
|
||||||
|
clear_port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TLSAssociationBadge({ tlsStreamId, size = "xs" }: { tlsStreamId?: string | null, size?: string }) {
|
||||||
|
const { data: streams } = useQuery<TLSStream[]>({
|
||||||
|
queryKey: ["tls_streams"],
|
||||||
|
queryFn: () => getapi("tls/streams")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tlsStreamId) return null;
|
||||||
|
const stream = streams?.find(s => s.id === tlsStreamId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
multiline
|
||||||
|
w={280}
|
||||||
|
label={stream
|
||||||
|
? `Decrypts TLS stream "${stream.name}" (public target ${stream.ip_int}:${stream.port}). The filter inspects the decrypted traffic on loopback:${stream.clear_port}, then it gets re-encrypted before reaching the real backend.`
|
||||||
|
: "Linked to a TLS Decrypt stream (stream details unavailable)"}
|
||||||
|
>
|
||||||
|
<Badge color="green" size={size as any} variant="light" radius="sm" leftSection={<TbShieldLock size={12} style={{ marginTop: 2 }} />}>
|
||||||
|
TLS{stream ? `: ${stream.name}` : ""}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
118
frontend/src/components/TLSServiceFields.tsx
Normal file
118
frontend/src/components/TLSServiceFields.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { Box, Button, Select, Space, TextInput, Textarea, Title, Group, FileButton, Text } from '@mantine/core';
|
||||||
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getapi, regex_ipv4, regex_ipv6 } from '../js/utils';
|
||||||
|
import PortAndInterface from './PortAndInterface';
|
||||||
|
import { MdAdd } from 'react-icons/md';
|
||||||
|
|
||||||
|
interface TLSServiceFieldsProps {
|
||||||
|
form: UseFormReturnType<any>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TLSServiceFields({ form, disabled }: TLSServiceFieldsProps) {
|
||||||
|
const { data: streams, isLoading } = useQuery<any[]>({
|
||||||
|
queryKey: ["tls_streams"],
|
||||||
|
queryFn: () => getapi("tls/streams")
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Don't auto-pick/overwrite the stream selection while editing an existing
|
||||||
|
// service — its ip_int/port/tls_stream_id already reflect the bound stream.
|
||||||
|
if (disabled) return;
|
||||||
|
if (!isCreatingNew && form.values.tls_enabled && streams && streams.length > 0) {
|
||||||
|
const match = streams.find(s => s.ip_int === form.values.ip_int && s.port === form.values.port);
|
||||||
|
if (!match) {
|
||||||
|
form.setFieldValue('ip_int', streams[0].ip_int);
|
||||||
|
form.setFieldValue('port', streams[0].port);
|
||||||
|
form.setFieldValue('tls_stream_id', streams[0].id);
|
||||||
|
} else if (!form.values.tls_stream_id) {
|
||||||
|
form.setFieldValue('tls_stream_id', match.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [streams, isCreatingNew, form.values.tls_enabled, disabled]);
|
||||||
|
|
||||||
|
const handleFileUpload = (file: File | null, fieldName: string) => {
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const text = e.target?.result as string;
|
||||||
|
form.setFieldValue(fieldName, text);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!form.values.tls_enabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pt="md">
|
||||||
|
<Group justify="space-between" align="center" mb="sm">
|
||||||
|
<Title order={6}>TLS Decryption Target</Title>
|
||||||
|
<Button size="xs" variant="light" leftSection={<MdAdd />} onClick={() => setIsCreatingNew(!isCreatingNew)} disabled={disabled}>
|
||||||
|
{isCreatingNew ? "Select Existing" : "Create New"}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Space h="sm" />
|
||||||
|
|
||||||
|
{!isCreatingNew ? (
|
||||||
|
<Select
|
||||||
|
data={streams?.map(s => ({
|
||||||
|
value: s.id,
|
||||||
|
label: `${s.name} (${s.ip_int}:${s.port})`
|
||||||
|
})) || []}
|
||||||
|
value={form.values.tls_stream_id || null}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (val) {
|
||||||
|
form.setFieldValue('tls_stream_id', val);
|
||||||
|
const stream = streams?.find(s => s.id === val);
|
||||||
|
if (stream) {
|
||||||
|
form.setFieldValue('ip_int', stream.ip_int);
|
||||||
|
form.setFieldValue('port', stream.port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Select a TLS Stream"
|
||||||
|
disabled={disabled || isLoading}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label="New TLS Stream Target IP & Port" />
|
||||||
|
<Space h="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" mb={4}>
|
||||||
|
<Text size="sm" fw={500}>TLS Certificate (PEM format) {isCreatingNew && form.values.tls_enabled && <Text span c="red">*</Text>}</Text>
|
||||||
|
<FileButton onChange={(file) => handleFileUpload(file, 'tls_cert')} accept=".pem,.crt,.cer,.txt,*">
|
||||||
|
{(props) => <Button {...props} size="xs" variant="subtle">Load from file</Button>}
|
||||||
|
</FileButton>
|
||||||
|
</Group>
|
||||||
|
<Textarea
|
||||||
|
placeholder="-----BEGIN CERTIFICATE-----..."
|
||||||
|
minRows={4}
|
||||||
|
styles={{ input: { fontFamily: 'monospace' } }}
|
||||||
|
{...form.getInputProps('tls_cert')}
|
||||||
|
required={isCreatingNew && form.values.tls_enabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Space h="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" mb={4}>
|
||||||
|
<Text size="sm" fw={500}>TLS Private Key (PEM format) {isCreatingNew && form.values.tls_enabled && <Text span c="red">*</Text>}</Text>
|
||||||
|
<FileButton onChange={(file) => handleFileUpload(file, 'tls_key')} accept=".pem,.key,.txt,*">
|
||||||
|
{(props) => <Button {...props} size="xs" variant="subtle">Load from file</Button>}
|
||||||
|
</FileButton>
|
||||||
|
</Group>
|
||||||
|
<Textarea
|
||||||
|
placeholder="-----BEGIN PRIVATE KEY-----..."
|
||||||
|
minRows={4}
|
||||||
|
styles={{ input: { fontFamily: 'monospace' } }}
|
||||||
|
{...form.getInputProps('tls_key')}
|
||||||
|
required={isCreatingNew && form.values.tls_enabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,17 +1,23 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500;700&display=swap');
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap');
|
|
||||||
|
|
||||||
:root{
|
:root{
|
||||||
--primary_color: #242a33;
|
--primary_color: #161618;
|
||||||
--secondary_color: #1A1B1E;
|
--secondary_color: #0a0a0c;
|
||||||
--third_color:#25262b;
|
--third_color: #1e1e20;
|
||||||
--fourth_color: #25262b99;
|
--fourth_color: #2c2c2f;
|
||||||
|
--accent-color: #00e5ff;
|
||||||
|
--accent-hover: #00b8cc;
|
||||||
|
--text-primary: #f5f5f5;
|
||||||
|
--text-secondary: #9aa0a6;
|
||||||
--mantine-color-body: var(--secondary_color) !important;
|
--mantine-color-body: var(--secondary_color) !important;
|
||||||
|
--mantine-font-family: 'Inter', sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Lato', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--secondary_color);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-flex{
|
.center-flex{
|
||||||
@ -132,19 +138,20 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.firegex__porthijack__servicerow__name{
|
.firegex__porthijack__servicerow__name{
|
||||||
|
font-family: 'Hanken Grotesk', sans-serif;
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
font-weight: bolder;
|
font-weight: 700;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
margin-bottom: 13px;
|
margin-bottom: 13px;
|
||||||
color:#FFF;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.firegex__porthijack__servicerow__portInput *{
|
.firegex__porthijack__servicerow__portInput *{
|
||||||
color: #FFF;
|
color: var(--accent-color);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.firegex__nfregex__row{
|
.firegex__nfregex__row{
|
||||||
@ -155,13 +162,21 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firegex__nfregex__row:hover{
|
||||||
}
|
}
|
||||||
|
|
||||||
.firegex__nfregex__name{
|
.firegex__nfregex__name{
|
||||||
|
font-family: 'Hanken Grotesk', sans-serif;
|
||||||
font-size: 1.6em;
|
font-size: 1.6em;
|
||||||
font-weight: bolder;
|
font-weight: 700;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
color:#FFF;
|
color: var(--text-primary);
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-left: -20px;
|
margin-left: -20px;
|
||||||
@ -175,12 +190,13 @@ body {
|
|||||||
.firegex__navbar__unstyled_button {
|
.firegex__navbar__unstyled_button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--mantine-spacing-xs);
|
padding: 12px 16px;
|
||||||
border-radius: var(--mantine-radius-sm);
|
border-radius: 8px;
|
||||||
color: var(--mantine-color-dark-0);
|
color: var(--text-secondary);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
font-weight: 500;
|
||||||
opacity: 1;
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.firegex__navbar__unstyled_button.disabled {
|
.firegex__navbar__unstyled_button.disabled {
|
||||||
@ -188,16 +204,20 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.firegex__navbar__unstyled_button.selected{
|
.firegex__navbar__unstyled_button.selected{
|
||||||
background-color: var(--mantine-color-dark-6);
|
background-color: var(--third_color);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border-color: var(--fourth_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.firegex__navbar__unstyled_button:hover {
|
.firegex__navbar__unstyled_button:hover {
|
||||||
background-color: var(--mantine-color-dark-6);
|
background-color: var(--third_color);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.firegex__footer{
|
.firegex__footer{
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
background-color: var(--primary_color);
|
background-color: var(--primary_color);
|
||||||
|
border-top: 1px solid var(--third_color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -227,9 +247,29 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--fourth_color);
|
background-color: transparent;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
border: #444 3px solid;
|
border: 1px solid var(--fourth_color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mantine-CodeHighlight-root, .mantine-CodeHighlight-pre {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firegex__clickable_row {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firegex__clickable_row:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.firegex__clickable_row:active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 599px) {
|
@media (max-width: 599px) {
|
||||||
|
|||||||
@ -55,9 +55,72 @@ export const customShikiAdapter: CodeHighlightAdapter = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import { createTheme } from '@mantine/core';
|
||||||
|
|
||||||
|
const myTheme = createTheme({
|
||||||
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
headings: { fontFamily: 'Hanken Grotesk, sans-serif' },
|
||||||
|
primaryColor: 'cyan',
|
||||||
|
colors: {
|
||||||
|
dark: [
|
||||||
|
'#f5f5f5', // 0: text
|
||||||
|
'#9aa0a6', // 1: text-secondary
|
||||||
|
'#5f6368', // 2: outline
|
||||||
|
'#3c4043', // 3: outline-variant
|
||||||
|
'#2c2c2f', // 4: surface-variant / fourth_color
|
||||||
|
'#1e1e20', // 5: surface-container-high / third_color
|
||||||
|
'#1a1a1c', // 6: surface-container
|
||||||
|
'#161618', // 7: surface-container-low / primary_color
|
||||||
|
'#0a0a0c', // 8: background / secondary_color
|
||||||
|
'#050505', // 9: surface-container-lowest
|
||||||
|
],
|
||||||
|
cyan: [
|
||||||
|
'#e0ffff',
|
||||||
|
'#b3fbff',
|
||||||
|
'#80f6ff',
|
||||||
|
'#4df0ff',
|
||||||
|
'#20ebff',
|
||||||
|
'#00e5ff', // 5: surface-tint / accent
|
||||||
|
'#00b8cc',
|
||||||
|
'#008f9f',
|
||||||
|
'#006975',
|
||||||
|
'#004f59',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
AppShell: {
|
||||||
|
styles: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: '#0a0a0c',
|
||||||
|
},
|
||||||
|
navbar: {
|
||||||
|
backgroundColor: '#161618',
|
||||||
|
borderRight: '1px solid #2c2c2f',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
backgroundColor: '#161618',
|
||||||
|
borderBottom: '1px solid #2c2c2f',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Card: {
|
||||||
|
defaultProps: {
|
||||||
|
bg: '#1e1e20',
|
||||||
|
radius: 'md',
|
||||||
|
withBorder: true,
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
root: {
|
||||||
|
borderColor: '#2c2c2f',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MantineProvider defaultColorScheme='dark'>
|
<MantineProvider defaultColorScheme='dark' theme={myTheme}>
|
||||||
<CodeHighlightAdapterProvider adapter={customShikiAdapter}>
|
<CodeHighlightAdapterProvider adapter={customShikiAdapter}>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Button, Group, Space, Modal, Switch } from '@mantine/core';
|
import { Button, Card, Group, Space, Modal, Stack, Switch } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { errorNotify, okNotify } from '../../js/utils';
|
import { errorNotify, okNotify } from '../../js/utils';
|
||||||
import { FirewallSettings, firewall } from '../../components/Firewall/utils';
|
import { FirewallSettings, firewall } from '../../components/Firewall/utils';
|
||||||
@ -33,22 +33,20 @@ export function SettingsModal({ opened, onClose }:{ opened:boolean, onClose:()=>
|
|||||||
|
|
||||||
return <Modal size="xl" title="Change firewall settings" opened={opened} onClose={onClose} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title="Change firewall settings" opened={opened} onClose={onClose} closeOnClickOutside={false} centered>
|
||||||
|
|
||||||
<Switch label="Keep rules on firegex shutdown" checked={settings.keep_rules} onChange={v => setSettings({...settings, keep_rules:v.target.checked})}/>
|
<Card withBorder radius="md" p="md" bg="transparent" style={{ borderColor: 'var(--fourth_color)' }}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Switch label="Keep rules on firegex shutdown" checked={settings.keep_rules} onChange={v => setSettings({...settings, keep_rules:v.target.checked})}/>
|
||||||
|
<Switch label="Allow loopback to communicate with itself" checked={settings.allow_loopback} onChange={v => setSettings({...settings, allow_loopback:v.target.checked})}/>
|
||||||
|
<Switch label="Allow established connection (essential to allow opening connection) (Dangerous to disable)" checked={settings.allow_established} onChange={v => setSettings({...settings, allow_established:v.target.checked})}/>
|
||||||
|
<Switch label="Allow icmp packets" checked={settings.allow_icmp} onChange={v => setSettings({...settings, allow_icmp:v.target.checked})}/>
|
||||||
|
<Switch label="Allow multicast DNS" checked={settings.multicast_dns} onChange={v => setSettings({...settings, multicast_dns:v.target.checked})}/>
|
||||||
|
<Switch label="Allow UPnP protocol" checked={settings.allow_upnp} onChange={v => setSettings({...settings, allow_upnp:v.target.checked})}/>
|
||||||
|
<Switch label="Drop invalid packet" checked={settings.drop_invalid} onChange={v => setSettings({...settings, drop_invalid:v.target.checked})}/>
|
||||||
|
<Switch label="Allow DHCP" checked={settings.allow_dhcp} onChange={v => setSettings({...settings, allow_dhcp:v.target.checked})}/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Switch label="Allow loopback to communicate with itself" checked={settings.allow_loopback} onChange={v => setSettings({...settings, allow_loopback:v.target.checked})}/>
|
<Group align="right">
|
||||||
<Space h="md" />
|
|
||||||
<Switch label="Allow established connection (essential to allow opening connection) (Dangerous to disable)" checked={settings.allow_established} onChange={v => setSettings({...settings, allow_established:v.target.checked})}/>
|
|
||||||
<Space h="md" />
|
|
||||||
<Switch label="Allow icmp packets" checked={settings.allow_icmp} onChange={v => setSettings({...settings, allow_icmp:v.target.checked})}/>
|
|
||||||
<Space h="md" />
|
|
||||||
<Switch label="Allow multicast DNS" checked={settings.multicast_dns} onChange={v => setSettings({...settings, multicast_dns:v.target.checked})}/>
|
|
||||||
<Space h="md" />
|
|
||||||
<Switch label="Allow UPnP protocol" checked={settings.allow_upnp} onChange={v => setSettings({...settings, allow_upnp:v.target.checked})}/>
|
|
||||||
<Space h="md" />
|
|
||||||
<Switch label="Drop invalid packet" checked={settings.drop_invalid} onChange={v => setSettings({...settings, drop_invalid:v.target.checked})}/>
|
|
||||||
<Space h="md" />
|
|
||||||
<Switch label="Allow DHCP" checked={settings.allow_dhcp} onChange={v => setSettings({...settings, allow_dhcp:v.target.checked})}/>
|
|
||||||
<Group align="right" mt="md">
|
|
||||||
<Button loading={submitLoading} onClick={submitRequest}>Save Setting</Button>
|
<Button loading={submitLoading} onClick={submitRequest}>Save Setting</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, FloatingIndicator, LoadingOverlay, Space, Switch, Table, Tabs, TextInput, ThemeIcon, Title, Tooltip, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Badge, Box, Card, Divider, FloatingIndicator, Group, LoadingOverlay, Space, Stack, Switch, Table, Tabs, TextInput, ThemeIcon, Title, Tooltip, useMantineTheme } from "@mantine/core"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { BsPlusLg, BsTrashFill } from "react-icons/bs"
|
import { BsPlusLg, BsTrashFill } from "react-icons/bs"
|
||||||
import { rem } from '@mantine/core';
|
import { rem } from '@mantine/core';
|
||||||
@ -25,196 +25,186 @@ import { PiWallLight } from "react-icons/pi";
|
|||||||
|
|
||||||
export const Firewall = () => {
|
export const Firewall = () => {
|
||||||
|
|
||||||
const [currentPolicy, setCurrentPolicy] = useState<ActionType>(ActionType.ACCEPT)
|
const [currentPolicy, setCurrentPolicy] = useState<ActionType>(ActionType.ACCEPT)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const rules = firewallRulesQuery()
|
const rules = firewallRulesQuery()
|
||||||
const [state, handlers] = useListState<Rule & {rule_id:string}>([]);
|
const [state, handlers] = useListState<Rule & { rule_id: string }>([]);
|
||||||
const [enableFwModal, setEnableFwModal] = useState(false)
|
const [enableFwModal, setEnableFwModal] = useState(false)
|
||||||
const [applyChangeModal, setApplyChangeModal] = useState(false)
|
const [applyChangeModal, setApplyChangeModal] = useState(false)
|
||||||
const [settingsModal, setSettingsModal] = useState(false)
|
const [settingsModal, setSettingsModal] = useState(false)
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const isMedium = useMediaQuery(`(min-width: 950px)`)??true
|
const isMedium = useMediaQuery(`(min-width: 950px)`) ?? true
|
||||||
const isSmall = useMediaQuery(`(max-width: 600px)`)??false
|
|
||||||
|
|
||||||
const [updateMevalueinternal, internalUpdateme] = useState(false)
|
const [updateMevalueinternal, internalUpdateme] = useState(false)
|
||||||
const updateMe = () => {
|
const updateMe = () => {
|
||||||
internalUpdateme(!updateMevalueinternal)
|
internalUpdateme(!updateMevalueinternal)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rules.isError)
|
||||||
|
errorNotify("Firewall Update failed!", getErrorMessage(rules.error))
|
||||||
|
}, [rules.isError])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rules.isLoading && rules.isFetched && !rules.isFetching) {
|
||||||
|
setCurrentPolicy(rules.data?.policy ?? ActionType.ACCEPT)
|
||||||
|
handlers.setState(JSON.parse(JSON.stringify((rules.data?.rules ?? []).map(v => ({ rule_id: makeid(30), ...v })))))
|
||||||
}
|
}
|
||||||
|
}, [rules.isFetched, rules.isLoading, rules.isFetching])
|
||||||
|
|
||||||
useEffect(()=> {
|
const fwEnabled = rules.data?.enabled ?? false
|
||||||
if(rules.isError)
|
const valuesChanged = JSON.stringify(rules.data?.rules) != JSON.stringify(state.map(v => {
|
||||||
errorNotify("Firewall Update failed!", getErrorMessage(rules.error))
|
const { rule_id, ...rest } = v
|
||||||
},[rules.isError])
|
return rest
|
||||||
|
})) || rules.data?.policy != currentPolicy
|
||||||
|
const [selectedTab, setSelectedTab] = useState<NFTables>(NFTables.FILTER)
|
||||||
|
|
||||||
useEffect(()=> {
|
|
||||||
if(!rules.isLoading && rules.isFetched && !rules.isFetching){
|
const enableFirewall = () => {
|
||||||
setCurrentPolicy(rules.data?.policy??ActionType.ACCEPT)
|
if (valuesChanged) {
|
||||||
handlers.setState(JSON.parse(JSON.stringify((rules.data?.rules??[]).map( v => ({rule_id: makeid(30), ...v})))))
|
applyChangesRaw().then(() => enableFirewallRaw())
|
||||||
|
} else {
|
||||||
|
enableFirewallRaw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableFirewallRaw = () => {
|
||||||
|
return firewall.enable()
|
||||||
|
.then(() => okNotify("Firewall enabled", "The firewall has been enabled"))
|
||||||
|
.catch((e) => errorNotify("Firewall enable failed!", getErrorMessage(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableFirewallRaw = () => {
|
||||||
|
return firewall.disable()
|
||||||
|
.then(() => okNotify("Firewall disabled", "The firewall has been disabled"))
|
||||||
|
.catch((e) => errorNotify("Firewall disable failed!", getErrorMessage(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchState = () => {
|
||||||
|
if (fwEnabled)
|
||||||
|
disableFirewallRaw()
|
||||||
|
else
|
||||||
|
if ([ActionType.REJECT, ActionType.DROP].includes(currentPolicy) || valuesChanged) {
|
||||||
|
setEnableFwModal(true)
|
||||||
|
} else {
|
||||||
|
enableFirewall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyChanges = () => {
|
||||||
|
if (fwEnabled && rules.data?.policy == ActionType.ACCEPT && [ActionType.REJECT, ActionType.DROP].includes(currentPolicy)) {
|
||||||
|
setApplyChangeModal(true)
|
||||||
|
} else {
|
||||||
|
applyChangesRaw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyRuleAdd = () => {
|
||||||
|
handlers.insert(0, {
|
||||||
|
rule_id: makeid(30),
|
||||||
|
active: true,
|
||||||
|
name: "Rule name",
|
||||||
|
proto: Protocol.TCP,
|
||||||
|
src: "",
|
||||||
|
dst: "",
|
||||||
|
port_src_from: 1,
|
||||||
|
port_dst_from: 8080,
|
||||||
|
port_src_to: 65535,
|
||||||
|
port_dst_to: 8080,
|
||||||
|
action: ActionType.ACCEPT,
|
||||||
|
mode: RuleMode.IN,
|
||||||
|
table: selectedTab
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseRules = () => {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyChangesRaw = () => {
|
||||||
|
const parsedRules = parseRules()
|
||||||
|
if (parsedRules === undefined) {
|
||||||
|
errorNotify("Firewall rules apply failed!", "The firewall rules are not valid")
|
||||||
|
return Promise.reject()
|
||||||
|
} else {
|
||||||
|
return firewall.ruleset({ rules: state, policy: currentPolicy })
|
||||||
|
.then(() => okNotify("Firewall rules applied", "The firewall rules has been applied"))
|
||||||
|
.catch((e) => errorNotify("Firewall rules apply failed!", getErrorMessage(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const items = state.map((item, index) => (
|
||||||
|
item.table == selectedTab && <Draggable key={item.rule_id} index={index} draggableId={item.rule_id}>
|
||||||
|
{(provided, snapshot) => {
|
||||||
|
const customInt = [
|
||||||
|
{ value: "0.0.0.0/0", netint: "ANY IPv4", label: "0.0.0.0/0" },
|
||||||
|
{ value: "::/0", netint: "ANY IPv6", label: "::/0" },
|
||||||
|
{ value: "", netint: "ANY", label: "ANY" }
|
||||||
|
]
|
||||||
|
const src_custom_int = customInt.map(v => v.value).includes(item.src) ? [] : [{ value: item.src, netint: "SELECTED", label: item.src }]
|
||||||
|
const dst_custom_int = customInt.map(v => v.value).includes(item.dst) ? [] : [{ value: item.dst, netint: "SELECTED", label: item.dst }]
|
||||||
|
const [srcPortEnabled, setSrcPortEnabled] = useState(item.port_src_from != 1 || item.port_src_to != 65535)
|
||||||
|
const [dstPortEnabled, setDstPortEnabled] = useState(item.port_dst_from != 1 || item.port_dst_to != 65535)
|
||||||
|
const [srcPortValue, setSrcPortValue] = useState(item.port_src_from == item.port_src_to ? `${item.port_src_from}` : `${item.port_src_from}-${item.port_src_to}`)
|
||||||
|
const [dstPortValue, setDstPortValue] = useState(item.port_dst_from == item.port_dst_to ? `${item.port_dst_from}` : `${item.port_dst_from}-${item.port_dst_to}`)
|
||||||
|
|
||||||
|
const port_range_setter = (rule: Rule, v: string, { src = false, dst = false }: { src?: boolean, dst?: boolean }) => {
|
||||||
|
const elements = v.split("-")
|
||||||
|
const values = [elements[0] ? parseInt(elements[0]) : 0, elements[1] ? parseInt(elements[1]) : 0]
|
||||||
|
values[1] = values[1] ? values[1] : values[0]
|
||||||
|
if (src) {
|
||||||
|
rule.port_src_from = values[0]
|
||||||
|
rule.port_src_to = values[1]
|
||||||
|
setSrcPortValue(v)
|
||||||
|
}
|
||||||
|
if (dst) {
|
||||||
|
rule.port_dst_from = values[0]
|
||||||
|
rule.port_dst_to = values[1]
|
||||||
|
setDstPortValue(v)
|
||||||
|
}
|
||||||
|
updateMe()
|
||||||
}
|
}
|
||||||
},[rules.isFetched, rules.isLoading, rules.isFetching])
|
|
||||||
|
|
||||||
const fwEnabled = rules.data?.enabled??false
|
const ip_setter = (rule: Rule, v: string | null, { src = false, dst = false }: { src?: boolean, dst?: boolean }) => {
|
||||||
const valuesChanged = JSON.stringify(rules.data?.rules) != JSON.stringify(state.map(v => {
|
const values = v ? v : ""
|
||||||
const {rule_id, ...rest} = v
|
if (src) {
|
||||||
return rest
|
rule.src = values
|
||||||
})) || rules.data?.policy != currentPolicy
|
}
|
||||||
const [selectedTab, setSelectedTab] = useState<NFTables>(NFTables.FILTER)
|
if (dst) {
|
||||||
|
rule.dst = values
|
||||||
|
}
|
||||||
const enableFirewall = () => {
|
updateMe()
|
||||||
if (valuesChanged){
|
|
||||||
applyChangesRaw().then(()=>enableFirewallRaw())
|
|
||||||
}else{
|
|
||||||
enableFirewallRaw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableFirewallRaw = () => {
|
|
||||||
return firewall.enable()
|
|
||||||
.then(()=>okNotify("Firewall enabled", "The firewall has been enabled"))
|
|
||||||
.catch((e)=>errorNotify("Firewall enable failed!", getErrorMessage(e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const disableFirewallRaw = () => {
|
|
||||||
return firewall.disable()
|
|
||||||
.then(()=>okNotify("Firewall disabled", "The firewall has been disabled"))
|
|
||||||
.catch((e)=>errorNotify("Firewall disable failed!", getErrorMessage(e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchState = () => {
|
|
||||||
if (fwEnabled)
|
|
||||||
disableFirewallRaw()
|
|
||||||
else
|
|
||||||
if ([ActionType.REJECT, ActionType.DROP].includes(currentPolicy) || valuesChanged){
|
|
||||||
setEnableFwModal(true)
|
|
||||||
}else{
|
|
||||||
enableFirewall()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const applyChanges = () => {
|
const proto_any = item.proto == Protocol.ANY
|
||||||
if (fwEnabled && rules.data?.policy == ActionType.ACCEPT && [ActionType.REJECT, ActionType.DROP].includes(currentPolicy)){
|
const disabletab = {
|
||||||
setApplyChangeModal(true)
|
port_box: proto_any,
|
||||||
}else{
|
src_port: !srcPortEnabled || proto_any,
|
||||||
applyChangesRaw()
|
dst_port: !dstPortEnabled || proto_any
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const condDiv = (val:React.ReactNode, cond:boolean) => {
|
return <Card
|
||||||
if (cond)
|
ref={provided.innerRef}
|
||||||
return <Box>{val}</Box>
|
{...provided.draggableProps}
|
||||||
else
|
withBorder
|
||||||
return val
|
radius="md"
|
||||||
}
|
p="md"
|
||||||
|
mb="md"
|
||||||
const emptyRuleAdd = () => {
|
w="100%"
|
||||||
handlers.insert(0,{
|
bg="transparent"
|
||||||
rule_id: makeid(30),
|
style={{ borderColor: 'var(--fourth_color)', transition: 'border-color 0.2s ease' }}
|
||||||
active: true,
|
>
|
||||||
name: "Rule name",
|
<Group wrap="nowrap" align="flex-start" gap="sm">
|
||||||
proto: Protocol.TCP,
|
<Box {...provided.dragHandleProps} style={{ display: 'flex', alignItems: 'center', height: rem(36) }}>
|
||||||
src: "",
|
<TbGripVertical style={{ width: rem(20), height: rem(20), color: 'var(--text-secondary)' }} />
|
||||||
dst: "",
|
</Box>
|
||||||
port_src_from: 1,
|
<Stack gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||||
port_dst_from: 8080,
|
<Group wrap="nowrap" gap="sm">
|
||||||
port_src_to: 65535,
|
|
||||||
port_dst_to: 8080,
|
|
||||||
action: ActionType.ACCEPT,
|
|
||||||
mode: RuleMode.IN,
|
|
||||||
table: selectedTab
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseRules = () => {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyChangesRaw = () => {
|
|
||||||
const parsedRules = parseRules()
|
|
||||||
if (parsedRules === undefined){
|
|
||||||
errorNotify("Firewall rules apply failed!", "The firewall rules are not valid")
|
|
||||||
return Promise.reject()
|
|
||||||
}else{
|
|
||||||
return firewall.ruleset({rules:state, policy:currentPolicy})
|
|
||||||
.then(()=>okNotify("Firewall rules applied", "The firewall rules has been applied"))
|
|
||||||
.catch((e)=>errorNotify("Firewall rules apply failed!", getErrorMessage(e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const items = state.map((item, index) => (
|
|
||||||
item.table == selectedTab && <Draggable key={item.rule_id} index={index} draggableId={item.rule_id}>
|
|
||||||
{(provided, snapshot) => {
|
|
||||||
const customInt = [
|
|
||||||
{ value: "0.0.0.0/0", netint: "ANY IPv4", label: "0.0.0.0/0" },
|
|
||||||
{ value: "::/0", netint: "ANY IPv6", label: "::/0" },
|
|
||||||
{ value: "", netint: "ANY", label: "ANY" }
|
|
||||||
]
|
|
||||||
const src_custom_int = customInt.map(v => v.value).includes(item.src)?[]:[{ value: item.src, netint: "SELECTED", label: item.src }]
|
|
||||||
const dst_custom_int = customInt.map(v => v.value).includes(item.dst)?[]:[{ value: item.dst, netint: "SELECTED", label: item.dst }]
|
|
||||||
const [srcPortEnabled, setSrcPortEnabled] = useState(item.port_src_from != 1 || item.port_src_to != 65535)
|
|
||||||
const [dstPortEnabled, setDstPortEnabled] = useState(item.port_dst_from != 1 || item.port_dst_to != 65535)
|
|
||||||
const [srcPortValue, setSrcPortValue] = useState(item.port_src_from==item.port_src_to?`${item.port_src_from}`:`${item.port_src_from}-${item.port_src_to}`)
|
|
||||||
const [dstPortValue, setDstPortValue] = useState(item.port_dst_from==item.port_dst_to?`${item.port_dst_from}`:`${item.port_dst_from}-${item.port_dst_to}`)
|
|
||||||
|
|
||||||
const port_range_setter = (rule:Rule, v:string, {src=false, dst=false}:{src?:boolean, dst?:boolean}) => {
|
|
||||||
const elements = v.split("-")
|
|
||||||
const values = [elements[0]?parseInt(elements[0]):0, elements[1]?parseInt(elements[1]):0]
|
|
||||||
values[1] = values[1]?values[1]:values[0]
|
|
||||||
if (src){
|
|
||||||
rule.port_src_from = values[0]
|
|
||||||
rule.port_src_to = values[1]
|
|
||||||
setSrcPortValue(v)
|
|
||||||
}
|
|
||||||
if (dst){
|
|
||||||
rule.port_dst_from = values[0]
|
|
||||||
rule.port_dst_to = values[1]
|
|
||||||
setDstPortValue(v)
|
|
||||||
}
|
|
||||||
updateMe()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ip_setter = (rule:Rule, v:string|null, {src=false, dst=false}:{src?:boolean, dst?:boolean}) => {
|
|
||||||
const values = v?v:""
|
|
||||||
if (src){
|
|
||||||
rule.src = values
|
|
||||||
}
|
|
||||||
if (dst){
|
|
||||||
rule.dst = values
|
|
||||||
}
|
|
||||||
updateMe()
|
|
||||||
}
|
|
||||||
|
|
||||||
const disable_style = { opacity:"0.4", cursor:"not-allowed" }
|
|
||||||
const proto_any = item.proto == Protocol.ANY
|
|
||||||
const disabletab = {
|
|
||||||
port_box: proto_any,
|
|
||||||
src_port: !srcPortEnabled || proto_any,
|
|
||||||
dst_port: !dstPortEnabled || proto_any
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalStyle = {
|
|
||||||
port_box: disabletab.port_box?disable_style:{},
|
|
||||||
src_port: disabletab.src_port?disable_style:{},
|
|
||||||
dst_port: disabletab.dst_port?disable_style:{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Box
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
>
|
|
||||||
<Box className='center-flex' style={{width:"100%"}}>
|
|
||||||
<Box {...provided.dragHandleProps}>
|
|
||||||
<TbGripVertical style={{ width: rem(30), height: rem(40) }} />
|
|
||||||
</Box>
|
|
||||||
<Space w="sm" />
|
|
||||||
<Box className={isMedium?"center-flex":"center-flex-row"} style={{width:"100%"}}>
|
|
||||||
<Box className="center-flex-row" style={{width:"100%"}}>
|
|
||||||
<Box className="center-flex" style={{width:"97%"}}>
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={item.active}
|
checked={item.active}
|
||||||
onChange={() =>{
|
onChange={() => {
|
||||||
item.active = !item.active
|
item.active = !item.active
|
||||||
updateMe()
|
updateMe()
|
||||||
}}
|
}}
|
||||||
@ -234,215 +224,207 @@ export const Firewall = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Space w="sm" />
|
<ActionIcon color="red" onClick={() => handlers.remove(index)} size="lg" radius="md" variant="filled"><BsTrashFill size={18} /></ActionIcon>
|
||||||
<ActionIcon color="red" onClick={()=>handlers.remove(index)} size="lg" radius="md" variant="filled"><BsTrashFill size={18} /></ActionIcon>
|
<TextInput defaultValue={item.name} onChange={(v) => { item.name = v.target.value; updateMe() }} style={{ flex: 1 }} />
|
||||||
<Space w="sm" />
|
</Group>
|
||||||
<TextInput defaultValue={item.name} onChange={(v)=>{item.name = v.target.value;updateMe()}} style={{width:"100%"}}/>
|
|
||||||
</Box>
|
<Group wrap={isMedium ? "nowrap" : "wrap"} align="flex-start" gap="md">
|
||||||
<Space h="sm" />
|
<Stack gap="xs" style={{ flex: 1, minWidth: isMedium ? 0 : "100%" }}>
|
||||||
<Box className="center-flex" style={{width:"97%"}}>
|
|
||||||
<Box style={{width:"100%"}}>
|
|
||||||
<InterfaceInput
|
<InterfaceInput
|
||||||
initialCustomInterfaces={[...src_custom_int, ...customInt]}
|
initialCustomInterfaces={[...src_custom_int, ...customInt]}
|
||||||
value={item.src}
|
value={item.src}
|
||||||
onChange={v => ip_setter(item, v, {src:true})}
|
onChange={v => ip_setter(item, v, { src: true })}
|
||||||
includeInterfaceNames
|
includeInterfaceNames
|
||||||
/>
|
/>
|
||||||
<Space h="sm" />
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Box className="center-flex" style={{width:"100%"}}>
|
<OnOffButton value={srcPortEnabled} onClick={() => {
|
||||||
<OnOffButton value={srcPortEnabled} onClick={() =>{
|
const value = !srcPortEnabled
|
||||||
const value = !srcPortEnabled
|
setSrcPortEnabled(value)
|
||||||
setSrcPortEnabled(value)
|
if (!value) {
|
||||||
if (!value){
|
item.port_src_from = 1
|
||||||
item.port_src_from = 1
|
item.port_src_to = 65535
|
||||||
item.port_src_to = 65535
|
updateMe()
|
||||||
updateMe()
|
} else {
|
||||||
}else{
|
port_range_setter(item, srcPortValue, { src: true })
|
||||||
port_range_setter(item, srcPortValue, {src:true})
|
}
|
||||||
}
|
}} size="lg" disabled={disabletab.port_box} variant="light" />
|
||||||
}} size="lg" disabled={disabletab.port_box} style={additionalStyle.port_box} variant="light" />
|
<PortRangeInput
|
||||||
<Space w="xs" />
|
onChange={v => port_range_setter(item, v.target.value, { src: true })}
|
||||||
<PortRangeInput
|
value={srcPortValue}
|
||||||
onChange={v => port_range_setter(item, v.target.value, {src:true})}
|
disabled={disabletab.src_port}
|
||||||
value={srcPortValue}
|
error={!disabletab.src_port && !srcPortValue}
|
||||||
disabled={disabletab.src_port}
|
style={{ flex: 1 }}
|
||||||
error={!disabletab.src_port && !srcPortValue}
|
/>
|
||||||
style={{width:"100%", ...additionalStyle.src_port}}
|
</Group>
|
||||||
/>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Box>
|
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', alignSelf: isMedium ? 'stretch' : 'center', padding: isMedium ? '28px 0 0' : '4px 0' }}>
|
||||||
<Space w="lg" />
|
<LuArrowBigRightDash size={22} color="var(--text-secondary)" style={isMedium ? undefined : { transform: 'rotate(90deg)' }} />
|
||||||
<LuArrowBigRightDash size={100} />
|
</Box>
|
||||||
<Space w="lg" />
|
|
||||||
<Box style={{width:"100%"}}>
|
<Stack gap="xs" style={{ flex: 1, minWidth: isMedium ? 0 : "100%" }}>
|
||||||
<InterfaceInput
|
<InterfaceInput
|
||||||
initialCustomInterfaces={[...dst_custom_int, ...customInt]}
|
initialCustomInterfaces={[...dst_custom_int, ...customInt]}
|
||||||
defaultValue={item.dst}
|
defaultValue={item.dst}
|
||||||
onChange={v => ip_setter(item, v, {dst:true})}
|
onChange={v => ip_setter(item, v, { dst: true })}
|
||||||
includeInterfaceNames
|
includeInterfaceNames
|
||||||
/>
|
/>
|
||||||
<Space h="sm" />
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Box className="center-flex" style={{width:"100%"}}>
|
<OnOffButton value={dstPortEnabled} onClick={() => {
|
||||||
<OnOffButton value={dstPortEnabled} onClick={() =>{
|
|
||||||
const value = !dstPortEnabled
|
const value = !dstPortEnabled
|
||||||
setDstPortEnabled(value)
|
setDstPortEnabled(value)
|
||||||
if (!value){
|
if (!value) {
|
||||||
item.port_dst_from = 1
|
item.port_dst_from = 1
|
||||||
item.port_dst_to = 65535
|
item.port_dst_to = 65535
|
||||||
updateMe()
|
updateMe()
|
||||||
}else{
|
} else {
|
||||||
port_range_setter(item, dstPortValue, {dst:true})
|
port_range_setter(item, dstPortValue, { dst: true })
|
||||||
}
|
}
|
||||||
}} size="lg" disabled={disabletab.port_box} style={additionalStyle.port_box} variant="light" />
|
}} size="lg" disabled={disabletab.port_box} variant="light" />
|
||||||
<Space w="xs" />
|
|
||||||
<PortRangeInput
|
<PortRangeInput
|
||||||
onChange={v => port_range_setter(item, v.target.value, {dst:true})}
|
onChange={v => port_range_setter(item, v.target.value, { dst: true })}
|
||||||
value={dstPortValue}
|
value={dstPortValue}
|
||||||
disabled={disabletab.dst_port}
|
disabled={disabletab.dst_port}
|
||||||
error={!disabletab.dst_port && !dstPortValue}
|
error={!disabletab.dst_port && !dstPortValue}
|
||||||
style={{width:"100%", ...additionalStyle.dst_port}}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Group>
|
||||||
</Box>
|
</Stack>
|
||||||
</Box>
|
</Group>
|
||||||
</Box>
|
|
||||||
{!isMedium?<Space h="sm" />:null}
|
<Group gap="xs" wrap="wrap">
|
||||||
<Box className={isMedium?"center-flex-row":"center-flex"} style={isMedium?{}:{width:"100%", justifyContent:"space-around"}}>
|
<ModeSelector
|
||||||
{condDiv(<>{condDiv(<ModeSelector
|
value={item.mode}
|
||||||
value={item.mode}
|
onChange={(value) => { item.mode = value as RuleMode; updateMe() }}
|
||||||
onChange={(value)=>{item.mode = value as RuleMode;updateMe()}}
|
style={{ flexGrow: 1, minWidth: rem(160) }}
|
||||||
style={{width:"100%"}}
|
table={item.table}
|
||||||
table={item.table}
|
/>
|
||||||
/>, !isMedium)}
|
<ProtocolSelector
|
||||||
<Space h="xs" />
|
value={item.proto}
|
||||||
{condDiv(<ProtocolSelector
|
onChange={(value) => { item.proto = value as Protocol; updateMe() }}
|
||||||
value={item.proto}
|
style={{ flexGrow: 1, minWidth: rem(160) }}
|
||||||
onChange={(value)=>{item.proto = value as Protocol;updateMe()}}
|
/>
|
||||||
style={{width:"100%"}}
|
<ActionTypeSelector
|
||||||
/>, !isMedium)}</>, isSmall)}
|
value={item.action}
|
||||||
<Space h="xs" />
|
onChange={(value) => { item.action = value as ActionType; updateMe() }}
|
||||||
{condDiv(<ActionTypeSelector
|
style={{ flexGrow: 1, minWidth: rem(160) }}
|
||||||
value={item.action}
|
/>
|
||||||
onChange={(value)=>{item.action = value as ActionType;updateMe()}}
|
</Group>
|
||||||
style={{width:"100%"}}
|
</Stack>
|
||||||
/>, !isMedium)}
|
</Group>
|
||||||
</Box>
|
</Card>
|
||||||
</Box>
|
}}
|
||||||
</Box>
|
</Draggable>
|
||||||
<Space h="md" />
|
)).filter(v => v);
|
||||||
<Divider />
|
|
||||||
<Space h="md" />
|
|
||||||
</Box>
|
|
||||||
}}
|
|
||||||
</Draggable>
|
|
||||||
)).filter(v => v);
|
|
||||||
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<LoadingOverlay visible={rules.isLoading} />
|
<LoadingOverlay visible={rules.isLoading} />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium ? 'center-flex' : 'center-flex-row'}>
|
||||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='red' ><PiWallLight size={20} /></ThemeIcon><Space w="xs" />Firewall Rules</Title>
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='red' ><PiWallLight size={20} /></ThemeIcon><Space w="xs" />Firewall Rules</Title>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium ? <Box className='flex-spacer' /> : <Space h="sm" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
Enabled: <Space w="sm" /> <Switch checked={fwEnabled} onChange={switchState} />
|
Enabled: <Space w="sm" /> <Switch checked={fwEnabled} onChange={switchState} />
|
||||||
<Space w="sm" />
|
<Space w="sm" />
|
||||||
Policy:
|
Policy:
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<ActionTypeSelector
|
<ActionTypeSelector
|
||||||
value={currentPolicy}
|
value={currentPolicy}
|
||||||
onChange={(value)=>setCurrentPolicy(value as ActionType)}
|
onChange={(value) => setCurrentPolicy(value as ActionType)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium ? <Box className='flex-spacer' /> : <Space h="sm" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaDirections style={{ marginBottom: -1, marginRight: 4}}/>Rules: {rules.isLoading?0:rules.data?.rules.length}</Badge>
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaDirections style={{ marginBottom: -1, marginRight: 4 }} />Rules: {rules.isLoading ? 0 : rules.data?.rules.length}</Badge>
|
||||||
<Space w="md" />
|
<Space w="md" />
|
||||||
<Tooltip label="Add a new rule" position='bottom' color="blue">
|
<Tooltip label="Add a new rule" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" onClick={emptyRuleAdd} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
<ActionIcon color="blue" onClick={emptyRuleAdd} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries({ queryKey: ["firewall"] })} size="lg" radius="md" variant="filled"
|
<ActionIcon color="indigo" onClick={() => queryClient.invalidateQueries({ queryKey: ["firewall"] })} size="lg" radius="md" variant="filled"
|
||||||
loading={rules.isFetching}><TbReload size={18} /></ActionIcon>
|
loading={rules.isFetching}><TbReload size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Settings" position='bottom' color="cyan">
|
<Tooltip label="Settings" position='bottom' color="cyan">
|
||||||
<ActionIcon color="cyan" onClick={()=>setSettingsModal(true)} size="lg" radius="md" variant="filled"><IoSettingsSharp size={18} /></ActionIcon>
|
<ActionIcon color="cyan" onClick={() => setSettingsModal(true)} size="lg" radius="md" variant="filled"><IoSettingsSharp size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Apply" position='bottom' color="grape">
|
<Tooltip label="Apply" position='bottom' color="grape">
|
||||||
<ActionIcon color="grape" onClick={applyChanges} size="lg" radius="md" variant="filled" disabled={!valuesChanged}>
|
<ActionIcon color="grape" onClick={applyChanges} size="lg" radius="md" variant="filled" disabled={!valuesChanged}>
|
||||||
<TiTick size={22} /></ActionIcon>
|
<TiTick size={22} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Space h="xl" />
|
||||||
|
<Divider />
|
||||||
|
<Space h="md" />
|
||||||
|
<Group justify="center" gap="sm">
|
||||||
|
<Box>Filtering Table:</Box>
|
||||||
|
<Tabs variant="pills" value={selectedTab} onChange={(v) => setSelectedTab(v == NFTables.MANGLE ? NFTables.MANGLE : NFTables.FILTER)}>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value={NFTables.FILTER}>
|
||||||
|
Filter
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value={NFTables.MANGLE}>
|
||||||
|
Mangle
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs>
|
||||||
|
</Group>
|
||||||
|
{items.length > 0 ? <DragDropContext
|
||||||
|
onDragEnd={({ destination, source }) =>
|
||||||
|
handlers.reorder({ from: source.index, to: destination?.index || 0 })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space h="md" />
|
||||||
|
<Droppable droppableId="dnd-list" direction="vertical">
|
||||||
|
{(provided) => (
|
||||||
|
<Box {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{items}
|
||||||
|
{provided.placeholder}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext> : <>
|
||||||
|
<Box className='center-flex-row'>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<Divider />
|
<Title className='center-flex' style={{ textAlign: "center" }} order={3}>Firewall Rules allows you to use nftables but through a web interface</Title>
|
||||||
<Space h="md"/>
|
<Space h="xs" />
|
||||||
<Tabs variant="pills" value={selectedTab} onChange={(v)=>setSelectedTab(v==NFTables.MANGLE?NFTables.MANGLE:NFTables.FILTER)} style={{ display:"flex", justifyContent:"center", alignItems:"center"}}>
|
<Title className='center-flex' style={{ textAlign: "center" }} order={5}>Add new rules, sort it and enable the firewall: be carefull, wrong rules could also drops out firegex access</Title>
|
||||||
<Box mr="md">Filtering Table:</Box>
|
<Space h="lg" />
|
||||||
<Tabs.List>
|
<Box className='center-flex' style={{ gap: 20 }}>
|
||||||
<Tabs.Tab value={NFTables.FILTER}>
|
<Tooltip label="Add a new rule" color="blue">
|
||||||
Filter
|
<ActionIcon color="blue" onClick={emptyRuleAdd} size="xl" radius="md" variant="filled">
|
||||||
</Tabs.Tab>
|
<BsPlusLg size="20px" /></ActionIcon>
|
||||||
<Tabs.Tab value={NFTables.MANGLE}>
|
</Tooltip>
|
||||||
Mangle
|
|
||||||
</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
</Tabs>
|
|
||||||
{items.length > 0?<DragDropContext
|
|
||||||
onDragEnd={({ destination, source }) =>
|
|
||||||
handlers.reorder({ from: source.index, to: destination?.index || 0 })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Space h="md" />
|
|
||||||
<Droppable droppableId="dnd-list" direction="vertical">
|
|
||||||
{(provided) => (
|
|
||||||
<Box {...provided.droppableProps} ref={provided.innerRef}>
|
|
||||||
{items}
|
|
||||||
{provided.placeholder}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>:<>
|
|
||||||
<Box className='center-flex-row'>
|
|
||||||
<Space h="xl" />
|
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Firewall Rules allows you to use nftables but through a web interface</Title>
|
|
||||||
<Space h="xs" />
|
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Add new rules, sort it and enable the firewall: be carefull, wrong rules could also drops out firegex access</Title>
|
|
||||||
<Space h="lg" />
|
|
||||||
<Box className='center-flex' style={{gap: 20}}>
|
|
||||||
<Tooltip label="Add a new rule" color="blue">
|
|
||||||
<ActionIcon color="blue" onClick={emptyRuleAdd} size="xl" radius="md" variant="filled">
|
|
||||||
<BsPlusLg size="20px" /></ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to apply the changes to the firewall?'
|
title='Are you sure to apply the changes to the firewall?'
|
||||||
description={`If there is a malconfiguration you can lose the access to your server! ⚠️`}
|
description={`If there is a malconfiguration you can lose the access to your server! ⚠️`}
|
||||||
onClose={()=>setEnableFwModal(false) }
|
onClose={() => setEnableFwModal(false)}
|
||||||
action={enableFirewall}
|
action={enableFirewall}
|
||||||
opened={enableFwModal}
|
opened={enableFwModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to apply the changes to the firewall?'
|
title='Are you sure to apply the changes to the firewall?'
|
||||||
description={`If there is a malconfiguration you can lose the access to your server! ⚠️`}
|
description={`If there is a malconfiguration you can lose the access to your server! ⚠️`}
|
||||||
onClose={()=>setApplyChangeModal(false) }
|
onClose={() => setApplyChangeModal(false)}
|
||||||
action={applyChangesRaw}
|
action={applyChangesRaw}
|
||||||
opened={applyChangeModal}
|
opened={applyChangeModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
opened={settingsModal}
|
opened={settingsModal}
|
||||||
onClose={()=>setSettingsModal(false)}
|
onClose={() => setSettingsModal(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Box, Code, LoadingOverlay, Space, Stack, Title, Tooltip, Card, Group, ThemeIcon } from '@mantine/core';
|
||||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||||
import { Badge, Divider, Menu } from '@mantine/core';
|
import { Badge, Divider, Menu } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@ -24,34 +24,35 @@ import { ModalLog } from '../../components/ModalLog';
|
|||||||
import { useListState } from '@mantine/hooks';
|
import { useListState } from '@mantine/hooks';
|
||||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||||
import { DocsButton } from '../../components/DocsButton';
|
import { DocsButton } from '../../components/DocsButton';
|
||||||
|
import TLSAssociationBadge from '../../components/TLSAssociationBadge';
|
||||||
|
|
||||||
export default function ServiceDetailsNFProxy() {
|
export default function ServiceDetailsNFProxy() {
|
||||||
|
|
||||||
const {srv} = useParams()
|
const { srv } = useParams()
|
||||||
const services = nfproxyServiceQuery()
|
const services = nfproxyServiceQuery()
|
||||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
const filtersList = nfproxyServicePyfiltersQuery(srv ?? "")
|
||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
const [buttonLoading, setButtonLoading] = useState(false)
|
const [buttonLoading, setButtonLoading] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
const filterCode = nfproxyServiceFilterCodeQuery(srv ?? "")
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
const [openLogModal, setOpenLogModal] = useState(false)
|
const [openLogModal, setOpenLogModal] = useState(false)
|
||||||
const [logData, logDataSetters] = useListState<string>([]);
|
const [logData, logDataSetters] = useListState<string>([]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
if (srv){
|
useEffect(() => {
|
||||||
if (openLogModal){
|
if (srv) {
|
||||||
|
if (openLogModal) {
|
||||||
logDataSetters.setState([])
|
logDataSetters.setState([])
|
||||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||||
logDataSetters.append(data)
|
logDataSetters.append(data)
|
||||||
});
|
});
|
||||||
}else{
|
} else {
|
||||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||||
socketio.off(`nfproxy-outstream-${srv}`);
|
socketio.off(`nfproxy-outstream-${srv}`);
|
||||||
logDataSetters.setState([])
|
logDataSetters.setState([])
|
||||||
@ -68,7 +69,7 @@ export default function ServiceDetailsNFProxy() {
|
|||||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||||
|
|
||||||
let status_color = "gray";
|
let status_color = "gray";
|
||||||
switch(serviceInfo.status){
|
switch (serviceInfo.status) {
|
||||||
case "stop": status_color = "red"; break;
|
case "stop": status_color = "red"; break;
|
||||||
case "active": status_color = "teal"; break;
|
case "active": status_color = "teal"; break;
|
||||||
}
|
}
|
||||||
@ -76,160 +77,168 @@ export default function ServiceDetailsNFProxy() {
|
|||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||||
if(!res){
|
if (!res) {
|
||||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
okNotify(`Service ${serviceInfo.name} started successfully!`, `The service on ${serviceInfo.port} has been started!`)
|
||||||
queryClient.invalidateQueries({ queryKey: serviceQueryKey })
|
queryClient.invalidateQueries({ queryKey: serviceQueryKey })
|
||||||
}else{
|
} else {
|
||||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`, `Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`, `Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false)
|
setButtonLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteService = () => {
|
const deleteService = () => {
|
||||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||||
if (!res){
|
if (!res) {
|
||||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
okNotify("Service delete complete!", `The service ${serviceInfo.name} has been deleted!`)
|
||||||
queryClient.invalidateQueries({ queryKey: serviceQueryKey })
|
queryClient.invalidateQueries({ queryKey: serviceQueryKey })
|
||||||
}else
|
} else
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
errorNotify("An error occurred while deleting a service", `Error: ${res}`)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
errorNotify("An error occurred while deleting a service", `Error: ${err}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
|
|
||||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||||
if(!res){
|
if (!res) {
|
||||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
okNotify(`Service ${serviceInfo.name} stopped successfully!`, `The service on ${serviceInfo.port} has been stopped!`)
|
||||||
queryClient.invalidateQueries({ queryKey: serviceQueryKey })
|
queryClient.invalidateQueries({ queryKey: serviceQueryKey })
|
||||||
}else{
|
} else {
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`, `Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`, `Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false);
|
setButtonLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
<Space h="sm" />
|
||||||
<LoadingOverlay visible={filtersList.isLoading} />
|
<LoadingOverlay visible={filtersList.isLoading} />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
<Card
|
||||||
<Box>
|
withBorder
|
||||||
<Title order={1}>
|
shadow="md"
|
||||||
<Box className="center-flex">
|
radius="md"
|
||||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
p="xl"
|
||||||
</Box>
|
mb="xl"
|
||||||
</Title>
|
bg="var(--third_color)"
|
||||||
</Box>
|
style={{ borderColor: 'var(--fourth_color)', overflow: "visible" }} // Allows dropdowns to overflow outside
|
||||||
{isMedium?null:<Space h="md" />}
|
>
|
||||||
<Box className='center-flex'>
|
<Group justify="space-between" align="center" wrap={isMedium ? "nowrap" : "wrap"} gap="xl">
|
||||||
<ExceptionWarning service_id={srv} />
|
<Box>
|
||||||
<Space w="sm" />
|
<Group gap="xs" wrap="nowrap" align="center">
|
||||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
<Tooltip label="Go back" position="top">
|
||||||
{serviceInfo.status}
|
<ActionIcon color="cyan" variant="subtle" radius="md" onClick={() => navigate("/nfproxy")}>
|
||||||
</Badge>
|
<FaArrowLeft size={16} />
|
||||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
</ActionIcon>
|
||||||
:{serviceInfo.port}
|
</Tooltip>
|
||||||
</Badge>
|
<MdDoubleArrow size={24} style={{ color: "var(--text-secondary)" }} />
|
||||||
|
<Title order={2} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
<MenuDropDownWithButton>
|
{serviceInfo.name}
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
</Title>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
</Group>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Group gap="sm" mt="md">
|
||||||
<Divider />
|
<ExceptionWarning service_id={srv} />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Badge color={status_color} radius="sm" size="lg" variant="light">{serviceInfo.status}</Badge>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="sm">
|
||||||
</MenuDropDownWithButton>
|
:{serviceInfo.port}
|
||||||
<Space w="md"/>
|
</Badge>
|
||||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
{serviceInfo.target_type === 'tls' && (
|
||||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
<TLSAssociationBadge tlsStreamId={serviceInfo.tls_stream_id} size="lg" />
|
||||||
<FiFileText size="20px" />
|
)}
|
||||||
</ActionIcon>
|
<Badge color={serviceInfo.ip_int.match(regex_ipv4) ? "cyan" : "pink"} radius="sm" size="lg" variant="light">
|
||||||
</Tooltip>
|
{serviceInfo.target_type === 'tls' ? 'decrypted traffic to ' : ''}{serviceInfo.ip_int} on {serviceInfo.proto}
|
||||||
</Box>
|
</Badge>
|
||||||
</Box>
|
</Group>
|
||||||
{isMedium?null:<Space h="md" />}
|
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
|
||||||
<Box className='center-flex'>
|
|
||||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
|
||||||
<Space w="xs" />
|
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
|
||||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
|
||||||
</Box>
|
|
||||||
{isMedium?null:<Space h="xl" />}
|
|
||||||
<Box className='center-flex'>
|
|
||||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
|
||||||
<ActionIcon color="cyan"
|
|
||||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
|
||||||
aria-describedby="tooltip-back-id">
|
|
||||||
<FaArrowLeft size="25px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
|
||||||
disabled={serviceInfo.status === "stop"}
|
|
||||||
aria-describedby="tooltip-stop-id">
|
|
||||||
<FaStop size="20px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
|
||||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
|
||||||
<FaPlay size="20px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider my="xl" />
|
|
||||||
|
|
||||||
{filterCode.data?<>
|
<Box style={{ flexGrow: 1, minWidth: isMedium ? 'auto' : '100%' }}>
|
||||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
<Group gap="xl" justify={isMedium ? "flex-start" : "space-between"}>
|
||||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
<Group gap="xs">
|
||||||
</>: null}
|
<Badge color="yellow" radius="sm" size="lg" variant="light" leftSection={<FaFilter size={12} style={{ marginTop: 2 }} />}>
|
||||||
|
{serviceInfo.blocked_packets}
|
||||||
|
</Badge>
|
||||||
|
<Badge color="orange" radius="sm" size="lg" variant="light" leftSection={<FaPencilAlt size={12} style={{ marginTop: 2 }} />}>
|
||||||
|
{serviceInfo.edited_packets}
|
||||||
|
</Badge>
|
||||||
|
<Badge color="violet" radius="sm" size="lg" variant="light" leftSection={<TbPlugConnected size={12} style={{ marginTop: 2 }} />}>
|
||||||
|
{serviceInfo.n_filters}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
<Group gap="sm" justify="flex-end" style={{ flexGrow: 1 }}>
|
||||||
<Space h="xl" />
|
<Tooltip label={serviceInfo.status === "stop" ? "Cannot stop" : "Stop service"} position="bottom" color="red">
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
<ActionIcon color="red" loading={buttonLoading} onClick={stopService} size="xl" radius="md" variant="light" disabled={serviceInfo.status === "stop"}>
|
||||||
<Space h="xs" />
|
<FaStop size={18} />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
</ActionIcon>
|
||||||
<Space h="xs" />
|
</Tooltip>
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
<Tooltip label={!["stop", "pause"].includes(serviceInfo.status) ? "Cannot start" : "Start service"} position="bottom" color="teal">
|
||||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading} variant="light" disabled={!["stop", "pause"].includes(serviceInfo.status)}>
|
||||||
|
<FaPlay size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Show logs" position="bottom" color="cyan">
|
||||||
|
<ActionIcon color="cyan" size="xl" radius="md" onClick={() => setOpenLogModal(true)} variant="light">
|
||||||
|
<FiFileText size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<MenuDropDownWithButton>
|
||||||
|
<Menu.Label><b>Service Tools</b></Menu.Label>
|
||||||
|
<Menu.Item leftSection={<IoSettingsSharp size={16} />} onClick={() => setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<BiRename size={16} />} onClick={() => setRenameModal(true)}>Change Name</Menu.Item>
|
||||||
|
<Divider />
|
||||||
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
|
<Menu.Item color="red" leftSection={<BsTrashFill size={16} />} onClick={() => setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
|
</MenuDropDownWithButton>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{filterCode.data ? <>
|
||||||
|
<Group justify="center" gap="xs" mt="xl">
|
||||||
|
<ThemeIcon radius="md" size="md" variant="light" color="yellow"><FaPython size={16} /></ThemeIcon>
|
||||||
|
<Title order={3}>Filter code</Title>
|
||||||
|
</Group>
|
||||||
|
<Card withBorder radius="md" p={0} bg="transparent" style={{ borderColor: 'var(--fourth_color)', marginTop: '20px' }}>
|
||||||
|
<CodeHighlight code={filterCode.data} language="python" style={{ backgroundColor: 'transparent' }} />
|
||||||
|
</Card>
|
||||||
|
</> : null}
|
||||||
|
|
||||||
|
{(!filtersList.data || filtersList.data.length == 0) ? (
|
||||||
|
<Stack align="center" gap="xs" py="xl">
|
||||||
|
<Title order={3} ta="center">No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||||
|
<Title order={3} ta="center">Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy' /></Title>
|
||||||
|
<Title order={3} ta="center">Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||||
|
</Stack>
|
||||||
|
) : <Box mt="xs">{filtersList.data?.map((filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name} />)}</Box>
|
||||||
}
|
}
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
onClose={()=>setDeleteModal(false) }
|
onClose={() => setDeleteModal(false)}
|
||||||
action={deleteService}
|
action={deleteService}
|
||||||
opened={deleteModal}
|
opened={deleteModal}
|
||||||
/>
|
/>
|
||||||
<RenameForm
|
<RenameForm
|
||||||
onClose={()=>setRenameModal(false)}
|
onClose={() => setRenameModal(false)}
|
||||||
opened={renameModal}
|
opened={renameModal}
|
||||||
service={serviceInfo}
|
service={serviceInfo}
|
||||||
/>
|
/>
|
||||||
<AddEditService
|
<AddEditService
|
||||||
opened={editModal}
|
opened={editModal}
|
||||||
onClose={()=>setEditModal(false)}
|
onClose={() => setEditModal(false)}
|
||||||
edit={serviceInfo}
|
edit={serviceInfo}
|
||||||
/>
|
/>
|
||||||
<ModalLog
|
<ModalLog
|
||||||
opened={openLogModal}
|
opened={openLogModal}
|
||||||
close={()=>setOpenLogModal(false)}
|
close={() => setOpenLogModal(false)}
|
||||||
title={`Logs for service ${serviceInfo.name}`}
|
title={`Logs for service ${serviceInfo.name}`}
|
||||||
data={logData.join("")}
|
data={logData.join("")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -6,12 +6,12 @@ import ServiceRow from '../../components/NFProxy/ServiceRow';
|
|||||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
import { TbPlugConnected, TbReload, TbCode } from 'react-icons/tb';
|
||||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||||
import { MdUploadFile } from "react-icons/md";
|
import { MdUploadFile } from "react-icons/md";
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import { useFileDialog } from '@mantine/hooks';
|
import { EditFilterModal } from '../../components/NFProxy/EditFilterModal';
|
||||||
import { CodeHighlight } from '@mantine/code-highlight';
|
import { CodeHighlight } from '@mantine/code-highlight';
|
||||||
import { DocsButton } from '../../components/DocsButton';
|
import { DocsButton } from '../../components/DocsButton';
|
||||||
|
|
||||||
@ -24,69 +24,8 @@ export default function NFProxy({ children }: { children: any }) {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
const services = nfproxyServiceQuery()
|
const services = nfproxyServiceQuery()
|
||||||
const fileDialog = useFileDialog({
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
accept: ".py",
|
// File upload logic has been moved to EditFilterModal
|
||||||
multiple: false,
|
|
||||||
resetOnOpen: true,
|
|
||||||
onChange: (files) => {
|
|
||||||
if (files?.length??0 > 0)
|
|
||||||
setFile(files![0])
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const [file, setFile] = useState<File | null>(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: <MdUploadFile size={20} />,
|
|
||||||
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: <MdUploadFile size={20} />,
|
|
||||||
autoClose: 5000,
|
|
||||||
loading: false,
|
|
||||||
})
|
|
||||||
}else{
|
|
||||||
notifications.update({
|
|
||||||
id: notify_id,
|
|
||||||
title: "Code upload failed",
|
|
||||||
message: `Error: ${res}`,
|
|
||||||
color: "red",
|
|
||||||
icon: <MdUploadFile size={20} />,
|
|
||||||
autoClose: 5000,
|
|
||||||
loading: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch( err => {
|
|
||||||
notifications.update({
|
|
||||||
id: notify_id,
|
|
||||||
title: "Code upload failed",
|
|
||||||
message: `Error: ${err}`,
|
|
||||||
color: "red",
|
|
||||||
icon: <MdUploadFile size={20} />,
|
|
||||||
autoClose: 5000,
|
|
||||||
loading: false,
|
|
||||||
})
|
|
||||||
}).finally(()=>{setFile(null)})
|
|
||||||
}
|
|
||||||
}, [file])
|
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if(services.isError)
|
if(services.isError)
|
||||||
@ -103,21 +42,21 @@ export default function NFProxy({ children }: { children: any }) {
|
|||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
{isMedium?"General stats:":null}
|
{isMedium?"General stats:":null}
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:Array.isArray(services.data)?services.data.length:0}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:Array.isArray(services.data)?services.data.reduce((acc, s)=> acc+=s.blocked_packets, 0):0}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:Array.isArray(services.data)?services.data.reduce((acc, s)=> acc+=s.edited_packets, 0):0}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:Array.isArray(services.data)?services.data.reduce((acc, s)=> acc+=s.n_filters, 0):0}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
{ srv?
|
{ srv?
|
||||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
<Tooltip label="Edit python filter code" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={() => setEditorOpen(true)}>
|
||||||
<MdUploadFile size={18} />
|
<TbCode size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||||
@ -136,8 +75,8 @@ export default function NFProxy({ children }: { children: any }) {
|
|||||||
<DocsButton doc="nfproxy" />
|
<DocsButton doc="nfproxy" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Space h="md" />
|
<Space h="xl" />
|
||||||
<Box className="center-flex-row" style={{gap: 20}}>
|
<Box className="center-flex-row" style={{ gap: 12, width: "100%" }}>
|
||||||
{srv?null:<>
|
{srv?null:<>
|
||||||
<LoadingOverlay visible={services.isLoading} />
|
<LoadingOverlay visible={services.isLoading} />
|
||||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||||
@ -167,6 +106,11 @@ export default function NFProxy({ children }: { children: any }) {
|
|||||||
{!srv?
|
{!srv?
|
||||||
<AddEditService opened={open} onClose={closeModal} />:null
|
<AddEditService opened={open} onClose={closeModal} />:null
|
||||||
}
|
}
|
||||||
|
<EditFilterModal
|
||||||
|
opened={editorOpen}
|
||||||
|
onClose={() => setEditorOpen(false)}
|
||||||
|
service={services.data?.find(s => s.service_id === srv)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Stack, Title, Tooltip, Card, Group } from '@mantine/core';
|
||||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||||
import RegexView from '../../components/RegexView';
|
import RegexView from '../../components/RegexView';
|
||||||
import AddNewRegex from '../../components/AddNewRegex';
|
import AddNewRegex from '../../components/AddNewRegex';
|
||||||
@ -8,9 +8,9 @@ import { Badge, Divider, Menu } from '@mantine/core';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||||
import { MdDoubleArrow } from "react-icons/md"
|
import { MdDoubleArrow, MdDownload, MdUpload } from "react-icons/md"
|
||||||
import YesNoModal from '../../components/YesNoModal';
|
import YesNoModal from '../../components/YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, getapi, postapi } from '../../js/utils';
|
||||||
import { BsTrashFill } from 'react-icons/bs';
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi'
|
||||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||||
@ -20,6 +20,7 @@ import { FaArrowLeft } from "react-icons/fa";
|
|||||||
import { VscRegex } from 'react-icons/vsc';
|
import { VscRegex } from 'react-icons/vsc';
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||||
|
import TLSAssociationBadge from '../../components/TLSAssociationBadge';
|
||||||
|
|
||||||
export default function ServiceDetailsNFRegex() {
|
export default function ServiceDetailsNFRegex() {
|
||||||
|
|
||||||
@ -89,88 +90,132 @@ export default function ServiceDetailsNFRegex() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
<Space h="sm" />
|
||||||
<LoadingOverlay visible={regexesList.isLoading} />
|
<LoadingOverlay visible={regexesList.isLoading} />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
<Card
|
||||||
<Box>
|
withBorder
|
||||||
<Title order={1}>
|
shadow="md"
|
||||||
<Box className="center-flex">
|
radius="md"
|
||||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
p="xl"
|
||||||
</Box>
|
mb="xl"
|
||||||
</Title>
|
bg="var(--third_color)"
|
||||||
</Box>
|
style={{ borderColor: 'var(--fourth_color)', overflow: "visible" }} // Allows dropdowns to overflow outside
|
||||||
{isMedium?null:<Space h="md" />}
|
>
|
||||||
<Box className='center-flex'>
|
<Group justify="space-between" align="center" wrap={isMedium ? "nowrap" : "wrap"} gap="xl">
|
||||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
<Box>
|
||||||
{serviceInfo.status}
|
<Group gap="xs" wrap="nowrap" align="center">
|
||||||
</Badge>
|
<Tooltip label="Go back" position="top">
|
||||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
<ActionIcon color="cyan" variant="subtle" radius="md" onClick={() => navigate("/nfregex")}>
|
||||||
:{serviceInfo.port}
|
<FaArrowLeft size={16} />
|
||||||
</Badge>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<MdDoubleArrow size={24} style={{ color: "var(--text-secondary)" }} />
|
||||||
|
<Title order={2} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
|
{serviceInfo.name}
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Group gap="sm" mt="md">
|
||||||
|
<Badge color={status_color} radius="sm" size="lg" variant="light">{serviceInfo.status}</Badge>
|
||||||
|
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="sm">
|
||||||
|
:{serviceInfo.port}
|
||||||
|
</Badge>
|
||||||
|
{serviceInfo.target_type === 'tls' && (
|
||||||
|
<TLSAssociationBadge tlsStreamId={serviceInfo.tls_stream_id} size="lg" />
|
||||||
|
)}
|
||||||
|
<Badge color={serviceInfo.ip_int.match(regex_ipv4) ? "cyan" : "pink"} radius="sm" size="lg" variant="light">
|
||||||
|
{serviceInfo.target_type === 'tls' ? 'decrypted traffic to ' : ''}{serviceInfo.ip_int} on {serviceInfo.proto}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box style={{ flexGrow: 1, minWidth: isMedium ? 'auto' : '100%' }}>
|
||||||
|
<Group gap="xl" justify={isMedium ? "flex-start" : "space-between"}>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Badge color="yellow" radius="sm" size="lg" variant="light" leftSection={<FaFilter size={12} style={{ marginTop: 2 }} />}>
|
||||||
|
{serviceInfo.n_packets}
|
||||||
|
</Badge>
|
||||||
|
<Badge color="violet" radius="sm" size="lg" variant="light" leftSection={<VscRegex size={12} style={{ marginTop: 2 }} />}>
|
||||||
|
{serviceInfo.n_regex}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<MenuDropDownWithButton>
|
<Group gap="sm" justify="flex-end" style={{ flexGrow: 1 }}>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Tooltip label={serviceInfo.status === "stop" ? "Cannot stop" : "Stop service"} position="bottom" color="red">
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<ActionIcon color="red" loading={buttonLoading} onClick={stopService} size="xl" radius="md" variant="light" disabled={serviceInfo.status === "stop"}>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<FaStop size={18} />
|
||||||
<Divider />
|
</ActionIcon>
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
</Tooltip>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Tooltip label={!["stop", "pause"].includes(serviceInfo.status) ? "Cannot start" : "Start service"} position="bottom" color="teal">
|
||||||
</MenuDropDownWithButton>
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading} variant="light" disabled={!["stop", "pause"].includes(serviceInfo.status)}>
|
||||||
</Box>
|
<FaPlay size={18} />
|
||||||
</Box>
|
</ActionIcon>
|
||||||
{isMedium?null:<Space h="md" />}
|
</Tooltip>
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
<MenuDropDownWithButton>
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Menu.Label><b>Service Tools</b></Menu.Label>
|
||||||
<Box className='center-flex'>
|
<Menu.Item leftSection={<IoSettingsSharp size={16} />} onClick={() => setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
<Menu.Item leftSection={<BiRename size={16} />} onClick={() => setRenameModal(true)}>Change Name</Menu.Item>
|
||||||
<Space w="xs" />
|
<Divider />
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
<Menu.Label><b>Rules Management</b></Menu.Label>
|
||||||
|
<Menu.Item leftSection={<MdDownload size={16} />} onClick={() => {
|
||||||
|
getapi(`nfregex/services/${serviceInfo.service_id}/export`).then(data => {
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `firegex_nfregex_${serviceInfo.name}_backup.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
okNotify("Exported", "Regex rules have been exported.");
|
||||||
|
}).catch(err => errorNotify("Export Failed", err.toString()));
|
||||||
|
}}>Export Rules</Menu.Item>
|
||||||
|
<Menu.Item leftSection={<MdUpload size={16} />} onClick={() => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target?.result as string);
|
||||||
|
postapi(`nfregex/services/${serviceInfo.service_id}/import`, data).then(() => {
|
||||||
|
okNotify("Imported", "Regex rules have been imported successfully.");
|
||||||
|
queryClient.invalidateQueries({ queryKey: serviceQueryKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["nfregex-service-regexes", serviceInfo.service_id] });
|
||||||
|
}).catch(err => errorNotify("Import Failed", err.toString()));
|
||||||
|
} catch (err: any) {
|
||||||
|
errorNotify("Invalid JSON", err.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}}>Import Rules</Menu.Item>
|
||||||
|
<Divider />
|
||||||
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
|
<Menu.Item color="red" leftSection={<BsTrashFill size={16} />} onClick={() => setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
|
</MenuDropDownWithButton>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
</Group>
|
||||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
</Card>
|
||||||
</Box>
|
{(!regexesList.data || regexesList.data.length == 0) ? (
|
||||||
{isMedium?null:<Space h="xl" />}
|
<Stack align="center" gap="xl" py="xl">
|
||||||
<Box className='center-flex'>
|
<Title order={3} ta="center">No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||||
<ActionIcon color="cyan"
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||||
aria-describedby="tooltip-back-id">
|
|
||||||
<FaArrowLeft size="25px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="md"/>
|
</Stack>
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
) : (
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
<Grid mt="xs">
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
|
||||||
disabled={serviceInfo.status === "stop"}
|
|
||||||
aria-describedby="tooltip-stop-id">
|
|
||||||
<FaStop size="20px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Space w="md"/>
|
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
|
||||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
|
||||||
<FaPlay size="20px" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Divider my="xl" />
|
|
||||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
|
||||||
<Space h="xl" />
|
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
|
||||||
<Space h="xl" /> <Space h="xl" />
|
|
||||||
<Box className='center-flex'>
|
|
||||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
|
||||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</>:
|
|
||||||
<Grid>
|
|
||||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
|
|||||||
@ -38,11 +38,11 @@ function NFRegex({ children }: { children: any }) {
|
|||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
{isMedium?"General stats:":null}
|
{isMedium?"General stats:":null}
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:Array.isArray(services.data)?services.data.length:0}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:Array.isArray(services.data)?services.data.reduce((acc, s)=> acc+=s.n_packets, 0):0}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:Array.isArray(services.data)?services.data.reduce((acc, s)=> acc+=s.n_regex, 0):0}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
@ -64,8 +64,8 @@ function NFRegex({ children }: { children: any }) {
|
|||||||
<DocsButton doc="nfregex" />
|
<DocsButton doc="nfregex" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Space h="md" />
|
<Space h="xl" />
|
||||||
<Box className="center-flex-row" style={{gap: 20}}>
|
<Box className="center-flex-row" style={{ gap: 12, width: "100%" }}>
|
||||||
{srv?null:<>
|
{srv?null:<>
|
||||||
<LoadingOverlay visible={services.isLoading} />
|
<LoadingOverlay visible={services.isLoading} />
|
||||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||||
|
|||||||
@ -47,8 +47,8 @@ function PortHijack() {
|
|||||||
<DocsButton doc="porthijack" />
|
<DocsButton doc="porthijack" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Space h="md" />
|
<Space h="xl" />
|
||||||
<Box className="center-flex-row" style={{gap: 20}}>
|
<Box className="center-flex-row" style={{ gap: 12, width: "100%" }}>
|
||||||
<LoadingOverlay visible={services.isLoading} />
|
<LoadingOverlay visible={services.isLoading} />
|
||||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||||
<Box className='center-flex-row'>
|
<Box className='center-flex-row'>
|
||||||
|
|||||||
232
frontend/src/pages/TLSDecrypt/index.tsx
Normal file
232
frontend/src/pages/TLSDecrypt/index.tsx
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import { ActionIcon, Box, Button, Checkbox, Code, Collapse, Group, Modal, NumberInput, Paper, Space, Table, Text, TextInput, Textarea, Title, Tooltip, FileButton } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FaPlay, FaStop, FaTrash, FaPencilAlt } from "react-icons/fa";
|
||||||
|
import { MdAdd } from "react-icons/md";
|
||||||
|
import { getapi, postapi, putapi, deleteapi, errorNotify, okNotify } from "../../js/utils";
|
||||||
|
import PortAndInterface from "../../components/PortAndInterface";
|
||||||
|
import { TbShieldLock } from "react-icons/tb";
|
||||||
|
|
||||||
|
export const tlsStreamsQueryKey = ["tls_streams"];
|
||||||
|
|
||||||
|
interface TLSStream {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ip_int: string;
|
||||||
|
port: number;
|
||||||
|
cert: string;
|
||||||
|
key: string;
|
||||||
|
status: string;
|
||||||
|
ssl_port: number;
|
||||||
|
clear_port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StreamFormModal = ({ opened, close, editStream }: { opened: boolean, close: () => void, editStream?: TLSStream | null }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const isEdit = !!editStream;
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
name: "",
|
||||||
|
ip_int: "",
|
||||||
|
port: 443,
|
||||||
|
cert: "",
|
||||||
|
key: ""
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
name: (val) => val.length > 0 ? null : "Name is required",
|
||||||
|
ip_int: (val) => val.length > 0 ? null : "Target IP is required",
|
||||||
|
port: (val) => val > 0 && val <= 65535 ? null : "Invalid port",
|
||||||
|
cert: (val) => val.length > 0 ? null : "Certificate is required",
|
||||||
|
key: (val) => val.length > 0 ? null : "Private Key is required"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened) {
|
||||||
|
if (editStream) {
|
||||||
|
form.setValues({
|
||||||
|
name: editStream.name,
|
||||||
|
ip_int: editStream.ip_int,
|
||||||
|
port: editStream.port,
|
||||||
|
cert: editStream.cert,
|
||||||
|
key: editStream.key
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [opened, editStream?.id]);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (values: typeof form.values) => {
|
||||||
|
return isEdit ? putapi(`tls/streams/${editStream!.id}`, values) : postapi("tls/streams", values);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
okNotify(isEdit ? "Stream updated" : "Stream created", `The TLS stream was successfully ${isEdit ? "updated" : "created"}`);
|
||||||
|
queryClient.invalidateQueries({ queryKey: tlsStreamsQueryKey });
|
||||||
|
form.reset();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
errorNotify(`Error ${isEdit ? "updating" : "creating"} stream`, err.toString());
|
||||||
|
},
|
||||||
|
onSettled: () => setLoading(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFileUpload = (file: File | null, fieldName: string) => {
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const text = e.target?.result as string;
|
||||||
|
form.setFieldValue(fieldName, text);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal opened={opened} onClose={close} title={<Group gap="xs"><TbShieldLock size={20} color="var(--accent-color)" /><Text fw={500}>{isEdit ? "Edit TLS Decrypt Stream" : "Add New TLS Decrypt Stream"}</Text></Group>} centered styles={{ body: { maxHeight: 'calc(100vh - 150px)', overflowY: 'auto' } }}>
|
||||||
|
<form onSubmit={form.onSubmit((v) => { setLoading(true); mutation.mutate(v); })}>
|
||||||
|
<TextInput label="Name" placeholder="My HTTPS Service" {...form.getInputProps("name")} required />
|
||||||
|
<Space h="sm" />
|
||||||
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label="Target" />
|
||||||
|
<Space h="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" mb={4}>
|
||||||
|
<Text size="sm" fw={500}>TLS Certificate (PEM format) <Text span c="red">*</Text></Text>
|
||||||
|
<FileButton onChange={(file) => handleFileUpload(file, 'cert')} accept=".pem,.crt,.cer,.txt,*">
|
||||||
|
{(props) => <Button {...props} size="xs" variant="subtle">Load from file</Button>}
|
||||||
|
</FileButton>
|
||||||
|
</Group>
|
||||||
|
<Textarea placeholder="-----BEGIN CERTIFICATE-----..." minRows={4} styles={{ input: { fontFamily: 'monospace' } }} {...form.getInputProps("cert")} required />
|
||||||
|
|
||||||
|
<Space h="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" mb={4}>
|
||||||
|
<Text size="sm" fw={500}>TLS Private Key (PEM format) <Text span c="red">*</Text></Text>
|
||||||
|
<FileButton onChange={(file) => handleFileUpload(file, 'key')} accept=".pem,.key,.txt,*">
|
||||||
|
{(props) => <Button {...props} size="xs" variant="subtle">Load from file</Button>}
|
||||||
|
</FileButton>
|
||||||
|
</Group>
|
||||||
|
<Textarea placeholder="-----BEGIN PRIVATE KEY-----..." minRows={4} styles={{ input: { fontFamily: 'monospace' } }} {...form.getInputProps("key")} required />
|
||||||
|
<Space h="md" />
|
||||||
|
<Group justify="right">
|
||||||
|
<Button type="submit" loading={loading}>{isEdit ? "Save Changes" : "Create Stream"}</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TLSDecrypt = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const [editStream, setEditStream] = useState<TLSStream | null>(null);
|
||||||
|
|
||||||
|
const { data: streams, isLoading } = useQuery<TLSStream[]>({
|
||||||
|
queryKey: tlsStreamsQueryKey,
|
||||||
|
queryFn: () => getapi("tls/streams")
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionMutation = useMutation({
|
||||||
|
mutationFn: ({ action, id }: { action: string, id: string }) => {
|
||||||
|
if (action === "delete") return deleteapi(`tls/streams/${id}`);
|
||||||
|
return postapi(`tls/streams/${id}/${action}`);
|
||||||
|
},
|
||||||
|
onSuccess: (_, { action }) => {
|
||||||
|
okNotify("Success", `Stream ${action} successful`);
|
||||||
|
queryClient.invalidateQueries({ queryKey: tlsStreamsQueryKey });
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
errorNotify("Error", err.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const openAdd = () => { setEditStream(null); open(); };
|
||||||
|
const openEdit = (stream: TLSStream) => { setEditStream(stream); open(); };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={2}>TLS Decryption Streams</Title>
|
||||||
|
<Button leftSection={<MdAdd />} onClick={openAdd}>Add Stream</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text c="dimmed" mb="xl">
|
||||||
|
Manage central TLS decryption streams. These streams transparently decrypt inbound TLS traffic,
|
||||||
|
forward it to inspection services like NFProxy/NFRegex in cleartext, and re-encrypt the traffic
|
||||||
|
before sending it to the backend.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box p="md" style={{ backgroundColor: 'var(--third_color)', borderRadius: '16px', border: '1px solid var(--fourth_color)', boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Name</Table.Th>
|
||||||
|
<Table.Th>Target</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>Internal SSL Port</Table.Th>
|
||||||
|
<Table.Th>Internal Clear Port</Table.Th>
|
||||||
|
<Table.Th>Actions</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{streams?.map(stream => (
|
||||||
|
<Table.Tr key={stream.id}>
|
||||||
|
<Table.Td>{stream.name}</Table.Td>
|
||||||
|
<Table.Td>{stream.ip_int}:{stream.port}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text c={stream.status === "active" ? "green" : "red"} fw={500}>
|
||||||
|
{stream.status.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td><Code>{stream.ssl_port}</Code></Table.Td>
|
||||||
|
<Table.Td><Code>{stream.clear_port}</Code></Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="xs">
|
||||||
|
{stream.status === "stop" ? (
|
||||||
|
<Tooltip label="Start Stream">
|
||||||
|
<ActionIcon color="green" onClick={() => actionMutation.mutate({ action: "start", id: stream.id })}>
|
||||||
|
<FaPlay />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip label="Stop Stream">
|
||||||
|
<ActionIcon color="orange" onClick={() => actionMutation.mutate({ action: "stop", id: stream.id })}>
|
||||||
|
<FaStop />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip label="Edit Stream">
|
||||||
|
<ActionIcon color="cyan" onClick={() => openEdit(stream)}>
|
||||||
|
<FaPencilAlt />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Delete Stream">
|
||||||
|
<ActionIcon color="red" onClick={() => actionMutation.mutate({ action: "delete", id: stream.id })}>
|
||||||
|
<FaTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
{streams?.length === 0 && (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={6} ta="center">No streams found</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<StreamFormModal opened={opened} close={() => { close(); setEditStream(null); }} editStream={editStream} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TLSDecrypt;
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
|
||||||
import svgrPlugin from 'vite-plugin-svgr';
|
import svgrPlugin from 'vite-plugin-svgr';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), viteTsconfigPaths(), svgrPlugin()],
|
plugins: [react(), svgrPlugin()],
|
||||||
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
87
run.py
87
run.py
@ -185,6 +185,7 @@ def gen_args(args_to_parse: list[str]|None = None):
|
|||||||
parser_config.add_argument('--port', "-p", type=int, required=False, help='Set default port for web service')
|
parser_config.add_argument('--port', "-p", type=int, required=False, help='Set default port for web service')
|
||||||
parser_config.add_argument('--host', required=False, help='Set default host IP address to bind the service to')
|
parser_config.add_argument('--host', required=False, help='Set default host IP address to bind the service to')
|
||||||
parser_config.add_argument('--socket-dir', required=False, type=str, help=f'Listen on socket_dir/firegex.sock instead of TCP (default from config: {config["socket_dir"]})', default=config["socket_dir"])
|
parser_config.add_argument('--socket-dir', required=False, type=str, help=f'Listen on socket_dir/firegex.sock instead of TCP (default from config: {config["socket_dir"]})', default=config["socket_dir"])
|
||||||
|
parser_config.add_argument('--password', required=False, type=str, nargs='?', const='', help='Change the password of the firewall (omit the value to be prompted for it interactively)')
|
||||||
parser_config.add_argument('--show', required=False, action="store_true", help='Show current configuration', default=False)
|
parser_config.add_argument('--show', required=False, action="store_true", help='Show current configuration', default=False)
|
||||||
args = parser.parse_args(args=args_to_parse)
|
args = parser.parse_args(args=args_to_parse)
|
||||||
|
|
||||||
@ -214,14 +215,15 @@ def gen_args(args_to_parse: list[str]|None = None):
|
|||||||
args.threads = multiprocessing.cpu_count()
|
args.threads = multiprocessing.cpu_count()
|
||||||
|
|
||||||
# Use config values as fallback, but allow command line to override
|
# Use config values as fallback, but allow command line to override
|
||||||
if "port" not in args or args.port < 1:
|
if "command" in args and args.command != "config":
|
||||||
args.port = config["port"]
|
if "port" not in args or args.port is None or args.port < 1:
|
||||||
|
args.port = config["port"]
|
||||||
if "host" not in args:
|
|
||||||
args.host = config["host"]
|
if "host" not in args or args.host is None:
|
||||||
|
args.host = config["host"]
|
||||||
if "socket_dir" not in args:
|
|
||||||
args.socket_dir = config["socket_dir"]
|
if "socket_dir" not in args or args.socket_dir is None:
|
||||||
|
args.socket_dir = config["socket_dir"]
|
||||||
|
|
||||||
# Save configuration if values were specified via command line and differ from config
|
# Save configuration if values were specified via command line and differ from config
|
||||||
config_changed = False
|
config_changed = False
|
||||||
@ -873,12 +875,12 @@ def run_standalone():
|
|||||||
puts("Starting as daemon...", color=colors.green)
|
puts("Starting as daemon...", color=colors.green)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Start process in background
|
out_file = open('/tmp/app.log', 'w')
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
chroot_cmd,
|
chroot_cmd,
|
||||||
shell=True,
|
shell=True,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=out_file,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=out_file,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
preexec_fn=os.setsid # Create new session
|
preexec_fn=os.setsid # Create new session
|
||||||
)
|
)
|
||||||
@ -963,6 +965,69 @@ def handle_config_command(args):
|
|||||||
config_changed = True
|
config_changed = True
|
||||||
puts(f"Socket dir set to: {args.socket_dir}", color=colors.green)
|
puts(f"Socket dir set to: {args.socket_dir}", color=colors.green)
|
||||||
|
|
||||||
|
if hasattr(args, 'password') and args.password is not None:
|
||||||
|
new_password = args.password
|
||||||
|
if new_password == "":
|
||||||
|
# No value was passed on the command line: prompt for it interactively
|
||||||
|
# (masked input), so the new password never lands in shell history or `ps`.
|
||||||
|
while True:
|
||||||
|
puts("Insert a new password for firegex: ", end="", color=colors.yellow, is_bold=True, flush=True)
|
||||||
|
new_password = getpass.getpass("")
|
||||||
|
if len(new_password) < 8:
|
||||||
|
puts("The password has to be at least 8 char long", color=colors.red, is_bold=True, flush=True)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
puts("Confirm the password: ", end="", color=colors.yellow, is_bold=True, flush=True)
|
||||||
|
if getpass.getpass("") != new_password:
|
||||||
|
puts("Passwords don't match!", color=colors.red, is_bold=True, flush=True)
|
||||||
|
exit(1)
|
||||||
|
elif len(new_password) < 8:
|
||||||
|
puts("Error: The password has to be at least 8 char long", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
hashed = hash_psw(new_password)
|
||||||
|
|
||||||
|
if g.standalone_mode and os.path.isfile(os.path.join(g.rootfs_path, "execute/db/firegex.db")):
|
||||||
|
import sqlite3
|
||||||
|
db_path = os.path.join(g.rootfs_path, "execute/db/firegex.db")
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
try:
|
||||||
|
conn.execute('INSERT INTO keys_values (key, value) VALUES (?, ?)', ('password', hashed))
|
||||||
|
except Exception:
|
||||||
|
conn.execute('UPDATE keys_values SET value = ? WHERE key = ?', (hashed, 'password'))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
puts("Password changed successfully! It will take effect immediately.", color=colors.green)
|
||||||
|
except Exception as e:
|
||||||
|
puts(f"Error changing password: {e}", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
elif volume_exists():
|
||||||
|
# The hash is passed as a plain argv item (sys.argv[1]) rather than interpolated
|
||||||
|
# into the -c source, so it never needs shell/Python-string escaping.
|
||||||
|
py_code = (
|
||||||
|
"import sqlite3, sys\n"
|
||||||
|
"conn = sqlite3.connect('/execute/db/firegex.db')\n"
|
||||||
|
"hashed = sys.argv[1]\n"
|
||||||
|
"try:\n"
|
||||||
|
" conn.execute('INSERT INTO keys_values (key, value) VALUES (?, ?)', ('password', hashed))\n"
|
||||||
|
"except Exception:\n"
|
||||||
|
" conn.execute('UPDATE keys_values SET value = ? WHERE key = ?', (hashed, 'password'))\n"
|
||||||
|
"conn.commit()\n"
|
||||||
|
"conn.close()\n"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
subprocess.run(["docker", "exec", "firegex", "python3", "-c", py_code, hashed], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
puts("Password changed successfully! It will take effect immediately.", color=colors.green)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
puts("Error: Could not change password. Is Firegex running?", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
puts("Error: Firegex is not running and no standalone data found.", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
config_changed = True
|
||||||
|
|
||||||
if config_changed:
|
if config_changed:
|
||||||
if save_config(config):
|
if save_config(config):
|
||||||
puts(f"Configuration saved to {g.configfile}", color=colors.green)
|
puts(f"Configuration saved to {g.configfile}", color=colors.green)
|
||||||
|
|||||||
@ -121,6 +121,28 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
# Backups never contain the password/secret (see export_db), so importing one back
|
||||||
|
# must not lock the current session out or force a re-login.
|
||||||
|
backup = firegex.export_backup()
|
||||||
|
if backup:
|
||||||
|
puts("Sucessfully exported backup ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't export backup ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
if firegex.import_backup(backup):
|
||||||
|
puts("Sucessfully imported backup ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't import backup ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
firegex3 = FiregexAPI(args.address)
|
||||||
|
if firegex3.login(args.password):
|
||||||
|
puts("Password survived the backup import ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Password was lost/changed after backup import ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
puts("List of available interfaces:", color=colors.yellow)
|
puts("List of available interfaces:", color=colors.yellow)
|
||||||
for interface in firegex.get_interfaces():
|
for interface in firegex.get_interfaces():
|
||||||
puts(
|
puts(
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
from utils.colors import colors, puts, sep
|
from utils.colors import colors, puts, sep
|
||||||
from utils.firegexapi import FiregexAPI
|
from utils.firegexapi import FiregexAPI
|
||||||
from utils.tcpserver import TcpServer
|
from utils.tcpserver import TcpServer
|
||||||
|
from utils.tls_helpers import generate_self_signed_cert_key, tls_connect_send_recv
|
||||||
import argparse
|
import argparse
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
@ -42,6 +43,9 @@ if __name__ == "__main__":
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--verbose", "-V", action="store_true", help="Verbose output", default=False
|
"--verbose", "-V", action="store_true", help="Verbose output", default=False
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tls", "-t", action="store_true", help="Test TLS Decrypt", default=False
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
sep()
|
sep()
|
||||||
@ -458,14 +462,14 @@ if __name__ == "__main__":
|
|||||||
X-TeSt: {secret.decode()}
|
X-TeSt: {secret.decode()}
|
||||||
Content-Length: 15
|
Content-Length: 15
|
||||||
|
|
||||||
A Friendly Body""").replace("\n", "\r\n")
|
A Friendly Body""").replace("\n ", "\n").replace("\n", "\r\n")
|
||||||
|
|
||||||
REQUEST_BODY_TEST = textwrap.dedent(f"""POST / HTTP/1.1
|
REQUEST_BODY_TEST = textwrap.dedent(f"""POST / HTTP/1.1
|
||||||
Host: localhost
|
Host: localhost
|
||||||
X-TeSt: NotTheSecret
|
X-TeSt: NotTheSecret
|
||||||
Content-Length: {len(secret.decode())}
|
Content-Length: {len(secret.decode())}
|
||||||
|
|
||||||
{secret.decode()}""").replace("\n", "\r\n")
|
{secret.decode()}""").replace("\n ", "\n").replace("\n", "\r\n")
|
||||||
|
|
||||||
HTTP_REQUEST_STREAM_TEST = textwrap.dedent(f"""
|
HTTP_REQUEST_STREAM_TEST = textwrap.dedent(f"""
|
||||||
from firegex.nfproxy.models import HttpRequest
|
from firegex.nfproxy.models import HttpRequest
|
||||||
@ -624,14 +628,14 @@ if __name__ == "__main__":
|
|||||||
X-TeSt: {secret.decode()}
|
X-TeSt: {secret.decode()}
|
||||||
Content-Length: 15
|
Content-Length: 15
|
||||||
|
|
||||||
A Friendly Body""").replace("\n", "\r\n")
|
A Friendly Body""").replace("\n ", "\n").replace("\n", "\r\n")
|
||||||
|
|
||||||
RESPONSE_BODY_TEST = textwrap.dedent(f"""HTTP/1.1 200 OK
|
RESPONSE_BODY_TEST = textwrap.dedent(f"""HTTP/1.1 200 OK
|
||||||
Host: localhost
|
Host: localhost
|
||||||
X-TeSt: NotTheSecret
|
X-TeSt: NotTheSecret
|
||||||
Content-Length: {len(secret.decode())}
|
Content-Length: {len(secret.decode())}
|
||||||
|
|
||||||
{secret.decode()}""").replace("\n", "\r\n")
|
{secret.decode()}""").replace("\n ", "\n").replace("\n", "\r\n")
|
||||||
|
|
||||||
HTTP_RESPONSE_STREAM_TEST = textwrap.dedent(f"""
|
HTTP_RESPONSE_STREAM_TEST = textwrap.dedent(f"""
|
||||||
from firegex.nfproxy.models import HttpResponse
|
from firegex.nfproxy.models import HttpResponse
|
||||||
@ -902,9 +906,9 @@ if __name__ == "__main__":
|
|||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
# Change settings
|
# Change settings, including proto (exercises the proto -> l4_proto conversion path)
|
||||||
if firegex.nfproxy_settings_service(
|
if firegex.nfproxy_settings_service(
|
||||||
service_id, 1338, "::dead:beef" if args.ipv6 else "123.123.123.123", True
|
service_id, 1338, "::dead:beef" if args.ipv6 else "123.123.123.123", True, proto="tcp"
|
||||||
):
|
):
|
||||||
srv_updated = firegex.nfproxy_get_service(service_id)
|
srv_updated = firegex.nfproxy_get_service(service_id)
|
||||||
if (
|
if (
|
||||||
@ -912,6 +916,7 @@ if __name__ == "__main__":
|
|||||||
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
|
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
|
||||||
in srv_updated["ip_int"]
|
in srv_updated["ip_int"]
|
||||||
and srv_updated["fail_open"]
|
and srv_updated["fail_open"]
|
||||||
|
and srv_updated["proto"] == "tcp"
|
||||||
):
|
):
|
||||||
puts("Sucessfully changed service settings ✔", color=colors.green)
|
puts("Sucessfully changed service settings ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
@ -924,4 +929,126 @@ if __name__ == "__main__":
|
|||||||
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
|
if args.tls:
|
||||||
|
# Testing a target_type='tls' pyfilter service: the backend must also speak TLS
|
||||||
|
# since nginx's loopback clear_port leg always re-encrypts to the real destination.
|
||||||
|
tls_service_name = f"{args.service_name} TLS"
|
||||||
|
tls_port = args.port + 10
|
||||||
|
|
||||||
|
for ele in firegex.nfproxy_get_services():
|
||||||
|
if ele["name"] == tls_service_name or ele["port"] == tls_port:
|
||||||
|
firegex.nfproxy_delete_service(ele["service_id"])
|
||||||
|
for s in firegex.tls_get_streams():
|
||||||
|
if s["port"] == tls_port:
|
||||||
|
firegex.tls_stop_stream(s["id"])
|
||||||
|
firegex.tls_delete_stream(s["id"])
|
||||||
|
|
||||||
|
backend_cert, backend_key = generate_self_signed_cert_key("127.0.0.1")
|
||||||
|
tls_server = TcpServer(tls_port, ipv6=args.ipv6, tls_cert=backend_cert, tls_key=backend_key)
|
||||||
|
tls_server.start()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
stream_cert, stream_key = generate_self_signed_cert_key("localhost")
|
||||||
|
tls_stream_id = firegex.tls_add_stream(
|
||||||
|
tls_service_name + "_Stream", "::1" if args.ipv6 else "127.0.0.1", tls_port, stream_cert, stream_key
|
||||||
|
)
|
||||||
|
if tls_stream_id:
|
||||||
|
puts(f"Sucessfully created TLS stream {tls_stream_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't create TLS stream ✗", color=colors.red)
|
||||||
|
tls_server.stop()
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
if firegex.tls_start_stream(tls_stream_id):
|
||||||
|
puts("Sucessfully started TLS stream ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't start TLS stream ✗", color=colors.red)
|
||||||
|
tls_server.stop()
|
||||||
|
firegex.tls_delete_stream(tls_stream_id)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
tls_ssl_port = firegex.tls_get_stream(tls_stream_id)["ssl_port"]
|
||||||
|
|
||||||
|
tls_srv_id = firegex.nfproxy_add_service(
|
||||||
|
tls_service_name, tls_port, "tcp", "::1" if args.ipv6 else "127.0.0.1",
|
||||||
|
target_type="tls", tls_stream_id=tls_stream_id
|
||||||
|
)
|
||||||
|
if tls_srv_id:
|
||||||
|
puts(f"Sucessfully created TLS service {tls_srv_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't create TLS service ✗", color=colors.red)
|
||||||
|
tls_server.stop()
|
||||||
|
firegex.tls_delete_stream(tls_stream_id)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
if firegex.nfproxy_start_service(tls_srv_id):
|
||||||
|
puts("Sucessfully started TLS service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't start TLS service ✗", color=colors.red)
|
||||||
|
firegex.nfproxy_delete_service(tls_srv_id)
|
||||||
|
tls_server.stop()
|
||||||
|
firegex.tls_delete_stream(tls_stream_id)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
tls_secret = bytes(secrets.token_hex(16).encode())
|
||||||
|
if firegex.nfproxy_set_code(tls_srv_id, get_vedict_test(tls_secret.decode(), "REJECT")):
|
||||||
|
puts(f"Sucessfully added TLS filter for {str(tls_secret)} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't add TLS filter ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
blocked_response = tls_connect_send_recv(
|
||||||
|
tls_ssl_port, args.ipv6, secrets.token_bytes(40) + tls_secret + secrets.token_bytes(40)
|
||||||
|
)
|
||||||
|
if not blocked_response:
|
||||||
|
puts("The malicious request over TLS was successfully blocked ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: The malicious request over TLS wasn't blocked ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
clean_data = secrets.token_bytes(64)
|
||||||
|
if tls_connect_send_recv(tls_ssl_port, args.ipv6, clean_data) == clean_data:
|
||||||
|
puts("Clean traffic over TLS passed through correctly ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Clean traffic over TLS wasn't echoed back correctly ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Cascade: stopping the TLS stream should stop the dependent service
|
||||||
|
if firegex.tls_stop_stream(tls_stream_id):
|
||||||
|
time.sleep(1)
|
||||||
|
if firegex.nfproxy_get_service(tls_srv_id)["status"] == "stop":
|
||||||
|
puts("TLS stream stop correctly cascaded to stop the dependent service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Dependent service wasn't cascade-stopped ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't stop the TLS stream ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Cascade: starting the dependent service should reactivate the TLS stream
|
||||||
|
if firegex.nfproxy_start_service(tls_srv_id):
|
||||||
|
time.sleep(1)
|
||||||
|
if firegex.tls_get_stream(tls_stream_id)["status"] == "active":
|
||||||
|
puts("Starting the service correctly cascaded to reactivate the TLS stream ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: TLS stream wasn't cascade-reactivated ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't start the TLS service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Deleting a still-referenced TLS stream must be rejected
|
||||||
|
if not firegex.tls_delete_stream(tls_stream_id):
|
||||||
|
puts("Correctly rejected deletion of a TLS stream still in use ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Deleting an in-use TLS stream should have failed ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
firegex.nfproxy_delete_service(tls_srv_id)
|
||||||
|
tls_server.stop()
|
||||||
|
firegex.tls_stop_stream(tls_stream_id)
|
||||||
|
firegex.tls_delete_stream(tls_stream_id)
|
||||||
|
puts("Sucessfully cleaned up TLS test service ✔", color=colors.green)
|
||||||
|
|
||||||
exit_test(0)
|
exit_test(0)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from utils.colors import colors, puts, sep
|
|||||||
from utils.firegexapi import FiregexAPI
|
from utils.firegexapi import FiregexAPI
|
||||||
from utils.tcpserver import TcpServer
|
from utils.tcpserver import TcpServer
|
||||||
from utils.udpserver import UdpServer
|
from utils.udpserver import UdpServer
|
||||||
|
from utils.tls_helpers import generate_self_signed_cert_key, tls_connect_send_recv
|
||||||
import argparse
|
import argparse
|
||||||
import secrets
|
import secrets
|
||||||
import base64
|
import base64
|
||||||
@ -49,14 +50,25 @@ if __name__ == "__main__":
|
|||||||
help="Select the protocol",
|
help="Select the protocol",
|
||||||
default="tcp",
|
default="tcp",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tls", "-t", action="store_true", help="Test TLS Decrypt", default=False
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if args.tls and args.proto != "tcp":
|
||||||
|
puts("TLS is only supported with TCP protocol.", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
sep()
|
sep()
|
||||||
puts("Testing will start on ", color=colors.cyan, end="")
|
puts("Testing will start on ", color=colors.cyan, end="")
|
||||||
puts(f"{args.address}", color=colors.yellow)
|
puts(f"{args.address}", color=colors.yellow)
|
||||||
|
|
||||||
firegex = FiregexAPI(args.address)
|
firegex = FiregexAPI(args.address)
|
||||||
|
|
||||||
|
status = firegex.status()
|
||||||
|
if status.get("status") == "init":
|
||||||
|
firegex.set_password(args.password)
|
||||||
|
|
||||||
# Login
|
# Login
|
||||||
if firegex.login(args.password):
|
if firegex.login(args.password):
|
||||||
puts("Sucessfully logged in ✔", color=colors.green)
|
puts("Sucessfully logged in ✔", color=colors.green)
|
||||||
@ -64,11 +76,17 @@ if __name__ == "__main__":
|
|||||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Create server
|
# Create server. When testing TLS, the backend itself must also speak TLS: nginx's
|
||||||
|
# loopback clear_port leg always re-encrypts before forwarding to the real destination
|
||||||
|
# (this mirrors a CTF challenge service that natively speaks TLS).
|
||||||
|
backend_cert, backend_key = generate_self_signed_cert_key("127.0.0.1") if args.tls else (None, None)
|
||||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||||
args.port, ipv6=args.ipv6
|
args.port, ipv6=args.ipv6, **({"tls_cert": backend_cert, "tls_key": backend_key} if args.tls else {})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tls_stream_id = None
|
||||||
|
tls_ssl_port = None
|
||||||
|
service_id = None
|
||||||
def exit_test(code):
|
def exit_test(code):
|
||||||
if service_id:
|
if service_id:
|
||||||
server.stop()
|
server.stop()
|
||||||
@ -76,7 +94,14 @@ if __name__ == "__main__":
|
|||||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit(1)
|
||||||
|
if tls_stream_id:
|
||||||
|
firegex.tls_stop_stream(tls_stream_id)
|
||||||
|
if firegex.tls_delete_stream(tls_stream_id):
|
||||||
|
puts("Sucessfully deleted TLS stream ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't delete TLS stream ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
exit(code)
|
exit(code)
|
||||||
|
|
||||||
srvs = firegex.nfregex_get_services()
|
srvs = firegex.nfregex_get_services()
|
||||||
@ -84,8 +109,33 @@ if __name__ == "__main__":
|
|||||||
if ele["name"] == args.service_name:
|
if ele["name"] == args.service_name:
|
||||||
firegex.nfregex_delete_service(ele["service_id"])
|
firegex.nfregex_delete_service(ele["service_id"])
|
||||||
|
|
||||||
|
if args.tls:
|
||||||
|
streams = firegex.tls_get_streams()
|
||||||
|
for s in streams:
|
||||||
|
if s["port"] == args.port:
|
||||||
|
firegex.tls_stop_stream(s["id"])
|
||||||
|
firegex.tls_delete_stream(s["id"])
|
||||||
|
|
||||||
|
cert, key = generate_self_signed_cert_key("localhost")
|
||||||
|
tls_stream_id = firegex.tls_add_stream("TLS Stream", "::1" if args.ipv6 else "127.0.0.1", args.port, cert, key)
|
||||||
|
if tls_stream_id:
|
||||||
|
puts(f"Sucessfully created TLS stream {tls_stream_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Failed to create TLS stream ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
if firegex.tls_start_stream(tls_stream_id):
|
||||||
|
puts("Sucessfully started TLS stream ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Failed to start TLS stream ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
tls_ssl_port = firegex.tls_get_stream(tls_stream_id)["ssl_port"]
|
||||||
|
|
||||||
service_id = firegex.nfregex_add_service(
|
service_id = firegex.nfregex_add_service(
|
||||||
args.service_name, args.port, args.proto, "::1" if args.ipv6 else "127.0.0.1"
|
args.service_name, args.port, args.proto, "::1" if args.ipv6 else "127.0.0.1",
|
||||||
|
target_type="tls" if args.tls else "flow",
|
||||||
|
tls_stream_id=tls_stream_id if args.tls else None
|
||||||
)
|
)
|
||||||
if service_id:
|
if service_id:
|
||||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||||
@ -101,8 +151,18 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
server.start()
|
server.start()
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
def send_check(data):
|
||||||
|
"""Sends data and checks it was echoed back unmodified. When testing TLS,
|
||||||
|
routes through a real TLS handshake against the stream's public ssl_port
|
||||||
|
(so the check actually exercises decrypt -> filter -> re-encrypt), instead
|
||||||
|
of talking to the backend directly."""
|
||||||
|
if args.tls:
|
||||||
|
return tls_connect_send_recv(tls_ssl_port, args.ipv6, data) == data
|
||||||
|
return server.sendCheckData(data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if server.sendCheckData(secrets.token_bytes(432)):
|
if send_check(secrets.token_bytes(432)):
|
||||||
puts("Successfully tested first proxy with no regex ✔", color=colors.green)
|
puts("Successfully tested first proxy with no regex ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Data was corrupted ", color=colors.red)
|
puts("Test Failed: Data was corrupted ", color=colors.red)
|
||||||
@ -121,6 +181,8 @@ if __name__ == "__main__":
|
|||||||
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Check if regex is present in the service
|
# Check if regex is present in the service
|
||||||
n_blocked = 0
|
n_blocked = 0
|
||||||
|
|
||||||
@ -136,7 +198,7 @@ if __name__ == "__main__":
|
|||||||
if r["regex"] == secret:
|
if r["regex"] == secret:
|
||||||
# Test the regex
|
# Test the regex
|
||||||
s = regex.upper() if upper else regex
|
s = regex.upper() if upper else regex
|
||||||
if not server.sendCheckData(
|
if not send_check(
|
||||||
secrets.token_bytes(40) + s + secrets.token_bytes(40)
|
secrets.token_bytes(40) + s + secrets.token_bytes(40)
|
||||||
):
|
):
|
||||||
puts(
|
puts(
|
||||||
@ -191,7 +253,7 @@ if __name__ == "__main__":
|
|||||||
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
if server.sendCheckData(
|
if send_check(
|
||||||
secrets.token_bytes(40)
|
secrets.token_bytes(40)
|
||||||
+ base64.b64decode(regex)
|
+ base64.b64decode(regex)
|
||||||
+ secrets.token_bytes(40)
|
+ secrets.token_bytes(40)
|
||||||
@ -342,32 +404,74 @@ if __name__ == "__main__":
|
|||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
# Change settings
|
# Change settings (skipped in --tls mode: the service's own port/ip/proto no longer
|
||||||
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
|
# drive traffic interception once linked to a TLS stream, see resolve_target())
|
||||||
if firegex.nfregex_settings_service(
|
if not args.tls:
|
||||||
service_id,
|
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
|
||||||
1338,
|
if firegex.nfregex_settings_service(
|
||||||
opposite_proto,
|
service_id,
|
||||||
"::dead:beef" if args.ipv6 else "123.123.123.123",
|
1338,
|
||||||
True,
|
"::dead:beef" if args.ipv6 else "123.123.123.123",
|
||||||
):
|
True,
|
||||||
srv_updated = firegex.nfregex_get_service(service_id)
|
proto=opposite_proto,
|
||||||
if (
|
|
||||||
srv_updated["port"] == 1338
|
|
||||||
and srv_updated["proto"] == opposite_proto
|
|
||||||
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
|
|
||||||
in srv_updated["ip_int"]
|
|
||||||
and srv_updated["fail_open"]
|
|
||||||
):
|
):
|
||||||
puts("Sucessfully changed service settings ✔", color=colors.green)
|
srv_updated = firegex.nfregex_get_service(service_id)
|
||||||
|
if (
|
||||||
|
srv_updated["port"] == 1338
|
||||||
|
and srv_updated["proto"] == opposite_proto
|
||||||
|
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
|
||||||
|
in srv_updated["ip_int"]
|
||||||
|
and srv_updated["fail_open"]
|
||||||
|
):
|
||||||
|
puts("Sucessfully changed service settings ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: Service settings weren't updated correctly ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
puts(
|
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
||||||
"Test Failed: Service settings weren't updated correctly ✗",
|
exit_test(1)
|
||||||
color=colors.red,
|
|
||||||
)
|
if args.tls:
|
||||||
|
# Stopping the TLS stream should cascade-stop the dependent service
|
||||||
|
if firegex.tls_stop_stream(tls_stream_id):
|
||||||
|
time.sleep(1)
|
||||||
|
srv_after_stop = firegex.nfregex_get_service(service_id)
|
||||||
|
if srv_after_stop["status"] == "stop":
|
||||||
|
puts("TLS stream stop correctly cascaded to stop the dependent service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Dependent service wasn't cascade-stopped ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't stop the TLS stream ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Starting the dependent service should cascade-reactivate the TLS stream
|
||||||
|
if firegex.nfregex_start_service(service_id):
|
||||||
|
time.sleep(1)
|
||||||
|
stream_after_start = firegex.tls_get_stream(tls_stream_id)
|
||||||
|
if stream_after_start["status"] == "active":
|
||||||
|
puts("Starting the service correctly cascaded to reactivate the TLS stream ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: TLS stream wasn't cascade-reactivated ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
if send_check(secrets.token_bytes(64)):
|
||||||
|
puts("Traffic flows correctly after the cascade reactivation ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Traffic didn't flow correctly after the cascade reactivation ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Deleting a still-referenced TLS stream must be rejected
|
||||||
|
if not firegex.tls_delete_stream(tls_stream_id):
|
||||||
|
puts("Correctly rejected deletion of a TLS stream still in use ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Deleting an in-use TLS stream should have failed ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
exit_test(0)
|
exit_test(0)
|
||||||
|
|||||||
@ -68,6 +68,7 @@ if __name__ == "__main__":
|
|||||||
args.port + 1, ipv6=args.ipv6, proxy_port=args.port
|
args.port + 1, ipv6=args.ipv6, proxy_port=args.port
|
||||||
)
|
)
|
||||||
|
|
||||||
|
service_id = None
|
||||||
def exit_test(code):
|
def exit_test(code):
|
||||||
if service_id:
|
if service_id:
|
||||||
server.stop()
|
server.stop()
|
||||||
@ -75,7 +76,7 @@ if __name__ == "__main__":
|
|||||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit(1)
|
||||||
exit(code)
|
exit(code)
|
||||||
|
|
||||||
srvs = firegex.ph_get_services()
|
srvs = firegex.ph_get_services()
|
||||||
|
|||||||
@ -2,3 +2,6 @@ iperf3
|
|||||||
requests
|
requests
|
||||||
matplotlib
|
matplotlib
|
||||||
numpy
|
numpy
|
||||||
|
cryptography
|
||||||
|
psutil
|
||||||
|
pydantic
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
TMP=$1
|
TMP=$1
|
||||||
PASSWORD=${TMP:=testpassword}
|
if [[ "$TMP" == "--set-pass" ]]; then
|
||||||
|
PASSWORD="testpassword"
|
||||||
|
else
|
||||||
|
PASSWORD=${TMP:-testpassword}
|
||||||
|
fi
|
||||||
ERROR=0
|
ERROR=0
|
||||||
|
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
@ -12,9 +16,17 @@ until curl --output /dev/null --silent --fail http://127.0.0.1:4444/api/status;
|
|||||||
printf '.'
|
printf '.'
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
if [[ "$TMP" == "--set-pass" ]]; then
|
||||||
|
curl -X POST http://127.0.0.1:4444/api/set-password -H "Content-Type: application/json" -d "{\"password\": \"$PASSWORD\"}" -s
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Running standard API test"
|
echo "Running standard API test"
|
||||||
python3 api_test.py -p $PASSWORD || ERROR=1
|
python3 api_test.py -p $PASSWORD || ERROR=1
|
||||||
|
echo "Running TLS Decrypt stream CRUD/edit/cascade test"
|
||||||
|
python3 tls_test.py -p $PASSWORD || ERROR=1
|
||||||
|
echo "Running TLS Decrypt stream CRUD/edit/cascade test ipv6"
|
||||||
|
python3 tls_test.py -p $PASSWORD -6 || ERROR=1
|
||||||
echo "Running Netfilter Regex TCP ipv4"
|
echo "Running Netfilter Regex TCP ipv4"
|
||||||
python3 nfregex_test.py -p $PASSWORD -m tcp || ERROR=1
|
python3 nfregex_test.py -p $PASSWORD -m tcp || ERROR=1
|
||||||
echo "Running Netfilter Regex TCP ipv6"
|
echo "Running Netfilter Regex TCP ipv6"
|
||||||
@ -35,6 +47,15 @@ echo "Running Netfilter Proxy ipv4"
|
|||||||
python3 nfproxy_test.py -p $PASSWORD || ERROR=1
|
python3 nfproxy_test.py -p $PASSWORD || ERROR=1
|
||||||
echo "Running Netfilter Proxy ipv6"
|
echo "Running Netfilter Proxy ipv6"
|
||||||
python3 nfproxy_test.py -p $PASSWORD -6 || ERROR=1
|
python3 nfproxy_test.py -p $PASSWORD -6 || ERROR=1
|
||||||
|
echo "Running Netfilter Proxy ipv4 TLS"
|
||||||
|
python3 nfproxy_test.py -p $PASSWORD --tls || ERROR=1
|
||||||
|
echo "Running Netfilter Proxy ipv6 TLS"
|
||||||
|
python3 nfproxy_test.py -p $PASSWORD -6 --tls || ERROR=1
|
||||||
|
|
||||||
|
echo "Running Netfilter Regex TCP ipv4 TLS"
|
||||||
|
python3 nfregex_test.py -p $PASSWORD -m tcp --tls || ERROR=1
|
||||||
|
echo "Running Netfilter Regex TCP ipv6 TLS"
|
||||||
|
python3 nfregex_test.py -p $PASSWORD -m tcp -6 --tls || ERROR=1
|
||||||
|
|
||||||
exit $ERROR
|
exit $ERROR
|
||||||
|
|
||||||
|
|||||||
186
tests/tls_test.py
Normal file
186
tests/tls_test.py
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from utils.colors import colors, puts, sep
|
||||||
|
from utils.firegexapi import FiregexAPI
|
||||||
|
from utils.tcpserver import TcpServer
|
||||||
|
from utils.tls_helpers import generate_self_signed_cert_key, tls_connect_send_recv
|
||||||
|
import argparse
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--address", "-a", type=str, required=False,
|
||||||
|
help="Address of firegex backend", default="http://127.0.0.1:4444/",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port", "-P", type=int, required=False,
|
||||||
|
help="Base port of the test stream", default=15443,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
sep()
|
||||||
|
puts("Testing TLS stream CRUD/edit/cascade will start on ", color=colors.cyan, end="")
|
||||||
|
puts(f"{args.address}", color=colors.yellow)
|
||||||
|
|
||||||
|
firegex = FiregexAPI(args.address)
|
||||||
|
|
||||||
|
if firegex.login(args.password):
|
||||||
|
puts("Sucessfully logged in ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Unknown response or wrong password ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
def exit_test(code):
|
||||||
|
exit(code)
|
||||||
|
|
||||||
|
host = "::1" if args.ipv6 else "127.0.0.1"
|
||||||
|
port_a = args.port
|
||||||
|
port_b = args.port + 1
|
||||||
|
service_name = "TLS CRUD Test Service"
|
||||||
|
|
||||||
|
# Cleanup leftovers from a previous failed run
|
||||||
|
for ele in firegex.nfregex_get_services():
|
||||||
|
if ele["name"] == service_name:
|
||||||
|
firegex.nfregex_delete_service(ele["service_id"])
|
||||||
|
for s in firegex.tls_get_streams():
|
||||||
|
if s["port"] in (port_a, port_b):
|
||||||
|
firegex.tls_stop_stream(s["id"])
|
||||||
|
firegex.tls_delete_stream(s["id"])
|
||||||
|
|
||||||
|
# 1. Create
|
||||||
|
cert_a, key_a = generate_self_signed_cert_key("localhost")
|
||||||
|
stream_id = firegex.tls_add_stream("TLS CRUD Stream", host, port_a, cert_a, key_a)
|
||||||
|
if stream_id:
|
||||||
|
puts(f"Sucessfully created TLS stream {stream_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't create TLS stream ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# 2. List / Get
|
||||||
|
stream = firegex.tls_get_stream(stream_id)
|
||||||
|
if stream and stream["ip_int"] and stream["port"] == port_a:
|
||||||
|
puts("Sucessfully fetched the stream via GET /tls/streams ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Stream not found in listing ✗", color=colors.red)
|
||||||
|
firegex.tls_delete_stream(stream_id)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# 3. Start + a real TLS handshake against the public ssl_port. The backend behind the
|
||||||
|
# stream must itself speak TLS since nginx's clear_port leg always re-encrypts to it.
|
||||||
|
backend_cert, backend_key = generate_self_signed_cert_key("127.0.0.1")
|
||||||
|
backend = TcpServer(port_a, ipv6=args.ipv6, tls_cert=backend_cert, tls_key=backend_key)
|
||||||
|
backend.start()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if firegex.tls_start_stream(stream_id):
|
||||||
|
puts("Sucessfully started TLS stream ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't start TLS stream ✗", color=colors.red)
|
||||||
|
backend.stop()
|
||||||
|
firegex.tls_delete_stream(stream_id)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
ssl_port = firegex.tls_get_stream(stream_id)["ssl_port"]
|
||||||
|
data = secrets.token_bytes(64)
|
||||||
|
if tls_connect_send_recv(ssl_port, args.ipv6, data) == data:
|
||||||
|
puts("Successfully performed a real TLS handshake and got the decrypted echo back ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: TLS handshake / decrypt round-trip failed ✗", color=colors.red)
|
||||||
|
backend.stop()
|
||||||
|
firegex.tls_stop_stream(stream_id)
|
||||||
|
firegex.tls_delete_stream(stream_id)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# 4. Create a dependent nfregex service linked to the stream
|
||||||
|
service_id = firegex.nfregex_add_service(
|
||||||
|
service_name, port_a, "tcp", host, target_type="tls", tls_stream_id=stream_id
|
||||||
|
)
|
||||||
|
if service_id:
|
||||||
|
puts(f"Sucessfully created dependent nfregex service {service_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't create dependent service ✗", color=colors.red)
|
||||||
|
backend.stop()
|
||||||
|
firegex.tls_stop_stream(stream_id)
|
||||||
|
firegex.tls_delete_stream(stream_id)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# 5. The (fixed) delete-guard must now correctly reject deletion while in use
|
||||||
|
if not firegex.tls_delete_stream(stream_id):
|
||||||
|
puts("Correctly rejected deletion of a TLS stream still in use ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Deleting an in-use TLS stream should have failed ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# 6. Edit: move the stream to a new ip/port/cert and confirm the dependent service follows
|
||||||
|
backend.stop()
|
||||||
|
backend = TcpServer(port_b, ipv6=args.ipv6, tls_cert=backend_cert, tls_key=backend_key)
|
||||||
|
backend.start()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
cert_b, key_b = generate_self_signed_cert_key("localhost")
|
||||||
|
if firegex.tls_edit_stream(stream_id, ip_int=host, port=port_b, cert=cert_b, key=key_b):
|
||||||
|
puts("Sucessfully edited the TLS stream's address/cert ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't edit the TLS stream ✗", color=colors.red)
|
||||||
|
backend.stop()
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
updated_stream = firegex.tls_get_stream(stream_id)
|
||||||
|
updated_service = firegex.nfregex_get_service(service_id)
|
||||||
|
if updated_stream["port"] == port_b and updated_service["port"] == port_b:
|
||||||
|
puts("The dependent service's mirrored ip/port followed the stream edit ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: The dependent service wasn't kept in sync with the stream edit ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
new_ssl_port = updated_stream["ssl_port"]
|
||||||
|
data2 = secrets.token_bytes(64)
|
||||||
|
if tls_connect_send_recv(new_ssl_port, args.ipv6, data2) == data2:
|
||||||
|
puts("TLS decryption still works correctly at the new address/port after the edit ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: TLS decryption broke after editing the stream's address ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# 7. Cascade: stopping the stream should stop the dependent service
|
||||||
|
if firegex.tls_stop_stream(stream_id):
|
||||||
|
time.sleep(1)
|
||||||
|
if firegex.nfregex_get_service(service_id)["status"] == "stop":
|
||||||
|
puts("Stopping the stream correctly cascaded to stop the dependent service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Dependent service wasn't cascade-stopped ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't stop the TLS stream ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# 8. Cascade: starting the dependent service should reactivate the stream
|
||||||
|
if firegex.nfregex_start_service(service_id):
|
||||||
|
time.sleep(1)
|
||||||
|
if firegex.tls_get_stream(stream_id)["status"] == "active":
|
||||||
|
puts("Starting the service correctly cascaded to reactivate the TLS stream ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: TLS stream wasn't cascade-reactivated ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't start the dependent service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
firegex.nfregex_delete_service(service_id)
|
||||||
|
backend.stop()
|
||||||
|
firegex.tls_stop_stream(stream_id)
|
||||||
|
if firegex.tls_delete_stream(stream_id):
|
||||||
|
puts("Sucessfully cleaned up TLS CRUD test ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Couldn't delete the TLS stream during cleanup ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
exit_test(0)
|
||||||
@ -79,6 +79,14 @@ class FiregexAPI:
|
|||||||
def reset(self, delete: bool):
|
def reset(self, delete: bool):
|
||||||
self.s.post(f"{self.address}api/reset", json={"delete":delete})
|
self.s.post(f"{self.address}api/reset", json={"delete":delete})
|
||||||
|
|
||||||
|
def export_backup(self):
|
||||||
|
req = self.s.get(f"{self.address}api/export")
|
||||||
|
return req.json()
|
||||||
|
|
||||||
|
def import_backup(self, backup: dict):
|
||||||
|
req = self.s.post(f"{self.address}api/import", json=backup)
|
||||||
|
return verify(req)
|
||||||
|
|
||||||
def nfregex_get_services(self):
|
def nfregex_get_services(self):
|
||||||
req = self.s.get(f"{self.address}api/nfregex/services")
|
req = self.s.get(f"{self.address}api/nfregex/services")
|
||||||
return req.json()
|
return req.json()
|
||||||
@ -103,10 +111,6 @@ class FiregexAPI:
|
|||||||
req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/rename" , json={"name":newname})
|
req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/rename" , json={"name":newname})
|
||||||
return verify(req)
|
return verify(req)
|
||||||
|
|
||||||
def nfregex_settings_service(self,service_id: str, port: int, proto: str, ip_int: str, fail_open: bool):
|
|
||||||
req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/settings" , json={"port":port, "proto":proto, "ip_int":ip_int, "fail_open":fail_open})
|
|
||||||
return verify(req)
|
|
||||||
|
|
||||||
def nfregex_get_service_regexes(self,service_id: str):
|
def nfregex_get_service_regexes(self,service_id: str):
|
||||||
req = self.s.get(f"{self.address}api/nfregex/services/{service_id}/regexes")
|
req = self.s.get(f"{self.address}api/nfregex/services/{service_id}/regexes")
|
||||||
data = req.json()
|
data = req.json()
|
||||||
@ -131,17 +135,24 @@ class FiregexAPI:
|
|||||||
req = self.s.post(f"{self.address}api/nfregex/regexes/{regex_id}/disable")
|
req = self.s.post(f"{self.address}api/nfregex/regexes/{regex_id}/disable")
|
||||||
return verify(req)
|
return verify(req)
|
||||||
|
|
||||||
def nfregex_add_regex(self, service_id: str, regex: str, mode: str, active: bool, is_case_sensitive: bool):
|
def nfregex_add_regex(self, service_id: str, regex: str, mode: str = "B", active: bool = True, is_case_sensitive: bool = True):
|
||||||
if isinstance(regex, str):
|
if isinstance(regex, str):
|
||||||
regex = regex.encode()
|
regex = regex.encode()
|
||||||
req = self.s.post(f"{self.address}api/nfregex/regexes",
|
req = self.s.post(f"{self.address}api/nfregex/regexes",
|
||||||
json={"service_id": service_id, "regex": base64.b64encode(regex).decode(), "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive})
|
json={"service_id": service_id, "regex": base64.b64encode(regex).decode(), "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive})
|
||||||
return verify(req)
|
return verify(req)
|
||||||
|
|
||||||
def nfregex_add_service(self, name: str, port: int, proto: str, ip_int: str, fail_open: bool = False):
|
def nfregex_add_service(self, name: str, port: int, proto: str, ip_int: str, fail_open: bool = False, target_type: str = "flow", tls_stream_id: str | None = None):
|
||||||
req = self.s.post(f"{self.address}api/nfregex/services" ,
|
payload = {"name": name, "port": port, "proto": proto, "ip_int": ip_int, "fail_open": fail_open, "target_type": target_type, "tls_stream_id": tls_stream_id}
|
||||||
json={"name":name,"port":port, "proto": proto, "ip_int": ip_int, "fail_open": fail_open})
|
req = self.s.post(f"{self.address}api/nfregex/services", json=payload)
|
||||||
return req.json()["service_id"] if verify(req) else False
|
return req.json()["service_id"] if verify(req) else False
|
||||||
|
|
||||||
|
def nfregex_settings_service(self, service_id: str, port: int, ip_int: str, fail_open: bool, proto: str | None = None, target_type: str = "flow", tls_stream_id: str | None = None):
|
||||||
|
payload = {"port": port, "ip_int": ip_int, "fail_open": fail_open, "target_type": target_type, "tls_stream_id": tls_stream_id}
|
||||||
|
if proto is not None:
|
||||||
|
payload["proto"] = proto
|
||||||
|
req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/settings", json=payload)
|
||||||
|
return verify(req)
|
||||||
|
|
||||||
def nfregex_get_metrics(self):
|
def nfregex_get_metrics(self):
|
||||||
req = self.s.get(f"{self.address}api/nfregex/metrics")
|
req = self.s.get(f"{self.address}api/nfregex/metrics")
|
||||||
@ -205,8 +216,11 @@ class FiregexAPI:
|
|||||||
req = self.s.put(f"{self.address}api/nfproxy/services/{service_id}/rename" , json={"name":newname})
|
req = self.s.put(f"{self.address}api/nfproxy/services/{service_id}/rename" , json={"name":newname})
|
||||||
return verify(req)
|
return verify(req)
|
||||||
|
|
||||||
def nfproxy_settings_service(self,service_id: str, port: int, ip_int: str, fail_open: bool):
|
def nfproxy_settings_service(self,service_id: str, port: int, ip_int: str, fail_open: bool, proto: str | None = None):
|
||||||
req = self.s.put(f"{self.address}api/nfproxy/services/{service_id}/settings" , json={"port":port, "ip_int":ip_int, "fail_open":fail_open})
|
payload = {"port":port, "ip_int":ip_int, "fail_open":fail_open}
|
||||||
|
if proto is not None:
|
||||||
|
payload["proto"] = proto
|
||||||
|
req = self.s.put(f"{self.address}api/nfproxy/services/{service_id}/settings" , json=payload)
|
||||||
return verify(req)
|
return verify(req)
|
||||||
|
|
||||||
def nfproxy_get_service_pyfilters(self,service_id: str):
|
def nfproxy_get_service_pyfilters(self,service_id: str):
|
||||||
@ -225,10 +239,10 @@ class FiregexAPI:
|
|||||||
req = self.s.post(f"{self.address}api/nfproxy/services/{service_id}/pyfilters/{filter_name}/disable")
|
req = self.s.post(f"{self.address}api/nfproxy/services/{service_id}/pyfilters/{filter_name}/disable")
|
||||||
return verify(req)
|
return verify(req)
|
||||||
|
|
||||||
def nfproxy_add_service(self, name: str, port: int, proto: str, ip_int: str, fail_open: bool = False):
|
def nfproxy_add_service(self, name: str, port: int, proto: str, ip_int: str, fail_open: bool = False, target_type: str = "flow", tls_stream_id: str | None = None):
|
||||||
req = self.s.post(f"{self.address}api/nfproxy/services" ,
|
payload = {"name": name, "port": port, "proto": proto, "ip_int": ip_int, "fail_open": fail_open, "target_type": target_type, "tls_stream_id": tls_stream_id}
|
||||||
json={"name":name,"port":port, "proto": proto, "ip_int": ip_int, "fail_open": fail_open})
|
req = self.s.post(f"{self.address}api/nfproxy/services", json=payload)
|
||||||
return req.json()["service_id"] if verify(req) else False
|
return req.json()["service_id"] if verify(req) else False
|
||||||
|
|
||||||
def nfproxy_get_code(self, service_id: str):
|
def nfproxy_get_code(self, service_id: str):
|
||||||
req = self.s.get(f"{self.address}api/nfproxy/services/{service_id}/code")
|
req = self.s.get(f"{self.address}api/nfproxy/services/{service_id}/code")
|
||||||
@ -236,4 +250,37 @@ class FiregexAPI:
|
|||||||
|
|
||||||
def nfproxy_set_code(self, service_id: str, code: str):
|
def nfproxy_set_code(self, service_id: str, code: str):
|
||||||
req = self.s.put(f"{self.address}api/nfproxy/services/{service_id}/code", json={"code":code})
|
req = self.s.put(f"{self.address}api/nfproxy/services/{service_id}/code", json={"code":code})
|
||||||
return verify(req)
|
return verify(req)
|
||||||
|
|
||||||
|
#TLS
|
||||||
|
def tls_get_streams(self):
|
||||||
|
req = self.s.get(f"{self.address}api/tls/streams")
|
||||||
|
return req.json()
|
||||||
|
|
||||||
|
def tls_add_stream(self, name: str, ip_int: str, port: int, cert: str, key: str):
|
||||||
|
payload = {"name": name, "ip_int": ip_int, "port": port, "cert": cert, "key": key}
|
||||||
|
req = self.s.post(f"{self.address}api/tls/streams", json=payload)
|
||||||
|
return req.json()["stream_id"] if verify(req) else False
|
||||||
|
|
||||||
|
def tls_delete_stream(self, stream_id: str):
|
||||||
|
req = self.s.delete(f"{self.address}api/tls/streams/{stream_id}")
|
||||||
|
return verify(req)
|
||||||
|
|
||||||
|
def tls_start_stream(self, stream_id: str):
|
||||||
|
req = self.s.post(f"{self.address}api/tls/streams/{stream_id}/start")
|
||||||
|
return verify(req)
|
||||||
|
|
||||||
|
def tls_stop_stream(self, stream_id: str):
|
||||||
|
req = self.s.post(f"{self.address}api/tls/streams/{stream_id}/stop")
|
||||||
|
return verify(req)
|
||||||
|
|
||||||
|
def tls_edit_stream(self, stream_id: str, name: str | None = None, ip_int: str | None = None, port: int | None = None, cert: str | None = None, key: str | None = None):
|
||||||
|
payload = {k: v for k, v in {"name": name, "ip_int": ip_int, "port": port, "cert": cert, "key": key}.items() if v is not None}
|
||||||
|
req = self.s.put(f"{self.address}api/tls/streams/{stream_id}", json=payload)
|
||||||
|
return verify(req)
|
||||||
|
|
||||||
|
def tls_get_stream(self, stream_id: str):
|
||||||
|
for stream in self.tls_get_streams():
|
||||||
|
if stream["id"] == stream_id:
|
||||||
|
return stream
|
||||||
|
return None
|
||||||
@ -1,18 +1,37 @@
|
|||||||
import queue
|
import queue
|
||||||
from multiprocessing import Process, Queue
|
from multiprocessing import Process, Queue
|
||||||
import socket
|
import socket
|
||||||
|
import ssl
|
||||||
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
def _start_tcp_server(port, server_queue: Queue, ipv6, verbose):
|
def _start_tcp_server(port, server_queue: Queue, ipv6, verbose, tls_cert=None, tls_key=None):
|
||||||
sock = socket.socket(
|
sock = socket.socket(
|
||||||
socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM
|
socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM
|
||||||
)
|
)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
||||||
sock.listen(8)
|
sock.listen(8)
|
||||||
|
|
||||||
|
tls_context = None
|
||||||
|
if tls_cert and tls_key:
|
||||||
|
tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".crt", delete=False) as cert_file, \
|
||||||
|
tempfile.NamedTemporaryFile(mode="w", suffix=".key", delete=False) as key_file:
|
||||||
|
cert_file.write(tls_cert)
|
||||||
|
key_file.write(tls_key)
|
||||||
|
cert_path, key_path = cert_file.name, key_file.name
|
||||||
|
tls_context.load_cert_chain(cert_path, key_path)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
connection, address = sock.accept()
|
connection, address = sock.accept()
|
||||||
|
if tls_context:
|
||||||
|
try:
|
||||||
|
connection = tls_context.wrap_socket(connection, server_side=True)
|
||||||
|
except ssl.SSLError:
|
||||||
|
connection.close()
|
||||||
|
continue
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
buf = connection.recv(4096)
|
buf = connection.recv(4096)
|
||||||
@ -40,26 +59,35 @@ def _start_tcp_server(port, server_queue: Queue, ipv6, verbose):
|
|||||||
|
|
||||||
|
|
||||||
class TcpServer:
|
class TcpServer:
|
||||||
def __init__(self, port, ipv6, proxy_port=None, verbose=False):
|
def __init__(self, port, ipv6, proxy_port=None, verbose=False, tls_cert=None, tls_key=None):
|
||||||
self.proxy_port = proxy_port
|
self.proxy_port = proxy_port
|
||||||
self.ipv6 = ipv6
|
self.ipv6 = ipv6
|
||||||
self.port = port
|
self.port = port
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
|
self.tls_cert = tls_cert
|
||||||
|
self.tls_key = tls_key
|
||||||
self._server_data_queue = Queue()
|
self._server_data_queue = Queue()
|
||||||
self._regen_process()
|
self._regen_process()
|
||||||
|
|
||||||
def _regen_process(self):
|
def _regen_process(self):
|
||||||
self.server = Process(
|
self.server = Process(
|
||||||
target=_start_tcp_server,
|
target=_start_tcp_server,
|
||||||
args=[self.port, self._server_data_queue, self.ipv6, self.verbose],
|
args=[self.port, self._server_data_queue, self.ipv6, self.verbose, self.tls_cert, self.tls_key],
|
||||||
)
|
)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.server.start()
|
self.server.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.server.terminate()
|
if self.server and self.server.is_alive():
|
||||||
self.server.join()
|
try:
|
||||||
|
self.server.terminate()
|
||||||
|
self.server.join()
|
||||||
|
import time
|
||||||
|
time.sleep(0.1)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
self.server = None
|
||||||
self._regen_process()
|
self._regen_process()
|
||||||
|
|
||||||
def connect_client(self):
|
def connect_client(self):
|
||||||
|
|||||||
69
tests/utils/tls_helpers.py
Normal file
69
tests/utils/tls_helpers.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import datetime
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
|
||||||
|
def generate_self_signed_cert_key(common_name: str = "127.0.0.1") -> tuple[str, str]:
|
||||||
|
"""Generates a self-signed X.509 certificate and RSA private key in 100% pure Python using cryptography."""
|
||||||
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
subject = issuer = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
|
||||||
|
cert = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(issuer)
|
||||||
|
.public_key(key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
|
||||||
|
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365))
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.sign(key, hashes.SHA256())
|
||||||
|
)
|
||||||
|
key_pem = key.private_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
serialization.NoEncryption(),
|
||||||
|
).decode("utf-8")
|
||||||
|
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||||
|
return cert_pem, key_pem
|
||||||
|
|
||||||
|
def send_decrypted_http_request(port: int, ipv6: bool, path: str = "/secret-tls-check") -> bytes | None:
|
||||||
|
"""Sends a sample HTTP request to a target port and returns received response bytes."""
|
||||||
|
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(2)
|
||||||
|
try:
|
||||||
|
sock.connect(("::1" if ipv6 else "127.0.0.1", port))
|
||||||
|
http_payload = f"GET {path} HTTP/1.1\r\nHost: localhost\r\nUser-Agent: Firegex-TLSTest\r\n\r\n".encode()
|
||||||
|
sock.sendall(http_payload)
|
||||||
|
data = sock.recv(4096)
|
||||||
|
sock.close()
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def tls_connect_send_recv(port: int, ipv6: bool, data: bytes, timeout: float = 3.0) -> bytes | None:
|
||||||
|
"""Performs a real TLS handshake against a stream's public ssl_port (trusting any
|
||||||
|
self-signed cert), sends raw bytes and returns the raw response, or None if the
|
||||||
|
connection was refused/reset/timed out (e.g. because a filter blocked it)."""
|
||||||
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
host = "::1" if ipv6 else "127.0.0.1"
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, port), timeout=timeout) as raw_sock:
|
||||||
|
with ctx.wrap_socket(raw_sock) as tls_sock:
|
||||||
|
tls_sock.sendall(data)
|
||||||
|
tls_sock.settimeout(timeout)
|
||||||
|
try:
|
||||||
|
return tls_sock.recv(65536)
|
||||||
|
except socket.timeout:
|
||||||
|
return b""
|
||||||
|
except (OSError, ssl.SSLError):
|
||||||
|
return None
|
||||||
@ -49,8 +49,15 @@ class UdpServer:
|
|||||||
self.server.start()
|
self.server.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.server.terminate()
|
if self.server:
|
||||||
self.server.join()
|
try:
|
||||||
|
self.server.terminate()
|
||||||
|
self.server.join()
|
||||||
|
import time
|
||||||
|
time.sleep(0.1)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
self.server = None
|
||||||
self._regen_process()
|
self._regen_process()
|
||||||
|
|
||||||
def connect_client(self):
|
def connect_client(self):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user