fps

FPS Testing And Quality Workflow

The regression baseline is meant to catch changes in TLS passthrough, Zero-RTT upgrade, encrypted envelope mode, carrier-pool TUN scheduling, fragmentation, shaper budgeting, CLI/config behavior and logging safety.

Quick Local Suite

python3 -m py_compile tests/integration/*.py tools/*.py
bash -n tools/*.sh docker/*.sh
cmake -S . -B build
cmake --build build -j 2
ctest --test-dir build --output-on-failure
ctest --test-dir build -L local --output-on-failure

The root CMake project defaults to CMAKE_BUILD_TYPE=Release for single-config generators and uses -O2 -DNDEBUG for GNU/Clang Release builds. Developers who need debug symbols or sanitizer-specific flags should pass -DCMAKE_BUILD_TYPE=Debug, RelWithDebInfo, or explicit CMAKE_CXX_FLAGS.

Real TUN checks require sudo/CAP_NET_ADMIN and stay opt-in:

cmake -S . -B cmake-build-tun -DFPS_ENABLE_TUN_TESTS=ON
cmake --build cmake-build-tun -j 2
sudo -n ctest --test-dir cmake-build-tun -L tun --output-on-failure

Quality And Sanitizers

Use the repeatable non-sudo quality wrapper:

tools/run_quality_checks.sh --all

The wrapper keeps separate build directories and does not run opt-in root/TUN or pcap tests. Modes:

Useful environment variables:

Local non-Docker Python runtime dependencies are pinned in requirements-runtime.txt.

GitHub Actions CI

The repository ships three GitHub Actions workflows:

The ci stage keeps package installation in the repository Dockerfile instead of duplicating Boost/OpenSSL/LLVM package lists in workflow YAML. It is not part of the product runtime image.

Workflow actions use current Node 24 based major versions. GitHub-hosted runners already satisfy this requirement; self-hosted runners must be at least Actions Runner v2.327.1.

The local CI test image can be repeated manually with:

docker build --target ci \
  --build-arg FPS_COMPILER=gcc -t fps:ci-local-gcc .
docker run --rm -e FPS_JOBS=2 fps:ci-local-gcc

If Docker Buildx is available, prefer docker buildx build --load --target ci for the first command; it skips unrelated product stages more reliably.

The CI matrix intentionally excludes root/TUN, pcap and long Docker soak tests. Those remain opt-in operator checks until they are safe and stable enough for a dedicated privileged runner.

Repository operations are intentionally conservative: branch protection keeps main behind pull requests and required CI checks, image publication stays manual, and release artifacts should be scanned for accidental secrets before they are shared.

The publish workflow writes to GitHub Container Registry with only:

permissions:
  contents: read
  packages: write

It publishes explicit tags only. For image_tag=v0.1.0-beta.1, expected tags are:

ghcr.io/OWNER/fps:v0.1.0-beta.1
ghcr.io/OWNER/fps:v0.1.0-beta.1-alpine

The unsuffixed tag points to the Ubuntu image. Alpine is always explicit. When image_tag is omitted, the workflow uses the current short commit SHA in the same two-tag shape. Do not publish latest until public release policy is defined.

Image publication disables Buildx provenance and SBOM attestations (provenance: false, sbom: false) to avoid extra untagged OCI package versions in GHCR. Supply-chain attestations and image signing are future release policy items, not implicit side effects of the publish workflow.

Alpine Docker smoke can be repeated locally with:

FPS_DOCKER_SUDO=1 FPS_DOCKERFILE=Dockerfile.alpine \
  FPS_DOCKER_IMAGE=fps:alpine tools/run_quality_checks.sh --docker

The Docker smoke includes a generated UUID/server-key --check-config pass. That check is required for Alpine release candidates because Alpine/OpenSSL runtime differences can otherwise break UUID-derived Zero-RTT auth while basic --help commands still work.

Docker Product Simulations

One-client TUN UDP simulation:

FPS_DOCKER_SUDO=1 tools/docker_tun_iperf_sim.py --build \
  --duration 10 --bandwidth 250K --length 512

The scenario starts an ephemeral compose stack with fps_client, fps_server and fps_carrier, runs UDP iperf3 between 10.88.0.2 and 10.88.0.1 through FPS TUN, then checks service liveness and non-zero event=session_stats TUN counters.

Dante proxy overlay smoke:

FPS_DOCKER_SUDO=1 tools/docker_socks_smoke.py --build

This starts FPS server/client, WSS carrier, a derivative Dante proxy image and a simple HTTP origin. The client performs a SOCKS5 TCP connect to 10.88.0.1:1080 through TUN and receives HTTP 200 from the origin. The base FPS image is not expected to contain Dante.

Two-client lease-routing smoke:

FPS_DOCKER_SUDO=1 tools/docker_multi_client_sim.py --build

This starts one FPS server and two FPS client containers with different UUIDs. Both clients receive distinct leases, UDP probes run server-to-client and client-to-server, a spoofed source packet is dropped by the server with event=ignored_spoofed_tun_source, and valid leased traffic still works after the spoof attempt.

Duplicate-UUID replacement smoke:

FPS_DOCKER_SUDO=1 tools/docker_duplicate_uuid_sim.py --build

This starts two FPS client containers with the same UUID in a staged flow. The second active instance replaces the first for the shared lease, server logs event=duplicate_client_replaced, status reports duplicate_client_replacements and server-to-client traffic remains functional only through the newer client.

Docker/TUN resilience soak smoke:

FPS_DOCKER_SUDO=1 tools/docker_resilience_soak.py --build \
  --duration 60 --clients 2 --stress-backpressure

This starts one server and two leased clients, runs sustained UDP plus a concurrent TCP/HTTP probe over TUN, verifies server-to-client routing, injects a spoofed source packet, stops and restarts one carrier client, and checks status JSON for expected counters without exposing UUIDs or keys. The optional --with-socks-overlay flag adds the derivative Dante overlay probe. The --stress-backpressure phase records whether write_queue_full was observed; absence of that specific counter is acceptable for routine soak if all services, leases, routing, spoof-drop and recovery checks pass. Make it strict only for manual tuning runs with --require-backpressure-event and --write-queue-bytes.

The same short gate is available through the quality wrapper:

FPS_DOCKER_SUDO=1 tools/run_quality_checks.sh --soak-smoke

For pre-public-beta soak, run the same tool longer and keep artifacts on failure:

FPS_DOCKER_SUDO=1 tools/docker_resilience_soak.py --duration 1800 \
  --clients 2 --with-socks-overlay --stress-backpressure --keep-artifacts

Long soak remains opt-in because it needs Docker, /dev/net/tun, NET_ADMIN and enough wall-clock time to observe reconnect/backpressure behavior.

Two-Host Pre-Release Soak

The local compose soak is useful, but it does not exercise a real machine split. For release candidates, also run a two-host Docker/TUN soak with the server stack on a separate Linux host and clients on the local host. The server side should publish the FPS carrier listener on an externally reachable TLS port such as :443; keep carrier origin and TUN setup inside containers.

The current beta-candidate shape is:

Do not add one-off two-host orchestration scripts to the repository unless they become stable product tooling. Keep the command shape, image tag, duration, throughput/loss summary, spoof-drop result, carrier recovery result and cleanup notes with the release-candidate record.

CTest Labels

Covered Areas

Unit tests cover:

Local integration tests cover:

Opt-in real TUN integration covers:

Fuzz targets:

Coverage artifacts live in cmake-build-coverage/coverage-summary.txt, cmake-build-coverage/coverage-summary.json and cmake-build-coverage/coverage-html/index.html. The script enforces total line and function thresholds through FPS_COVERAGE_MIN_LINES and FPS_COVERAGE_MIN_FUNCTIONS; treat a threshold miss as a quality gate failure unless the threshold or exclusion policy is deliberately changed. Expected low zones: Linux TUN device open in non-sudo mode, plus operational/error branches in tcp_relay_app.cpp and tcp_bridge_session.cpp.

Fuzz artifacts live in cmake-build-fuzz; seed corpora live in tests/fuzz/corpus. The quality script copies seeds into the build directory so libFuzzer does not mutate tracked corpus files during smoke runs.

Wire-Shape Checks

Manual capture utility:

tools/capture_tls_wire.sh --port 8443 --out /tmp/fps-wire.pcap -- COMMAND...

With tshark installed, inspect /tmp/fps-wire.pcap.tls.txt. FPS link traffic should remain parseable as TLS records with Application Data carrying opaque bytes. Wireshark should not lose TLS record boundaries and reinterpret the flow as arbitrary TCP bytes.

Libpcap-based shape check:

python3 tools/is_pcap_looks_like_tls.py /tmp/fps-wire.pcap \
  --port 8443 \
  --require-bidirectional \
  --require-application-data \
  --min-records 4

The check validates TLS record framing and content types only. Timing and size distribution analysis remains out of scope.

Remaining Gaps