From 6de83796db20aaa53ab86d86055e682ad75bb2f7 Mon Sep 17 00:00:00 2001 From: maria Date: Sat, 5 Sep 2026 14:41:25 -0400 Subject: [PATCH 1/3] fix(desktop): separate LAN and Tailscale pairing endpoints (#9882) (cherry picked from commit 60e1b73948debac845c3dc72aac35c9adbd4cd64) --- apps/desktop/src/app/DesktopApp.ts | 5 ++- .../src/backend/DesktopServerExposure.test.ts | 40 +++++++++++++++---- .../src/backend/DesktopServerExposure.ts | 15 +++++-- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index 6080ed3ca..369df5739 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -194,7 +194,10 @@ const bootstrap = Effect.gen(function* () { yield* logBootstrapInfo("bootstrap enabled network access", { endpointUrl: serverExposureState.endpointUrl, }); - } else if (settings.serverExposureMode === "network-accessible") { + } else if ( + settings.serverExposureMode === "network-accessible" && + serverExposureState.mode === "local-only" + ) { yield* logBootstrapWarning( "bootstrap fell back to local-only because no advertised network host was available", ); diff --git a/apps/desktop/src/backend/DesktopServerExposure.test.ts b/apps/desktop/src/backend/DesktopServerExposure.test.ts index dcfee9377..a8a929f35 100644 --- a/apps/desktop/src/backend/DesktopServerExposure.test.ts +++ b/apps/desktop/src/backend/DesktopServerExposure.test.ts @@ -307,9 +307,9 @@ describe("DesktopServerExposure", () => { ); }); - it.effect("resolves advertised endpoints from the scoped runtime state", () => + it.effect("keeps LAN and Tailscale endpoints distinct when Tailscale is enumerated first", () => withHarness( - { ...lanNetworkInterfaces, ...tailnetNetworkInterfaces }, + { ...tailnetNetworkInterfaces, ...lanNetworkInterfaces }, Effect.gen(function* () { const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; yield* serverExposure.configureFromSettings({ port: 4173 }); @@ -324,6 +324,32 @@ describe("DesktopServerExposure", () => { ), ); + it.effect("keeps Tailscale-only hosts network-accessible", () => + withHarness( + tailnetNetworkInterfaces, + Effect.gen(function* () { + const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; + const settings = yield* DesktopAppSettings.DesktopAppSettings; + yield* settings.setServerExposureMode("network-accessible"); + + const state = yield* serverExposure.configureFromSettings({ port: 4173 }); + assert.equal(state.mode, "network-accessible"); + assert.equal(state.advertisedHost, null); + assert.equal(state.endpointUrl, null); + assert.equal((yield* serverExposure.backendConfig).bindHost, "0.0.0.0"); + + const endpoints = yield* serverExposure.getAdvertisedEndpoints; + assert.deepEqual( + endpoints.map((endpoint) => [endpoint.reachability, endpoint.httpBaseUrl]), + [ + ["loopback", "http://127.0.0.1:4173/"], + ["private-network", "http://100.90.1.2:4173/"], + ], + ); + }), + ), + ); + it.effect("does not spawn the tailscale CLI while server exposure is local-only", () => withHarness( lanNetworkInterfaces, @@ -345,7 +371,7 @@ describe("DesktopServerExposure", () => { ), ); - it.effect("uses ConfigProvider desktop exposure overrides", () => + it.effect("preserves explicit Tailscale exposure overrides", () => withHarness( lanNetworkInterfaces, Effect.gen(function* () { @@ -353,17 +379,17 @@ describe("DesktopServerExposure", () => { yield* serverExposure.configureFromSettings({ port: 4173 }); const change = yield* serverExposure.setMode("network-accessible"); - assert.equal(change.state.advertisedHost, "10.0.0.7"); - assert.equal(change.state.endpointUrl, "http://10.0.0.7:4173"); + assert.equal(change.state.advertisedHost, "100.90.1.2"); + assert.equal(change.state.endpointUrl, "http://100.90.1.2:4173"); const endpoints = yield* serverExposure.getAdvertisedEndpoints; assert.deepEqual( endpoints.map((endpoint) => endpoint.httpBaseUrl), - ["http://127.0.0.1:4173/", "http://10.0.0.7:4173/", "https://public.example.test/"], + ["http://127.0.0.1:4173/", "http://100.90.1.2:4173/", "https://public.example.test/"], ); }), { - T3CODE_DESKTOP_LAN_HOST: "10.0.0.7", + T3CODE_DESKTOP_LAN_HOST: "100.90.1.2", T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://public.example.test", }, ), diff --git a/apps/desktop/src/backend/DesktopServerExposure.ts b/apps/desktop/src/backend/DesktopServerExposure.ts index f04d2af7b..9fd523501 100644 --- a/apps/desktop/src/backend/DesktopServerExposure.ts +++ b/apps/desktop/src/backend/DesktopServerExposure.ts @@ -9,7 +9,7 @@ import { type DesktopServerExposureMode, type DesktopServerExposureState, } from "@t3tools/contracts"; -import { readTailscaleStatus } from "@t3tools/tailscale"; +import { isTailscaleIpv4Address, readTailscaleStatus } from "@t3tools/tailscale"; import * as Context from "effect/Context"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -65,7 +65,9 @@ const normalizeOptionalHost = (value: string | undefined): string | undefined => }; const isUsableLanIpv4Address = (address: string): boolean => - !address.startsWith("127.") && !address.startsWith("169.254."); + !address.startsWith("127.") && + !address.startsWith("169.254.") && + !isTailscaleIpv4Address(address); const isHttpsEndpointUrl = (value: string): boolean => { try { @@ -378,7 +380,14 @@ function resolveRuntimeState(input: { ...(advertisedHostOverride ? { advertisedHostOverride } : {}), }); const unavailable = - input.requestedMode === "network-accessible" && requestedExposure.endpointUrl === null; + input.requestedMode === "network-accessible" && + requestedExposure.endpointUrl === null && + !Object.values(input.networkInterfaces).some((addresses) => + addresses?.some( + (address) => + !address.internal && address.family === "IPv4" && isTailscaleIpv4Address(address.address), + ), + ); const exposure = unavailable ? resolveDesktopServerExposure({ mode: "local-only", From 6bb34775b3217e723fc0a82a24aa85ae3def8258 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Sat, 5 Sep 2026 19:49:37 -0600 Subject: [PATCH 2/3] docs(upstream): record pairing endpoint fixes --- .agents/upstream-review.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.agents/upstream-review.md b/.agents/upstream-review.md index 5ba4d4963..26cb46c98 100644 --- a/.agents/upstream-review.md +++ b/.agents/upstream-review.md @@ -15,6 +15,23 @@ Two standing sections outlive any single batch and must be read on every review: ## Review batches +## 2026-09-05 — LAN and Tailscale pairing (A4, partial) + +The maintainer approved A4 against upstream `f12d39359f0f76a64ff2d77959c5baf821df15be`. +Adopted #9882 (`60e1b73948debac845c3dc72aac35c9adbd4cd64`) as `c76159b31a` +on `upstream/2026-09-05-pairing-endpoints`, awaiting merge. Clean port: do not +advertise a Tailscale interface as LAN, retain network access on Tailscale-only +hosts, and preserve explicit host overrides. The bootstrap warning now reflects +an actual local-only fallback. Pylon identity and runtime-home boundaries remain. + +Focused exposure tests, desktop typecheck, targeted lint, and formatting pass. +Applies to desktop-hosted environments and web/mobile pairing over LAN/Tailscale, +independent of provider; local-only and explicit HTTPS endpoint behavior stay +covered. No UI layout, protocol, or other launcher changes. + +The cursor is unchanged for this partial selection. Deferred/watch results +are recorded with #268; no register or watch row changes in this PR. + ## 2026-09-02 — `9b2d04317c68233782e0630464ac86d77d0686f3..beae2147a9487ec47ac992319f2216914b4cb62d` The maintainer's standing instruction for this batch was to stop escalating From 9400fb20a9cae5141e38911485162a2608329b98 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Sat, 5 Sep 2026 20:33:14 -0600 Subject: [PATCH 3/3] docs(upstream): consolidate batch records in PR #279 --- .agents/upstream-review.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.agents/upstream-review.md b/.agents/upstream-review.md index 26cb46c98..5ba4d4963 100644 --- a/.agents/upstream-review.md +++ b/.agents/upstream-review.md @@ -15,23 +15,6 @@ Two standing sections outlive any single batch and must be read on every review: ## Review batches -## 2026-09-05 — LAN and Tailscale pairing (A4, partial) - -The maintainer approved A4 against upstream `f12d39359f0f76a64ff2d77959c5baf821df15be`. -Adopted #9882 (`60e1b73948debac845c3dc72aac35c9adbd4cd64`) as `c76159b31a` -on `upstream/2026-09-05-pairing-endpoints`, awaiting merge. Clean port: do not -advertise a Tailscale interface as LAN, retain network access on Tailscale-only -hosts, and preserve explicit host overrides. The bootstrap warning now reflects -an actual local-only fallback. Pylon identity and runtime-home boundaries remain. - -Focused exposure tests, desktop typecheck, targeted lint, and formatting pass. -Applies to desktop-hosted environments and web/mobile pairing over LAN/Tailscale, -independent of provider; local-only and explicit HTTPS endpoint behavior stay -covered. No UI layout, protocol, or other launcher changes. - -The cursor is unchanged for this partial selection. Deferred/watch results -are recorded with #268; no register or watch row changes in this PR. - ## 2026-09-02 — `9b2d04317c68233782e0630464ac86d77d0686f3..beae2147a9487ec47ac992319f2216914b4cb62d` The maintainer's standing instruction for this batch was to stop escalating