fps

FPS Protocol And Architecture Specification

Version: 0.11, transcript-bound classified-record transport and adaptive shaper beta snapshot

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

1. Purpose

FPS is an experimental covert datagram transport carried over live TLS cover sessions. The current product adapter, and the first production-facing use case, maps those datagrams to a leased L3 TUN VPN service. 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, ordinary carrier TLS records continue to be forwarded byte-for-byte; FPS inserts separate classified TLS Application Data records for opaque datagram/control payloads and consumes them 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 client work has started through a command-line Kotlin/NDK scaffold; the current build separates protocol core, carrier/datagram core, TUN adapter and Linux runtime targets so future adapters can reuse authenticated carriers without inheriting Linux TUN or ip orchestration.

2. Architecture Baseline

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

Key v5 properties:

Implementation boundary:

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 Transcript-Bound Precheck

Implemented v5 client-auth precheck layout inside the TLS Application Data payload:

server_hint[8] | client_hint[8] | encrypted_capsule | capsule_tag[16]

Hints are derived from the pre-candidate transcript snapshot:

server_hint = H("fps/zero-rtt/client-auth/server-hint/v5" ||
                bidirectional_transcript_snapshot ||
                server_public_key || profile_id)[0..8]

client_hint = H("fps/zero-rtt/client-auth/client-hint/v5" ||
                bidirectional_transcript_snapshot || client_public_key ||
                server_public_key || profile_id)[0..8]

Server-side verification first rejects candidates with a wrong server_hint, then scans the configured UUID-derived client public keys for client_hint, and only then attempts capsule decryption for the likely client. The encrypted client-auth capsule contains protocol version, capabilities, the client ephemeral public key and opaque client payload.

The server accept record uses the same visible shape with server-accept labels:

server_hint[8] | client_hint[8] | encrypted_accept_capsule | accept_tag[16]

The encrypted accept capsule contains protocol version, capabilities, the server ephemeral public key and opaque server payload. Session keys are derived from static-static DH, client-ephemeral/server-static DH, server-ephemeral/client-static DH, ephemeral-ephemeral DH, the bidirectional transcript snapshot and both encrypted auth/accept wire records.

This is not a complete active CPU DoS defense because an attacker that knows the public construction can create plausible server_hint values. Its main value is removing visible public-key-shaped handshake material and avoiding full decrypt work for ordinary random carrier records.

4.2 Server Accept Race Handling

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

Required client behavior: after sending an auth candidate, the client must trial-decrypt plausible peer-direction TLS Application Data records as possible server accepts. If a record does not classify as a valid accept record, the client must forward it byte-for-byte to the browser as cover traffic and keep waiting until the accept arrives or a bounded policy expires. Only after a valid accept does the client treat the carrier as authenticated. Treating the first peer-direction record as mandatory accept is a correctness bug and can break otherwise valid cover sessions.

4.3 Future No-Bootstrap Classifier

The same transcript-bound idea can become a later no-bootstrap FPS record classifier: every visible TLS Application Data record could be tested as either ordinary carrier bytes or an FPS candidate without a prior explicit authentication record. This is a larger future design, not the beta baseline. It must prove:

5. Classified FPS Records

After upgrade, FPS no longer wraps the entire carrier TLS byte stream. Each ordinary carrier TLS record is forwarded byte-for-byte and included in the per-direction transcript. Covert traffic is inserted as a separate TLS Application Data record:

server_hint[8] | client_hint[8] | encrypted_record | record_tag[16]

Hints are derived from direction, profile id, authenticated client/server public keys, session keys, the per-direction carrier transcript snapshot, visible record index and implicit FPS sequence. The AEAD associated data binds the same metadata plus the visible payload length.

Encrypted plaintext contains protocol version, flags, implicit sequence, an opaque datagram/control frame bundle and padding. It does not contain real carrier TLS bytes.

Sequence and nonce discipline:

6. Covert Datagram Transport And TUN Adapter

The protocol core exposes a best-effort opaque datagram transport over authenticated carrier sessions. The transport does not know whether the payload is an IP packet, a control message for a higher-level adapter or a future application-specific payload.

CovertDatagramTransport maintains the generic carrier pool:

The Linux VPN runtime is implemented as TunTunnelAdapter on top of that generic transport:

Duplicate UUID policy:

Opaque datagram fragmentation:

Server-assigned IPv4 leases:

7. Shaper

Current implemented scope:

