mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 03:41:17 +02:00
175 lines
10 KiB
Markdown
175 lines
10 KiB
Markdown
# T3 Connect
|
|
|
|
T3 Connect uses Clerk for cloud identity. The relay manages environment links,
|
|
credentials for reaching environments, and managed tunnel allocations. After
|
|
bootstrap, clients send application traffic through the environment's tunnel
|
|
hostname; the relay Worker does not proxy their HTTP or WebSocket sessions.
|
|
The one exception is automation webhooks: the relay forwards
|
|
`/v1/hooks/:environmentId/:hookId/:token` to the environment's tunnel so
|
|
senders get a stable URL. It keeps bodies and tokens out of its traces and
|
|
leaves token and signature verification to the environment
|
|
([forwarder](../../infra/relay/src/hooks/HookForwarder.ts)).
|
|
|
|
By default the forwarder stores nothing. An environment can opt in to having
|
|
the relay hold requests while it is offline
|
|
(`hold_webhooks_while_offline` on its link). Only then does the relay store the
|
|
raw request, including the hook token in the path, in a Durable Object for
|
|
that environment, with SQLite storage. The object pushes held requests back
|
|
through the tunnel from its alarm, oldest first, and backs off while the
|
|
environment stays away. When the tunnel reconnects, the environment asks the
|
|
relay to deliver right away. Requests are deleted once the environment
|
|
answers, after 24 hours, or when no user has the environment linked. The relay
|
|
still never checks the token; delivery goes through the same environment route.
|
|
Every forward carries `x-t3-relay-delivery-id`, so a request that reached the
|
|
environment before a timeout and is delivered again later runs once
|
|
([inbox object](../../infra/relay/src/hooks/HookInboxObject.ts)).
|
|
|
|
A Durable Object, not Postgres or Queues, because held requests are write-once,
|
|
read-once bodies of up to 1 MiB that need per-environment order, caps, and
|
|
retry timing. Queues cap messages at 128 KB and cannot hold one environment's
|
|
requests back while it is away.
|
|
|
|
Clerk, deployment, and native authentication setup live in the
|
|
[Connect setup runbook](../operations/connect-setup.md).
|
|
|
|
## The relay is a trusted broker
|
|
|
|
An authenticated cloud user still needs an active environment link. The relay
|
|
asks that environment to mint a one-time bootstrap credential bound to the
|
|
client's DPoP key. The client exchanges it directly with the environment for an
|
|
[environment session](./environment-auth.md). The relay never receives that
|
|
session token, and possessing the bootstrap credential alone does not permit
|
|
redeeming it without the client's private key.
|
|
|
|
Both sides authenticate this exchange. The environment accepts only bounded,
|
|
replay-guarded relay proofs for its own identity, linked user, and requested
|
|
operation. Signed environment responses bind the result to the request nonce;
|
|
mint responses also bind the credential to the client proof key. The relay
|
|
verifies those bindings before returning a credential. This prevents a different
|
|
process behind the tunnel from impersonating the linked environment. The checks
|
|
meet in the
|
|
[environment link service](../../apps/server/src/cloud/CloudLink.ts) and
|
|
[relay connector](../../infra/relay/src/environments/EnvironmentConnector.ts).
|
|
|
|
The relay holds the signing authority for mint requests. DPoP protects an honest
|
|
exchange from credential reuse; it does not make a compromised relay signing
|
|
key harmless. Keep that trust assumption explicit when changing the protocol.
|
|
|
|
Managed tunnels expose only a validated loopback HTTP origin. Link proof checks
|
|
reject forwarded authority headers, and the relay resolves endpoints from its
|
|
own managed allocations rather than a caller-supplied URL. Health and mint
|
|
requests must not follow redirects. These restrictions keep endpoint discovery
|
|
from turning into arbitrary relay egress or exposing another service on the
|
|
environment host.
|
|
|
|
## A link outlives a connector process
|
|
|
|
CLI authorization, desired exposure, and a running connector have different
|
|
lifetimes. Linking can record intent while the server is stopped. Startup
|
|
reconciles that intent. CLI logout removes the stored cloud credential and
|
|
disables exposure without uninstalling the environment's background service.
|
|
|
|
Managed allocations belong to a user/environment pair. Provisioning checkpoints
|
|
external tunnel and DNS resources so retries can reconcile partial work. A
|
|
normal shutdown of a CLI-managed link releases its tunnel to avoid paying for
|
|
an idle resource, retaining the hostname reservation for the next startup.
|
|
It also retains the allocation record so the environment remains "offline"
|
|
rather than becoming "not authorized".
|
|
|
|
Two cases must retain the tunnel across shutdown. A link installed through a
|
|
client has no startup provisioning path and depends on its stored connector
|
|
token. An update handoff immediately starts a replacement server, and replacing
|
|
the tunnel would add routing propagation delay to every update. These exceptions
|
|
belong to [shutdown handling](../../apps/server/src/cloud/CloudLink.ts).
|
|
|
|
Release and unlink claim the allocation generation before deleting external
|
|
resources. A delayed cleanup must not delete a tunnel reused by a concurrent
|
|
restart or relink. Unlink commits authorization revocation before external
|
|
teardown, because a database failure must leave the active link usable. Failed
|
|
teardown retains enough state to retry. See the
|
|
[managed endpoint lifecycle](../../infra/relay/src/environments/ManagedEndpointProvider.ts).
|
|
|
|
## Idle tunnels are reclaimed and recovered
|
|
|
|
Cloudflare bills a tunnel whether or not a connector is attached, so a laptop
|
|
that sleeps with a linked environment leaves a paid tunnel behind. The relay's
|
|
five-minute maintenance job can reclaim those tunnels. `RELAY_TUNNEL_CLEANUP_MODE`
|
|
selects `off`, `dry-run`, or `enabled`, with `off` as the default. The mode is
|
|
read at deploy time, so changing it means a relay deploy, not a variable flip.
|
|
A candidate is a same-stage tunnel that Cloudflare reports down for at least
|
|
five minutes, or one that never connected and is at least an hour old. The
|
|
longer grace for never-connected tunnels covers a pairing still in progress.
|
|
|
|
Cleanup deletes only tunnels whose host has registered recovery. Allocations
|
|
without recovery registration belong to hosts that cannot replace a deleted
|
|
tunnel and are left alone. Allocations with no recorded tunnel ID, or a
|
|
different tunnel ID, are skipped because a provision may own them. A tunnel with
|
|
no allocation row at all is counted as `skippedOrphan` and never deleted: there
|
|
is no row to lock, so a relink that adopts it by name could race the delete.
|
|
Clear those by hand. Each sweep is bounded: at most ten list requests, 100 deletions, a
|
|
two-minute deadline, and an early stop on a Cloudflare rate limit. Each sweep
|
|
starts one budget further along the candidate list, so a block of deletes that
|
|
keep failing cannot starve the tunnels listed after them. See the
|
|
[reaper](../../infra/relay/src/environments/ManagedEndpointReaper.ts).
|
|
|
|
A host registers recovery at startup by sending its tunnel ID and loopback
|
|
origin with a short-lived signature from the environment key. Registration
|
|
touches Cloudflare only when the local host or port changed, and once per
|
|
existing allocation on the first registration after the upgrade because the
|
|
stored origin is empty. First registrations are jittered so an auto-update wave
|
|
does not hit the relay at once. The host stores a confirmed-origin marker with
|
|
the connector config, and a later boot starts the connector before registration
|
|
only when that marker matches the current config and port. If registration
|
|
cannot reach the relay for ten minutes, the host starts its stored config anyway
|
|
and keeps registering in the background until it can reconcile the origin.
|
|
If the connector exits, or `cloudflared` reports repeated tunnel
|
|
rejections, the host asks the relay for a replacement, at most once every two
|
|
minutes. The relay
|
|
provisions under the same allocation, so the hostname and DNS record survive
|
|
and clients keep their bindings. Every mutation on an allocation bumps its
|
|
`generation`, and deletion locks the row at the generation it claimed, so a
|
|
host that reconnects mid-sweep wins.
|
|
|
|
## The relay client follows the server's pin
|
|
|
|
The host runs `cloudflared` pinned by `CLOUDFLARED_VERSION` in
|
|
[`relayClient.ts`](../../packages/shared/src/relayClient.ts), and bumping it there
|
|
(version, URLs, and checksums) is the whole release step. A linked host that
|
|
starts a server with a new pin keeps its connector up on the newest older managed
|
|
release, installs the pinned one in the background, then restarts only the
|
|
connector child on it. If that restart fails to spawn, the host requests recovery
|
|
as it would for an exited connector. A host with no relay client installs one
|
|
first. A `cloudflared` on `PATH` or an explicit `T3CODE_CLOUDFLARED_PATH` is the
|
|
user's choice and is never replaced, since linking asks before downloading.
|
|
|
|
Once the pinned release registers a connection, managed releases older than the
|
|
pin are deleted except the newest of them. That one, and any newer release, can
|
|
belong to another server sharing the same T3 home, such as a rollback after a
|
|
failed update, which could not run again without it.
|
|
|
|
The version comes from running `cloudflared version`, not from the folder name.
|
|
Connectors from before `--no-autoupdate` replaced themselves in place, so a
|
|
managed folder can hold another release. Such a binary still runs as a fallback
|
|
while the pin installs. Every binary needs `CLOUDFLARED_MIN_VERSION`, the oldest
|
|
release that accepts every flag the connector is started with. Raise it whenever
|
|
a new flag is added.
|
|
|
|
## OAuth traps
|
|
|
|
Interactive clients and the headless CLI use the same Clerk application but
|
|
different credentials. The relay accepts both session-template JWTs and CLI
|
|
OAuth tokens; requiring a JWT template for the CLI would reject valid logins.
|
|
The CLI is a public OAuth client using PKCE and stores no client secret.
|
|
|
|
Loopback CLI authorization starts on the hosted `/connect` page so sign-in
|
|
completes before entering Clerk's authorize endpoint. Sending a signed-out
|
|
browser straight to that endpoint loses the authorize parameters during the
|
|
sign-in redirect. The [shared flow](../../packages/shared/src/connectAuth.ts)
|
|
preserves PKCE and state for the loopback callback.
|
|
|
|
SSH and headless sessions use Clerk's OAuth device authorization grant because
|
|
the browser cannot ordinarily reach a listener on the remote machine. The CLI
|
|
polls Clerk's token endpoint directly while the user approves a short code on
|
|
Clerk's hosted device page; the hosted app plays no part and there is no
|
|
redirect URI or PKCE. The grant must be enabled on the CLI OAuth application
|
|
or the device endpoint returns an error before any prompt is shown.
|