Server-Provided FRPS Token
Summary
Replace the remote-access heartbeat response boolean with a token-bearing contract. When the server wants a managed client to open remote access, the heartbeat response includes the shared FRPS auth token. The client treats a non-empty token as the start signal, passes that token to the transient unit as a systemd credential, and stops FRPC when no token is provided.
Goals
- Make the heartbeat response the source of truth for both remote-access intent and the FRPS auth token needed to satisfy the current upstream FRPS token auth deployment.
- Keep the device runtime API token separate from the FRPS auth token.
- Avoid persisting the FRPS token in client config or identity files.
- Keep the client-owned
frpc.tomltemplate and frpc-native{{ .Envs.FRPS_AUTH_TOKEN }}expansion model. - Keep FRPC lifecycle owned by the
nixstasis-frpctransient systemd unit.
Non-Goals
- Implementing per-device FRPS tokens.
- Adding an FRPS auth plugin or replacing upstream token authentication.
- Changing UI permissions for opening or closing remote access.
- Changing the terminal channel or browser authorization model.
- Persisting the FRPS token on client hosts.
Current Behavior
- The Phoenix heartbeat response includes
remote_access_requested: boolean. - The Go client starts FRPC when
remote_access_requestedis true. - FRPC receives its auth token from client runtime config (
frp.auth_token). - Compose configures FRPS with a single shared
FRPS_AUTH_TOKEN. - The Phoenix
nixstasisservice does not currently receiveFRPS_AUTH_TOKEN.
Proposed Contract
Heartbeat response data changes from:
{
"data": {
"remote_access_requested": true,
"commands": []
}
}
to:
{
"data": {
"remote_access_token": "<shared-frps-token>",
"commands": []
}
}
Semantics:
remote_access_tokennon-empty: start or keep FRPC running with that token.remote_access_tokenomitted: stop FRPC if active; otherwise remain stopped.remote_access_tokenmay decode as an empty string on older or malformed responses; the client treats that the same as omission.remote_access_requestedis removed from the client response contract because no release has shipped with the current branch behavior.
Server Design
- Compose passes
FRPS_AUTH_TOKENto both services:frps: uses it infrps.tomlasauth.token.nixstasis: uses it only to populate authenticated heartbeat responses whendevice.remote_access_requestedis true.
- Add a small helper that resolves the heartbeat FRPS token from
FRPS_AUTH_TOKENonly whendevice.remote_access_requestedis true. - Keep
HeartbeatJSON.show/1focused on rendering response data. It receives or calls the helper result and conditionally includesremote_access_token:
Remote access token rendering cases:
- Absent when
device.remote_access_requestedis false. - Configured
FRPS_AUTH_TOKENwhendevice.remote_access_requestedis true. - If remote access is requested but
FRPS_AUTH_TOKENis missing, the heartbeat should continue returning telemetry/command responses and omit the token. The helper should log a clear error so the UI timeout is diagnosable.
Client Design
transport.PollResponsereplacesRemoteAccessRequested boolwithRemoteAccessToken string.pollOncestarts FRPC whenresp.RemoteAccessToken != "".- Before
Manager.Start,pollOncederives runtime FRP config from the local config and MAC address, then setsfrpConfig.AuthToken = resp.RemoteAccessToken. runtimeFRPConfigderives only dynamic client-owned values, such as FRP proxy name from MAC whenfrp.nameis not configured.pollOncestops FRPC whenresp.RemoteAccessToken == ""and current FRP status is active.Manager.Startcontinues validating that the final FRP config has a non-empty auth token before invokingsystemd-run.Manager.Startcontinues passing the token to the transient unit throughLoadCredential=FRPS_AUTH_TOKEN:<path>; it must not pass the token throughsystemd-run --setenvmetadata.
Security Notes
- The shared FRPS token is exposed to authenticated managed devices only when an operator has requested remote access for that device.
- This preserves the current upstream FRPS token auth model, but shared-token revocation remains all-or-nothing.
- Per-device revocation and token audit are explicitly deferred to a future FRPS auth-plugin feature.
Docs And Contracts Affected
docs/src/client-server-interface.mddocs/src/modules/deployment-compose.mddeploy/compose/scripts/check_runtime_contract.shdeploy/compose/docker-compose.ymldeploy/compose/README.mdpackages/server/README.mdpackages/client/README.mdpackages/client/scripts/mock_api/main.gopackages/client/internal/frp/manager.godocs/src/modules/client-frp-manager.mddocs/src/modules/edge-frp.mddocs/src/runtime-boundaries.mddocs/src/planned-features.md
Validation
- Server tests prove heartbeat omits
remote_access_tokenwhen remote access is false. - Server tests prove heartbeat includes
FRPS_AUTH_TOKENwhen remote access is true and the env var is configured. - Server tests prove heartbeat omits
remote_access_tokenand logs a clear error when remote access is requested butFRPS_AUTH_TOKENis missing. - Client tests prove a non-empty
remote_access_tokenstarts FRPC using that token. - Client tests prove an empty/missing token stops FRPC when active.
- Runtime contract checks prove the Phoenix service receives
FRPS_AUTH_TOKEN. - Client README no longer tells operators to configure a static
frp.auth_tokenfor normal server-requested remote access. - Mock API flags and fixtures use
remote_access_tokenfor client-side testing. - FRP manager comments distinguish non-secret template values passed by
--setenvfrom the secret token passed through systemd credentials. - Docs and module pages no longer describe
remote_access_requestedas the heartbeat response trigger after implementation. - Existing client tests continue to pass with
GOEXPERIMENT=jsonv2 go test -race ./.... - Server precommit checks pass with
mix precommit.
Reconciliation Bookends
- Before implementation, rebase or merge
mainso the completedself-extracting-installerdocs and systemd credential model are present. - During implementation, update server, client, deployment contract, and docs in
the same unit of work so
remote_access_requestedis no longer documented as a heartbeat response field. - Before completion, rerun the affected docs search for
remote_access_requested,remote_access_token, andFRPS_AUTH_TOKENand reconcile any remaining stale references.