Skip to content

[Bug]: Desktop shows Tailscale HTTPS as on and offers it for pairing when the Serve URL reaches a different T3 Code server #35512

Closed

Description

@coygeek

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

  1. Save this test as apps/desktop/src/backend/zzrepro-foreign-serve.test.ts. It calls the production resolveTailscaleAdvertisedEndpoints with a Tailscale status in the format of tailscale status --json and an HTTP client that answers the probe with the descriptor of another T3 Code server:
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import { HttpClient, HttpClientResponse } from "effect/http";

import { resolveTailscaleAdvertisedEndpoints } from "./tailscaleEndpointProvider.ts";

const otherServerDescriptor = {
  environmentId: "environment-of-the-other-server",
  label: "Other T3 server",
  platform: { os: "linux", arch: "x64" },
  serverVersion: "0.1.9",
  capabilities: { repositoryIdentity: true },
};

describe("foreign Serve", () => {
  it.effect("advertises Tailscale HTTPS when Serve fronts another T3 server", () =>
    Effect.gen(function* () {
      const requests: string[] = [];
      const endpoints = yield* resolveTailscaleAdvertisedEndpoints({
        port: 3773,
        serveEnabled: true,
        servePort: 443,
        networkInterfaces: {
          tailscale0: [{ address: "100.64.0.10", family: "IPv4", internal: false }],
        },
        statusJson: JSON.stringify({
          Self: {
            DNSName: "host-a.tailnet-name.ts.net.",
            TailscaleIPs: ["100.64.0.10"],
            CapMap: { https: null },
          },
        }),
      }).pipe(
        Effect.provideService(
          HttpClient.HttpClient,
          HttpClient.make((request) => {
            requests.push(request.url);
            return Effect.succeed(
              HttpClientResponse.fromWeb(
                request,
                new Response(JSON.stringify(otherServerDescriptor), {
                  status: 200,
                  headers: { "content-type": "application/json" },
                }),
              ),
            );
          }),
        ),
      );
      const https = endpoints.find((endpoint) => endpoint.label === "Tailscale HTTPS");
      console.log(JSON.stringify({ requests, https }));
      assert.equal(https?.status, "available");
    }),
  );
});
  1. Run vp test run apps/desktop/src/backend/zzrepro-foreign-serve.test.ts --silent=false from the repository root, then delete the file.

Deterministic: the endpoint was available on each of the 2 runs made. The full desktop app, a real tailscaled, and a second device were not used; the server-side "left off" behavior and the Settings and pairing UI are source-traced.

Expected behavior

The Tailscale HTTPS endpoint is reported as available, and offered for pairing, only when the Serve URL reaches this desktop's own backend. The repository already treats a different environment behind the same Serve port as a failure elsewhere: t3 pair --tailscale compares environmentId and stops with "Tailscale Serve on HTTPS port 443 already fronts a different T3 Code server" (apps/server/src/cli/pair.ts:123-130, :390-394, :421-425), and the probe's own comment says "only a T3 descriptor proves this server" (packages/tailscale/src/tailscale.ts:721). docs/user/remote-access.md:169-172 describes Tailscale HTTPS as a route to this desktop.

Actual behavior

With Tailscale HTTPS enabled in the desktop app, apps/desktop/src/backend/tailscaleEndpointProvider.ts marks the MagicDNS endpoint https://<machine>.<tailnet>.ts.net/ as available, hosted-HTTPS compatible, with the description "HTTPS endpoint served by Tailscale Serve.", whenever /.well-known/t3/environment at that URL returns any valid T3 Code descriptor. It does not compare the descriptor's environmentId with the desktop's own backend. When Tailscale Serve's root on the configured port proxies to another T3 Code server on the same machine, the desktop therefore reports Tailscale HTTPS as working for its own backend and offers that URL as a pairing target, although the URL reaches the other server. A run of the production provider with a foreign descriptor (below) returned "status":"available". Source-only for the rest of the path: since #35042 the desktop's backend deliberately leaves Serve off in exactly this state and logs Tailscale Serve left off; its port already serves another server or app (apps/server/src/server.ts:949-953), while Settings → Sharing & access shows the Tailscale HTTPS switch checked (apps/web/src/components/settings/ConnectionsSettings.tsx:1012) and the pairing-link share options include every endpoint that is not unavailable (apps/web/src/components/settings/AuthorizedClientsSettings.tsx:182). A pairing link minted by the desktop's backend is then sent to the other server, which does not know that credential.

