fps

FPS Protocol And Architecture Specification

Version: 0.7, duplicate UUID replace_old beta increment

Implementation language: C++20, Boost.Asio, Boost.Test, Boost.JSON, Boost.Log and OpenSSL.

1. Purpose

FPS is an experimental hidden L3 TUN tunnel carried over live TLS cover sessions. On the fps_client <-> fps_server link an observer should see a TCP stream made of TLS Application Data records. Real browser/origin TLS bytes are not terminated by FPS; after upgrade they are packed into FPS envelopes and restored before they reach the real TLS endpoints.

The expanded project name, Free Porn Storage, is a deliberate misdirection. It is not descriptive branding; it is meant to make casual discovery and search by unprepared users or classifiers less useful. The protocol and implementation documents should still use the short name, FPS, for technical clarity.

Linux is the active target platform for both client and server. Android remains future work through VpnService; the current code separates reusable protocol core from Linux TUN and ip runtime boundaries.

2. Architecture Baseline

Browser / cover client
        |
        v
fps_client == TLS-application-record-shaped FPS link == fps_server
        |                                                |
   client TUN                                      server TUN
                                                         |
                                                         v
                                                   real HTTPS origin

Key v2 properties:

Carrier origin resolution is an operator concern. For browser-created carrier sessions in beta deployments, the recommended client-side mechanism is a minimal /etc/hosts override that maps the carrier hostname to the local fps_client listener while keeping the browser-visible hostname, SNI and Host header unchanged. FPS does not currently include a DNS proxy or global resolver helper.

3. Wire Rules

Every byte added by FPS on the fps_client <-> fps_server link must be wrapped in an outer TLS record:

+------------+-------------+-------------+------------------+
| type = 23  | version     | length      | opaque bytes     |
+------------+-------------+-------------+------------------+
| 1 byte     | 2 bytes     | 2 bytes     | length bytes     |
+------------+-------------+-------------+------------------+

Rules:

Wire-shape checks:

4. Zero-RTT Authentication

ZeroRttUpgradeEngine uses X25519, HKDF-SHA256 and ChaCha20-Poly1305 through OpenSSL.

Current construction:

Security constraints:

4.1 CPU-Friendly Precheck

Implemented v2 precheck layout inside the TLS Application Data payload:

ephemeral_public[32] | precheck_box[32] | upgrade_ciphertext | upgrade_tag[16]

precheck_box is ChaCha20-Poly1305 over client_id[16], where:

client_id = SHA256(
  "fps/zero-rtt/client-id/v1" ||
  server_public_key ||
  profile_id ||
  client_public_key
)[0..16]

Capsule material is derived from X25519(client_ephemeral, server_static) and the channel binding. Server-side verification performs one ephemeral-static X25519, decrypts the small capsule, looks up one client public key and only then attempts one full upgrade decrypt. This removes allowlist-order trial decrypts from the normal path.

Precheck is not a complete DoS solution. A plausible candidate still costs one X25519 and one small AEAD attempt.

High-priority refactor: the current handshake still exposes a fixed-position 32-byte public-key field at the start of every Zero-RTT candidate. Even when this field is intended to be ephemeral, a public-key-shaped prefix at byte zero is a much easier classifier target than timing analysis; if implementation, randomness or reuse bugs ever make this value stable or biased, repeated FPS sessions from the same client could produce visible distribution peaks in the first TLS Application Data payload bytes. The next wire revision should remove this visible public-key prefix. A candidate design is to reserve only a short opaque lookup hint such as:

hint = H(current_time_bucket || previous_tls_payload_hash ||
         client_public_key || server_public_key)[0..N]

and encrypt the rest of the upgrade capsule into ciphertext. The server can then try a small number of cheap hint matches, optionally through a Bloom filter or a bounded allowlist scan, and run the expensive cryptographic verification only for the likely client. The hint must remain time/window-bound and profile-bound so it does not become a durable plaintext client identifier.

4.2 Server Confirmation Race Handling

The server confirmation envelope is not guaranteed to be the very next TLS record observed by the client after it sends the Zero-RTT upgrade. Browser and origin TCP streams are independent, and ordinary origin-to-browser TLS records can race ahead of the FPS server’s confirmation record.

