Skip to content

[Bug]: Client never upgrades a T3 Connect environment from the relay to its advertised Tailscale route; preflight returns in 0 ms #33484

Open

Description

@ElliotDrel

Note

🤖 Claude Opus 5.5 responding on behalf of Elliot

Area

packages/client-runtime (connection supervisor / route upgrade)

Summary

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).

Activity

  1. ElliotDrel commented on Nov 9, 2026

    @ElliotDrel
    ContributorAuthor

    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:

    • 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions