FPS client setup is centered on generated profiles rather than hand-written key material.
Profiles configure the current Linux TUN VPN adapter on top of the reusable FPS covert datagram core. The client UUID authenticates the carrier datagram transport; the server-assigned TUN lease is adapter metadata, not a separate identity mechanism.
client_uuid.tun.auto_configure=true lets the Linux client apply the leased address and
MTU without hard-coding a client-side address.fps_server --generate-client-profile emits an importable client JSON profile
or an fps://v1 URI.fps_client --write-config-from-uri writes a 0600 client config without
requiring helper scripts.This is adequate for CLI/Docker beta operators. It is not a consumer installer: administrators still prepare the server config, allowlist one UUID per device and choose the routing or proxy policy.
For a copyable end-to-end Docker flow, use public-beta-quickstart.md. For revocation and rotation procedures, use rotation.md.
FPS supports two equivalent profile transports:
fps://v1/... string
suitable for scripts and future QR/mobile import.Both are generated by FPS binaries. Shell/Python scripts remain for Docker simulations and Linux routing examples, not for defining the product profile schema.
A generated client profile contains only information the client cannot safely infer:
auto_configure preference;ops.status_socket;It does not contain:
Routes and DNS are deployment policy. They remain explicit operator commands or Docker entrypoint environment, not silent side effects of importing a profile.
Production carrier sessions are expected to come from a browser or another ordinary application. The simple supported client-side setup is a local hosts override for the carrier origin hostname:
127.0.0.1 carrier.example.net
Then run fps_client on the local HTTPS carrier port and open the real carrier
URL, for example:
https://carrier.example.net/
wss://carrier.example.net/
This preserves the browser-visible origin name, Host and SNI while routing the
TCP connection to the local FPS client. It avoids the common certificate and
CORS problems caused by opening https://127.0.0.1/... for a site whose
certificate and web policy are written for carrier.example.net.
DNS cannot select a TCP port. If the application insists on default HTTPS, the
local FPS client must listen on 127.0.0.1:443 or the user must open an explicit
URL with the configured port. Add an ::1 hosts entry only when the local FPS
client also listens on IPv6; otherwise IPv6 lookups can bypass the listener.
FPS does not ship a DNS proxy. Use a reviewed hosts-file entry or router DNS override for selected carrier domains.
Do not share one client_uuid across multiple devices. The UUID determines the
client public key, the persistent server lease key and the assigned TUN IPv4
address. Two machines using the same UUID would therefore contend for the same
lease address and can misroute return traffic or collide at the L3/L4 level.
The product policy is one UUID per device/profile. The only supported duplicate
policy is replace_old: a newer active instance for the same UUID supersedes
older carriers for that UUID. Shared/group UUIDs and multi-device round-robin for
one UUID are intentionally not supported.
Implementation detail: fps_client generates a random per-process instance id
at startup and sends it only inside encrypted post-auth control metadata. This
lets the server distinguish “same client opened another carrier” from “another
device reused the same UUID” without adding a config field or exposing the value
in logs/status output.
Generate a raw client UUID:
CLIENT_UUID="$(fps_client --generate-client-uuid)"
Generate JSON:
fps_server --generate-client-profile \
--config server.json \
--client-uuid "$CLIENT_UUID" \
--server-endpoint fps.example.net:8443 \
--client-listen 127.0.0.1:7443 \
--client-status-socket /run/fps/client.status \
--format json \
--output client.json
Generate a URI instead:
fps_server --generate-client-profile \
--config server.json \
--client-uuid "$CLIENT_UUID" \
--server-endpoint fps.example.net:8443 \
--format uri
Decode/import on the client:
fps_client --print-config-from-uri 'fps://v1/...'
fps_client --write-config-from-uri 'fps://v1/...' --output client.json
fps_client --check-config --config client.json
The URI carries URL-safe base64 JSON without padding:
fps://v1/<base64url-json-profile>
A query-string layout would be more readable, but it becomes fragile once optional mobile and platform fields appear. Keeping JSON as the only profile schema makes URI/QR a transport wrapper rather than a second config format. URI import normalizes the decoded JSON and validates the client UUID plus server public key before writing a config file.
When using Docker bind mounts, direct --output writes from a root-running
container create root-owned host files. Prefer host-side redirection or run the
container with the host UID/GID:
docker run --rm -v "$PWD/config:/etc/fps:ro" fps:local \
fps_server --generate-client-profile --config /etc/fps/server.json \
--client-uuid "$CLIENT_UUID" --server-endpoint fps.example.net:8443 \
> client.json
chmod 600 client.json
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/config:/etc/fps" fps:local \
fps_client --write-config-from-uri 'fps://v1/...' \
--output /etc/fps/client.json
Do not write --output /tmp/client.json inside a one-shot docker run --rm
container unless /tmp is bind-mounted. The file is otherwise created inside
the temporary container and is removed with it.
--client-status-socket PATH is explicit because native and Docker deployments
use different runtime paths. Docker examples use /run/fps/*.status mounted
through a named volume so one-shot status containers can query the daemon.
Generated client profiles use the normal randomized upgrade delay sigma. Set
security.zero_rtt.client_upgrade_delay_sigma_ms to 0 manually when a
reproducible packet capture or deterministic integration lab requires a fixed
upgrade moment.
Android profiles can include app-owned carrier entries under carriers. The
first internal Android carrier path is HTTPS GET; max_response_bytes bounds
how much response body the app drains from each keep-alive request before
treating the carrier as unhealthy. This is an Android runtime guard, not a
Linux daemon setting.
Android profiles can also include an inline shaper object using the same
compact CDF arrays as Linux configs. Keep mobile profiles self-contained:
Android intentionally rejects shaper.profile_file.
fps://v1 URI schema.