Version: 0.7, duplicate UUID replace_old beta increment
Implementation language: C++20, Boost.Asio, Boost.Test, Boost.JSON, Boost.Log and OpenSSL.
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.
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:
security.zero_rtt is the only supported carrier authentication mechanism.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.
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:
23).Wire-shape checks:
tools/capture_tls_wire.sh --port PORT -- COMMAND;tshark is installed, the generated TLS summary should show parseable
TLS records rather than Wireshark falling back to an opaque TCP stream.ZeroRttUpgradeEngine uses X25519, HKDF-SHA256 and ChaCha20-Poly1305 through
OpenSSL.
Current construction:
client_uuid; FPS
deterministically derives the client’s X25519 key pair from that UUID.client_uuid is a per-device/per-profile bearer secret. Shared or group UUIDs
are unsupported because one UUID maps to one client public key and one
persistent TUN lease identity.hash(previous TLS record) and profile id.Security constraints:
client_uuid and server_public_key_base64.server_private_key_base64,
server_public_key_base64 and allowed_client_uuids.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.
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.
After upgrade, each visible FPS record carries an AEAD-encrypted envelope:
inner_tls_bytes: real TLS record bytes from browser/origin;frames: TUN/control frames;padding_size: profile/shaper padding.Sequence and nonce discipline:
SessionManager maintains a carrier pool:
add_carrier_session is called only after Zero-RTT authentication;Duplicate UUID policy:
replace_old: the newer instance supersedes older
carriers for that UUID/lease;fps_client generates a random per-process client_instance_id at startup
and reuses it for all carriers created by that process;TUN fragmentation:
tun_packet remains for packets <= codec.max_frame_payload;tun_packet_fragment;packet_id,
fragment_index, fragment_count and total_size;Server-assigned IPv4 leases:
tun.lease_pool,
tun.server_address and persistent tun.lease_file;control frame
containing client-instance metadata. With a server lease pool, the server
registers the carrier only after that metadata arrives;control frame containing
a tun_lease: version, IPv4 family, prefix length, client IPv4, server IPv4,
network IPv4 and MTU;tun.auto_configure=true, using
ip addr replace <lease>/<prefix> dev <tun> and
ip link set dev <tun> up mtu <mtu>;tun.client_isolation=true by default: the server drops inbound IPv4 TUN
packets addressed to another leased client in the same pool;Current implemented scope:
Deferred work:
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:
tun.enabled=true requires security.zero_rtt.enabled=true;codec.max_frame_payload must be positive;codec.allow_fragmentation=true and TUN,
codec.max_frame_payload must be larger than the fragment header;codec.allow_fragmentation=false, tun.mtu must not exceed
codec.max_frame_payload;tun.lease_pool requires tun.lease_file, and tun.server_address
must be a usable IPv4 address inside the pool.Lease-management CLI:
fps_server --lease-list --config server.json prints JSON summary with pool,
server address, IP leases, public-key fingerprints and allowlist status;fps_server --lease-revoke-client-uuid UUID --config server.json removes the
lease for the UUID-derived client public key; idempotent not_found exits 0;fps_server --lease-prune --config server.json removes leases that are no
longer derived from current allowed_client_uuids;Operational status:
ops.status_socket enables a local UNIX socket;fps_client --status --config client.json and
fps_server --status --config server.json query the configured socket;--status-socket PATH overrides the config path for ad-hoc queries;sessions.last_closed, bounded sessions.recent_closed, auth counters under
auth, envelope counters under envelope, TUN packet/drop counters and
shaper/backpressure counters;auth contains candidate, authenticated, precheck failure, unknown-client,
decrypt failure, replay and confirmation failure counters;envelope contains decode/encode failure, tamper/invalid and
records-decoded/records-encoded counters;session_id, authentication state, close reason
and non-secret direction/component/stage/error names;Client profile CLI:
fps_server --generate-client-profile --config server.json --client-uuid UUID
--server-endpoint HOST:PORT prints a valid fps_client JSON profile;--client-status-socket PATH adds ops.status_socket to that generated
client profile when the operator wants status to work immediately;--format uri prints the same profile as fps://v1/<base64url-json-profile>;--output PATH [--force] writes generated JSON or URI output as secret
material with 0600 permissions; existing files are not overwritten unless
--force is set;fps_client --print-config-from-uri URI decodes an fps://v1 URI back to
client JSON;fps_client --write-config-from-uri URI --output PATH [--force] decodes and
writes client JSON with the same secret-file overwrite rules;client_uuid, server_public_key_base64,
profile id, carrier endpoint, codec settings and client-side TUN
auto-configuration defaults;--generate-client-uuid prints only one raw canonical UUID line;server_private_key_base64,
allowed_client_uuids, lease file paths or server lease-pool internals;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:
fps_linux_runtime contains relay CLI app, Linux TUN open and production
TunRuntime;TunRuntime is injected into the relay app and provides TUN opening plus
no-shell ip execution;VpnService file descriptor and Android network configurator.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:
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.
Near productionization gaps:
fps://v1 profile URI;See beta-status.md and client-profiles.md.
fps_client and fps_server.