You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Client never upgrades a T3 Connect environment from the relay to its advertised Tailscale route; preflight returns in 0 ms #33484
A remote environment that was linked through T3 Connect never moves off the relay to its Tailscale Serve route, even though the server advertises that route and it answers. The relay then drops several times an hour, and every drop disconnects the client. The faster, stabler route is sitting there unused.
Environment
Server: Windows 11 desktop app, 0.1.8-nightly.20261109.4399, Tailscale Serve enabled.
Client: Windows 11 desktop app, same nightly, on the same tailnet with a direct (non-DERP) Tailscale path.
What the client traces show
The environment's spans always carry environment.target.kind: RelayConnectionTarget.
ConnectionDriver.preflight runs every 60 s (for example 13:38:45, 13:39:45, 13:40:45 and 13:41:45Z). Each span takes about 0 ms and has no ConnectionDriver.checkRoute child and no broker.authorize after it, so it never reaches the "ready" path. No BetterRouteAvailable reset follows.
The client can reach the tailnet route. Plain GET https://<host>.<tailnet>.ts.net/.well-known/t3/environment returns 200 in 180–500 ms during the same windows.
In a separate probe from the client every few seconds over about an hour, the median was 38 ms for the tailnet route and 55 ms for the relay route.
Meanwhile the relay session drops with connection.failure.reason: transport and detail "<env> disconnected.": 9 times in about 4 hours (15:53, 16:01, 16:03, 16:07, 16:08×2, 17:02, 17:29Z). Some of these match the host's tunnel log failed to accept QUIC stream: timeout: no recent network activity, while the host's server answered locally in about 10 ms.
Code reading (static, may be wrong)
preflight (connection/driver.ts:220-234) returns "silent" immediately, with no HTTP request, when sendsTokenInClear is true (routes.ts:335-351). That fits a 0 ms span with no child span. A "silent" result also backs the route off exponentially, up to 30 min (supervisor.ts:161-168).
environment.target.kind comes from entry.target, captured once when the supervisor is created (supervisor.ts:284). It stays RelayConnectionTarget whatever route the session actually uses, which makes these traces hard to read.
Expected
When the advertised tailnet route answers, the client should upgrade off the relay, or at least fail over to it when the relay drops.
If the route is deliberately skipped, the preflight span should say why (silent, unprepared, cooldown), and the span should record the active route rather than the entry's original target.
Possibly related: #24516 (the opposite direction: upgrades tear down a healthy socket).
Live confirmation that the session stays on the relay
Checked on the host while the remote client was connected:
Live connections:netstat shows one established connection to 127.0.0.1:3773 from cloudflared (the relay client) and none from tailscaled. tailscale serve status shows / proxy http://127.0.0.1:3773, so a Tailscale session would arrive through tailscaled.
The host advertises the tailnet route: the server's DirectEndpoints.resolve and tailscale.probeTailscaleHttpsEndpoint spans succeed (13 ms probe).
The client can reach it: the remote client gets GET https://<host>.<tailnet>.ts.net/.well-known/t3/environment → 200, median 38 ms over about an hour.
A correction to my code reading above
The preflights repeat at a steady 60 s with no backoff. In checkBetterRoutes (supervisor.ts:385-421), a "silent" result would back the route off from 120 s up to 30 min. A steady 60 s cadence instead fits checkRoute returning "unchecked" with no span (driver.ts:166). That happens when the better-ranked route has no HTTP base URL (routeHttpBaseUrl, routes.ts:68-72), for example a non-Bearer profile, while the tailnet route itself never gets a checkRoute span. Either way, the preflight never reports "ready", so BetterRouteAvailable never fires.
A cheap improvement would be to annotate ConnectionDriver.preflight with the route kind and the result (ready, unprepared, silent or unchecked). Then a trace can show which route blocks the upgrade.
Activity
ElliotDrel commentedon Nov 9, 2026
Note
🤖 Claude Opus 5.5 responding on behalf of Elliot
Live confirmation that the session stays on the relay
Checked on the host while the remote client was connected:
netstatshows one established connection to127.0.0.1:3773fromcloudflared(the relay client) and none fromtailscaled.tailscale serve statusshows/ proxy http://127.0.0.1:3773, so a Tailscale session would arrive throughtailscaled.DirectEndpoints.resolveandtailscale.probeTailscaleHttpsEndpointspans succeed (13 ms probe).GET https://<host>.<tailnet>.ts.net/.well-known/t3/environment→ 200, median 38 ms over about an hour.A correction to my code reading above
The preflights repeat at a steady 60 s with no backoff. In
checkBetterRoutes(supervisor.ts:385-421), a"silent"result would back the route off from 120 s up to 30 min. A steady 60 s cadence instead fitscheckRoutereturning"unchecked"with no span (driver.ts:166). That happens when the better-ranked route has no HTTP base URL (routeHttpBaseUrl,routes.ts:68-72), for example a non-Bearer profile, while the tailnet route itself never gets acheckRoutespan. Either way, the preflight never reports"ready", soBetterRouteAvailablenever fires.A cheap improvement would be to annotate
ConnectionDriver.preflightwith the route kind and the result (ready,unprepared,silentorunchecked). Then a trace can show which route blocks the upgrade.