shaper.record_size_cdf_c2s and shaper.record_size_cdf_s2c buckets are full outer TLS record wire sizes, including the 5-byte TLS record header. A sampled size smaller than the classified-record overhead, or larger than the configured classified-record padding capacity, blocks injection until another scheduling attempt. codec.max_frame_padding also limits classified-record padding in the current Linux relay config. shaper.inter_record_delay_us_cdf_c2s and shaper.inter_record_delay_us_cdf_s2c buckets are inter-record delays in microseconds. Public CDF config uses compact [value, cumulative_probability] pairs, for example [[512, 0.4], [1500, 1.0]].

Adaptive behavior:

Deferred work:

8. Configuration

Config format: JSON parsed through Boost.JSON.

Minimal server-side v5 shape:

{
  "network": {
    "listen": "127.0.0.1:8443",
    "origin": "127.0.0.1:9443",
    "read_buffer_size": 65536,
    "tcp_no_delay": true
  },
  "security": {
    "zero_rtt": {
      "enabled": true,
      "profile_id": "example-origin-v5",
      "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"],
      "version": 5,
      "min_records_before_trial": 1,
      "upgrade_direction": "client_to_server",
      "client_upgrade_delay_ms": 0,
      "client_upgrade_delay_sigma_ms": 0
    }
  },
  "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. security.zero_rtt.version is optional and defaults to 5. When present it must be 5; pre-production wire formats are intentionally not accepted as compatibility modes. security.zero_rtt.client_upgrade_delay_ms defaults to 2000 for client configs and 0 for server configs. The delay is checked when more carrier TLS records arrive after bidirectional Application Data is observed; it is intended for continuous carrier sessions, not as a wall-clock timer that injects FPS bytes into an idle carrier. security.zero_rtt.client_upgrade_delay_sigma_ms defaults to one third of client_upgrade_delay_ms for client configs and 0 for server configs. Each client carrier samples one effective delay when the bidirectional TLS Application Data channel first becomes eligible for upgrade: clamp(client_upgrade_delay_ms + N(0, sigma_ms), 0, 2 * client_upgrade_delay_ms). Set the sigma field to 0 for reproducible tests and packet-capture experiments. FPS still never sends the client auth record before observing TLS Application Data in both carrier directions and the required transcript records.

Optional shaper adaptive fields live under shaper.adaptive:

{
  "shaper": {
    "enabled": true,
    "profile_id": "example-origin-v5",
    "record_size_cdf_c2s": [[512, 0.4], [1500, 1.0]],
    "record_size_cdf_s2c": [[512, 0.4], [1500, 1.0]],
    "inter_record_delay_us_cdf_c2s": [[20000, 0.5], [100000, 1.0]],
    "inter_record_delay_us_cdf_s2c": [[20000, 0.5], [100000, 1.0]],
    "adaptive": {
      "enabled": true,
      "min_records": 16,
      "min_observation_ms": 2000,
      "decay": 0.98,
      "snapshot_interval_ms": 30000
    }
  }
}

Android client profiles use the same inline shaper object and compact CDF arrays when static shaping is configured. Android intentionally rejects shaper.profile_file: generated/imported mobile profiles must remain self-contained. Kotlin validates the JSON shape with Android’s JSON runtime, then passes primitive CDF arrays through JNI; native code constructs the shared platform-neutral Shaper and attaches it to TlsTcpCarrierSession.

network.tcp_no_delay defaults to true and is applied to both accepted and outbound FPS relay TCP sockets. This disables Nagle on the FPS link so shaped classified TLS records are not delayed or coalesced by an implicit TCP policy. Other TCP knobs such as corking, socket buffer sizes, keepalive and quick ACK are intentionally not part of the beta config; batching belongs to the shaper, and platform-specific TCP heuristics can create new fingerprints.

Validation rules:

Lease-management CLI:

Operational status:

Shaper profile export CLI:

Server keypair CLI:

Offline shaper profile tooling:

Client profile CLI:

8.1 Platform Boundary

fps_core is the narrow platform-neutral layer intended for Android reuse: crypto, Zero-RTT, classified-record codec, fps://v1 client profile normalization and generic datagram scheduling. TUN framing/adaptation and the TLS/TCP carrier are explicit opt-in targets above that core. The current Android scaffold adds a headless Kotlin runtime boundary: it parses client JSON and fps://v1 profiles, models carrier probes and split-tunnel allowlists, models the VPN startup state machine, provides live OkHttp HTTPS/WSS probe/keepalive traffic for app-owned carrier sessions, owns the first lease-triggered Android VpnService TUN file descriptor, keeps platform operations behind hooks, and builds an NDK library that reuses FPS native core pieces without linking Linux runtime code.

Linux-specific runtime is separate:

9. Observability

Runtime logs use the project FPS_LOG_* facade. Linux uses Boost.Log behind that facade; Android native code uses an __android_log_print backend at the same macro boundary. 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 frame-bundle/classified-record decode, Zero-RTT candidates and TUN/control payload 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