fps

Client Profiles And Carrier Setup

FPS client setup is centered on generated profiles rather than hand-written key material.

Current Behavior

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.

Supported Onboarding Shapes

FPS supports two equivalent profile transports:

Both are generated by FPS binaries. Shell/Python scripts remain for Docker simulations and Linux routing examples, not for defining the product profile schema.

Profile Contents

A generated client profile contains only information the client cannot safely infer:

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.

Carrier Hostname Mapping

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.

UUID Sharing Policy

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.

CLI Workflow

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.

Docker Notes

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.

Security Notes

Future Work