Required client behavior: after sending an upgrade candidate and deriving tentative session keys, the client must trial-decrypt plausible peer-direction TLS Application Data records as possible server confirmations. If a record does not decrypt as a valid confirmation envelope, the client must forward it byte-for-byte to the browser as cover traffic and keep waiting until the confirmation arrives or a bounded policy expires. Only after a valid confirmation does the client switch that carrier fully into envelope mode. Treating the first peer-direction record as mandatory confirmation is a correctness bug and can break otherwise valid cover sessions.

5. Envelope Mode

After upgrade, each visible FPS record carries an AEAD-encrypted envelope:

Sequence and nonce discipline:

6. Carrier Pool And TUN

SessionManager maintains a carrier pool:

Duplicate UUID policy:

TUN fragmentation:

Server-assigned IPv4 leases:

7. Shaper

Current implemented scope:

Deferred work:

8. Configuration

Config format: JSON parsed through Boost.JSON.

Minimal server-side v2 shape:

{
  "network": {
    "listen": "127.0.0.1:8443",
    "origin": "127.0.0.1:9443",
    "read_buffer_size": 65536
  },
  "security": {
    "zero_rtt": {
      "enabled": true,
      "profile_id": "example-origin-v2",
      "server_private_key_base64": "PASTE_server_private_key_base64_HERE",
      "server_public_key_base64": "PASTE_server_public_key_base64_HERE",
      "allowed_client_uuids": ["123e4567-e89b-42d3-a456-426614174000"],
      "timestamp_window_sec": 30,
      "replay_cache_size": 4096,
      "trial_decrypt_limit": 16,
      "min_records_before_trial": 1,
      "upgrade_direction": "client_to_server"
    }
  },
  "codec": {
    "max_frame_payload": 1280,
    "max_frame_padding": 64,
    "allow_fragmentation": true
  },
  "tun": {
    "enabled": true,
    "name": "fps0",
    "mtu": 1280,
    "max_write_queue_packets": 64,
    "lease_pool": "10.66.0.0/30",
    "server_address": "10.66.0.1",
    "lease_file": "leases.json",
    "client_isolation": true
  },
  "limits": {
    "max_session_write_queue_bytes": 1048576
  },
  "logging": {
    "level": "info"
  },
  "ops": {
    "status_socket": "/run/fps/server.status"
  }
}

Client config uses network.server, client_uuid, server_public_key_base64 and, when desired, tun.auto_configure=true. Server config uses only inline padded RFC4648 base64 fields server_private_key_base64/server_public_key_base64 and allowed_client_uuids.

Validation rules:

Lease-management CLI:

Operational status:

Client profile CLI:

8.1 Platform Boundary

fps_core is the platform-neutral layer intended for future Android reuse: crypto, Zero-RTT/envelope, codec, session/carrier scheduling, TUN framing, lease/control payloads and TUN packet pump do not depend on Linux ip or /dev/net/tun.

Linux-specific runtime is separate:

9. Observability

Runtime logs use the project FPS_LOG_* facade over Boost.Log. Service structs that carry operational counters or state should be annotated with Boost.Describe and logged through the project describe-to-JSON helper instead of repeating every field by hand. The current log sink still emits text fields such as event=session_stats stats={...}, but the structured tail is valid JSON so operators can gradually move from grep to jq-style tooling.

Allowed logs:

Forbidden logs:

10. Testing Baseline

Ordinary non-sudo ctest should cover:

Opt-in sudo/TUN suite should cover:

Quality/safety checks also include clang-20 warning build, ASan/UBSan, Valgrind unit pass, llvm-cov gate and bounded libFuzzer smoke for TLS record parsing, covert/envelope decode, Zero-RTT candidates and TUN/control frame parsing. Product-level Docker simulations cover one-client UDP iperf3, two-client lease routing/spoof-drop, duplicate UUID replace_old behavior and the official Dante SOCKS5 overlay example smoke.

See testing.md.

11. Roadmap

Near productionization gaps:

See beta-status.md and client-profiles.md.

12. Terms