FPS client setup is centered on generated profiles rather than hand-written key material.
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 beta DNS proxy. A future DNS helper, if added, should be an explicit per-domain route-only tool with system DNS fallback, not a magical global resolver replacement. For now, document and review the small hosts-file change instead.
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.
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 --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
--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.
fps://v1 URI schema.