Impact and scope: Impact: P2 - a desktop user whose Serve port is held by another T3 Code server on the same machine sees Tailscale HTTPS as working and shares pairing links that cannot pair with this desktop; a safe workaround exists (choose another Serve port in the Tailscale HTTPS setup dialog, or stop the other server's Serve route).

Affects any desktop host where another live T3 Code server publishes the root of the same Serve port: for example a t3 serve --tailscale-serve server or t3 pair --tailscale mapping (which persists across restarts) under another T3 home, or a second desktop instance as in #10363. The desktop's own backend is not reachable over that URL at all, because its server leaves Serve off. How often users run two servers on one machine was not measured.

Impact

Minor bug or occasional failure

Version or commit

main @ 86ff9d0 (apps/desktop 0.1.9)

Environment

Linux x86_64, Node.js 26.13.1; Vitest through vp test. Platform-independent provider logic.

Logs or stack traces

Output of the reproduction (one line, wrapped here only by the viewer):

{"requests":["https://host-a.tailnet-name.ts.net/.well-known/t3/environment"],"https":{"httpBaseUrl":"https://host-a.tailnet-name.ts.net/","status":"available","compatibility":{"hostedHttpsApp":"compatible","desktopApp":"compatible"},"description":"HTTPS endpoint served by Tailscale Serve."}}
 Test Files  1 passed (1)
      Tests  1 passed (1)

Source references at the pinned commit:

  • packages/tailscale/src/tailscale.ts:713-728: probeTailscaleHttpsEndpoint returns true for any body that decodes as ExecutionEnvironmentDescriptor; environmentId is decoded but never compared.
  • apps/desktop/src/backend/tailscaleEndpointProvider.ts:95-110: the probe result alone sets status, hostedHttpsCompatibility, and the description.
  • apps/desktop/src/backend/DesktopServerExposure.ts:604-632: getAdvertisedEndpoints passes the saved Serve setting and port to the provider; nothing else checks which server answers.
  • apps/server/src/server.ts:909-924 and :944-956: the desktop's backend classifies the Serve root as other when it proxies to another live loopback listener and leaves Serve off.

Screenshots, recordings, or supporting files

No response

Workaround

See Impact and scope under Actual behavior.

Additional details

  • Interface: UI: desktop Settings → Sharing & access, Tailscale HTTPS row and pairing-link share options (fed by the desktop:get-advertised-endpoints IPC method)
  • Earliest failure stage: Integration or transport

Analysis or suspected cause

Source: the desktop has no input naming its own environment when it probes, so any T3 descriptor passes. The local backend's descriptor at its loopback URL carries the environmentId the probe result would need to match.

References

#34735 (closed by #34744) made the probe require a T3 discovery descriptor, and that PR lists "the probe does not check that the descriptor's environmentId matches this server" under "Not in this change"; this report is that remaining case. #10363 (closed by #35042) made the server leave Serve alone when another live server holds the port, which is the state in which the desktop now misreports. #32332 (open) adds a desktop pre-check when Tailscale HTTPS is turned on, but does not change how an already-enabled endpoint is probed. #14746 (open) is about the switch reading the probe instead of the saved setting when the probe fails; this report is about the probe passing for the wrong server.


Found by reading T3 Code's Tailscale integration against the Tailscale source (tailscale/tailscale@071bf947, v1.105.0); written up with AI assistance (Claude Code).

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