Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
infra/relay (the form's list has no relay option). The host side is apps/server.
Summary
The relay records each managed endpoint's origin, the host and port pair, in the allocation table. It treats that record as proof that the Cloudflare tunnel serves the same origin. No repair path reads the tunnel's live configuration when the recorded origin matches what the host reports.
When the real ingress on Cloudflare drifts to a different port, the drift is permanent. The tunnel stays healthy and connected. Every request it proxies hits a dead local port. Clients see 502 and origin_unreachable forever, and the documented fix, restarting T3 Code, changes nothing, because the restart takes the same matching-origin path.
We hit this in production on 2026-10-30. The tunnel ingress served http://127.0.0.1:4711, the server listened on 3773, and the allocation recorded 127.0.0.1:3773.
This is the residual gap after #7458. That issue covered the case where the recorded origin differed from the server's port, and the fix added the origin-sync path that repairs it. Here the recorded origin is correct and the actual ingress is not, so the sync never runs.
Steps to reproduce
Minimal, as a test in infra/relay/src/environments/ManagedEndpointProvider.test.ts:
- Give the allocation origin
127.0.0.1:3773, withreadyAtanddnsRecordIdset. - Make the Cloudflare mock report a tunnel whose configuration serves
http://127.0.0.1:4711. - Call
reconcileOriginwith origin127.0.0.1:3773. - It returns
readyand leaves the ingress untouched.
To reach this state on a real environment, one half-completed origin sync is enough. The server moves to a new port, putConfiguration writes that port to Cloudflare, and markReady then loses the generation claim. The allocation keeps the old port while the ingress serves the new one. When the server returns to the old port, every later registration matches the allocation and skips the repair. We believe this is how the production occurrence started, but we could not prove the historical write, so treat this paragraph as reconstruction. The permanent, unrepaired drift is the part we verified live, in the logs below.
Expected behavior
- After a boot registration or a health check, the recorded origin and the tunnel's actual ingress agree.
- When they disagree, the next
reconcileOrigincall repairs the ingress.
Actual behavior
reconcileOriginreturnsreadyon an origin match without reading the tunnel configuration (ManagedEndpointProvider.tsL780-L785).reuseReadyTunnelreuses the tunnel and skips ingress and DNS repair for a matching origin (L647-L651).- The only routine writer of the ingress is a full
provision, and a linked environment that reports the recorded origin never triggers one.
Impact
Blocks work completely. Remote access to the environment stays dead until someone toggles T3 Connect off and on by hand.
Version or commit
Relay production at https://relay.t3.codes, serving host 0.1.6-nightly.20261030.3940. Code links point at 7471a6c2093e7c04851ba06d282b2d709da72d6c on main.
Environment
macOS 27 arm64. T3 Code Nightly desktop 0.1.6-nightly.20261030.3940. cloudflared 2026.10.4, managed by the server. The environment was linked through the desktop UI (Settings, Sharing and access), not the CLI.
Logs or stack traces
Client diagnostics, verbatim:
This device: ok
T3 Connect: ok
Aryan's MacBook Air tunnel: ok
T3 Code on Aryan's MacBook Air: FAILED
Cause: origin_unreachable
HTTP status: 502
Attempts: 3
Route: T3 Connect
App version: 2.2.0
Host version: 0.1.6-nightly.20261030.3940
Error: Relay could not reach the environment endpoint (endpoint_request_failed).
Trace ID: febb511cdc0a14975a2005b275be8c7e
Host-side evidence, collected while the failure was live. The environment ID is e4465974-9b0f-4a39-a8b7-f2c5006f6ebb and the endpoint hostname is prod-6dce4976cc01ffc7.t3coderelay.com.
cloudflared's remote config, read through its metrics port, shows the ingress pointing at the wrong port while the connector is fully healthy:
$ curl -s http://127.0.0.1:51827/config | jq '.config.ingress[0]'
{
"hostname": "prod-6dce4976cc01ffc7.t3coderelay.com",
"service": "http://127.0.0.1:4711",
...
}
$ curl -s http://127.0.0.1:51827/ready
{"status":200,"readyConnections":4,"connectorId":"ae387feb-8433-47d2-98af-c37513794561"}The public endpoint returns 502, the recorded port answers 200, and the ingress port refuses connections:
$ curl -s -o /dev/null -w '%{http_code}\n' https://prod-6dce4976cc01ffc7.t3coderelay.com/.well-known/t3/environment
502
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3773/.well-known/t3/environment
200
$ curl -s http://127.0.0.1:4711/
curl: (7) Failed to connect to 127.0.0.1 port 4711The host's own state was self-consistent. ~/.t3/userdata/server-runtime.json reported origin http://127.0.0.1:3773, and the relay-confirmed origin marker in the secret store recorded 127.0.0.1:3773 as well. Boot registrations succeeded on every start, which means the relay's allocation also recorded 127.0.0.1:3773. A differing record would have triggered the repair path described below.
Root cause
All links are to 7471a6c2093e7c04851ba06d282b2d709da72d6c on main.
reconcileOriginruns on every boot registration. Whenallocation.originequals the origin the host reports, it returnsreadyimmediately (L780-L785). It verifies that the tunnel still exists on Cloudflare (L759-L779), but it never reads the tunnel's configuration.- The repair path runs only when the two origins differ (L787-L846). It writes the new ingress with
putConfigurationand then persists the origin withmarkReady, guarded byWHERE generation = input.generation(ManagedEndpointAllocations.tsL369-L375). IfmarkReadyloses that race,reconcileOriginfails withclaim-lost(L839-L846). Cloudflare then serves a port the allocation does not record. recoverManagedEndpointcallsprovisionwithreuseReadyTunnel: true. For a matching origin,reuseReadyTunnelreturns the tunnel "with only a liveness check and a fresh token, skipping ingress and DNS repair" (L156-L160, L637-L651). The comment states the tunnel "already serves this origin", but nothing verifies that.- A full
provisionrewrites the ingress (L1315-L1374). A linked environment that reports the recorded origin never triggers one. - The relay maps cloudflared's
502and503toorigin_unreachable(endpointFailure.tsL24-L30). The host's reachability check then tells the user to "Restart T3 Code so the tunnel uses the port the server listens on" (ConnectReachability.tsL110-L116). A restart re-runs step 1 and changes nothing. The troubleshooting doc gives the same advice for this failure mode.
Suggested fixes
- In
reconcileOrigin, when the recorded origin matches, also read the tunnel's live configuration and compare the ingressservicewithformatOriginService(input.origin). On mismatch, run the existingputConfigurationandmarkReadypath. The registration already pays for one Cloudflare read, thetunnels.getexistence check, so one more read is cheap. This turns every boot registration into a convergence point. - Optionally, on the host side:
inspectConnectoralready reads cloudflared's/config(ManagedEndpointRuntime.tsL740-L762). When the ingress service differs from the server's own origin, the reachability report could name both ports, or the host could request a relink instead of telling the user to restart.
Workaround
On the host, open Settings, go to Sharing and access, and turn Enable T3 Connect off. Wait until the tunnel count drops, then wait about 30 seconds, and turn it on. The relink runs a full provision, which rewrites the ingress. The endpoint returned 200 right after, and the client check passed.
One caveat: the first relink attempt, immediately after the off toggle, failed at stage configure-tunnel with trace ID fb747b2da0ab9e340e40b7dc77d8c068. It looked like a Cloudflare-side race on the just-released tunnel. A retry 30 seconds later succeeded.
Possibly related: #8844 reports the same client symptom, endpoint_request_failed, with the cause undiagnosed. I am not claiming it is this bug.
Activity