From 1f697719413afe9f3d28e46cb2668a8c8470e188 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 5 Sep 2026 03:32:46 +0000 Subject: [PATCH 1/2] fix(desktop): separate LAN and Tailscale pairing endpoints --- .../src/backend/DesktopServerExposure.test.ts | 78 +++++++++++++------ .../src/backend/DesktopServerExposure.ts | 15 +++- 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/apps/desktop/src/backend/DesktopServerExposure.test.ts b/apps/desktop/src/backend/DesktopServerExposure.test.ts index dcfee93778d1..a3887fb3928a 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,34 @@ describe("DesktopServerExposure", () => { ), ); + it.effect( + "keeps a Tailscale-only host network-accessible without advertising a LAN endpoint", + () => + 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,28 +373,30 @@ describe("DesktopServerExposure", () => { ), ); - it.effect("uses ConfigProvider desktop exposure overrides", () => - withHarness( - lanNetworkInterfaces, - Effect.gen(function* () { - const serverExposure = yield* DesktopServerExposure.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"); - - 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/"], - ); - }), - { - T3CODE_DESKTOP_LAN_HOST: "10.0.0.7", - T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://public.example.test", - }, - ), + it.effect( + "honors an explicit Tailscale address in ConfigProvider desktop exposure overrides", + () => + withHarness( + lanNetworkInterfaces, + Effect.gen(function* () { + const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; + yield* serverExposure.configureFromSettings({ port: 4173 }); + const change = yield* serverExposure.setMode("network-accessible"); + + 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://100.90.1.2:4173/", "https://public.example.test/"], + ); + }), + { + T3CODE_DESKTOP_LAN_HOST: "100.90.1.2", + T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://public.example.test", + }, + ), ); it.effect("advertises loopback, LAN, and configured manual endpoints from runtime state", () => diff --git a/apps/desktop/src/backend/DesktopServerExposure.ts b/apps/desktop/src/backend/DesktopServerExposure.ts index f04d2af7b1f6..9fd5235016c0 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 30a4991322b69c6ddc5dcc7fc3373843a51ed08a Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 5 Sep 2026 03:55:24 +0000 Subject: [PATCH 2/2] fix(desktop): report only actual network exposure fallbacks --- apps/desktop/src/app/DesktopApp.ts | 5 +- .../src/backend/DesktopServerExposure.test.ts | 96 +++++++++---------- 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index d5a8ac3b7836..d21eefd65f85 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 a3887fb3928a..a8a929f35744 100644 --- a/apps/desktop/src/backend/DesktopServerExposure.test.ts +++ b/apps/desktop/src/backend/DesktopServerExposure.test.ts @@ -324,32 +324,30 @@ describe("DesktopServerExposure", () => { ), ); - it.effect( - "keeps a Tailscale-only host network-accessible without advertising a LAN endpoint", - () => - 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("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", () => @@ -373,30 +371,28 @@ describe("DesktopServerExposure", () => { ), ); - it.effect( - "honors an explicit Tailscale address in ConfigProvider desktop exposure overrides", - () => - withHarness( - lanNetworkInterfaces, - Effect.gen(function* () { - const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; - yield* serverExposure.configureFromSettings({ port: 4173 }); - const change = yield* serverExposure.setMode("network-accessible"); - - 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://100.90.1.2:4173/", "https://public.example.test/"], - ); - }), - { - T3CODE_DESKTOP_LAN_HOST: "100.90.1.2", - T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://public.example.test", - }, - ), + it.effect("preserves explicit Tailscale exposure overrides", () => + withHarness( + lanNetworkInterfaces, + Effect.gen(function* () { + const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; + yield* serverExposure.configureFromSettings({ port: 4173 }); + const change = yield* serverExposure.setMode("network-accessible"); + + 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://100.90.1.2:4173/", "https://public.example.test/"], + ); + }), + { + T3CODE_DESKTOP_LAN_HOST: "100.90.1.2", + T3CODE_DESKTOP_HTTPS_ENDPOINTS: "https://public.example.test", + }, + ), ); it.effect("advertises loopback, LAN, and configured manual endpoints from runtime state", () =>