From 84add475256d312dc7680dba92a04f9cf3ddd89a Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 3 Apr 2026 13:07:17 +0530 Subject: [PATCH] fix(pairing): allow emulator ws setup urls --- src/cli/qr-cli.test.ts | 13 +++++++++++++ src/pairing/setup-code.test.ts | 17 ++++++++++++++++- src/pairing/setup-code.ts | 8 ++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/cli/qr-cli.test.ts b/src/cli/qr-cli.test.ts index 0cceca131b0..ee0cdc2b499 100644 --- a/src/cli/qr-cli.test.ts +++ b/src/cli/qr-cli.test.ts @@ -229,6 +229,19 @@ describe("registerQrCli", () => { expect(output).toContain("gateway.tailscale.mode=serve"); }); + it("allows android emulator cleartext override urls", async () => { + loadConfig.mockReturnValue({ + gateway: { + bind: "loopback", + auth: { mode: "token", token: "tok" }, + }, + }); + + await runQr(["--setup-code-only", "--url", "ws://10.0.2.2:18789"]); + + expectLoggedSetupCode("ws://10.0.2.2:18789"); + }); + it("accepts --token override when config has no auth", async () => { loadConfig.mockReturnValue({ gateway: { diff --git a/src/pairing/setup-code.test.ts b/src/pairing/setup-code.test.ts index 84610554d47..ce59beb660e 100644 --- a/src/pairing/setup-code.test.ts +++ b/src/pairing/setup-code.test.ts @@ -399,6 +399,21 @@ describe("pairing setup code", () => { urlSource: "gateway.bind=custom", }, }, + { + name: "allows android emulator cleartext setup urls", + config: { + gateway: { + bind: "custom", + customBindHost: "10.0.2.2", + auth: { mode: "token", token: "tok_123" }, + }, + } satisfies ResolveSetupConfig, + expected: { + authLabel: "token", + url: "ws://10.0.2.2:18789", + urlSource: "gateway.bind=custom", + }, + }, ] as const)("$name", async ({ config, options, expected }) => { await expectResolvedSetupSuccessCase({ config, @@ -443,7 +458,7 @@ describe("pairing setup code", () => { options: { networkInterfaces: () => createIpv4NetworkInterfaces("192.168.1.20"), } satisfies ResolveSetupOptions, - expectedError: "ws:// is only valid for localhost", + expectedError: "ws:// is only valid for localhost or the Android emulator", }, ] as const)("$name", async ({ config, options, expectedError }) => { await expectResolvedSetupFailureCase({ diff --git a/src/pairing/setup-code.ts b/src/pairing/setup-code.ts index 5d08d6dcde3..34bc168c912 100644 --- a/src/pairing/setup-code.ts +++ b/src/pairing/setup-code.ts @@ -69,10 +69,14 @@ function describeSecureMobilePairingFix(source?: string): string { "Mobile pairing requires a secure remote gateway URL (wss://) or Tailscale Serve/Funnel." + sourceNote + " Fix: prefer gateway.tailscale.mode=serve, or set gateway.remote.url / " + - "plugins.entries.device-pair.config.publicUrl to a wss:// URL. ws:// is only valid for localhost." + "plugins.entries.device-pair.config.publicUrl to a wss:// URL. ws:// is only valid for localhost or the Android emulator." ); } +function isMobilePairingCleartextAllowedHost(host: string): boolean { + return isLoopbackHost(host) || host === "10.0.2.2"; +} + function validateMobilePairingUrl(url: string, source?: string): string | null { if (isSecureWebSocketUrl(url)) { return null; @@ -85,7 +89,7 @@ function validateMobilePairingUrl(url: string, source?: string): string | null { } const protocol = parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" ? "ws:" : parsed.protocol; - if (protocol !== "ws:" || isLoopbackHost(parsed.hostname)) { + if (protocol !== "ws:" || isMobilePairingCleartextAllowedHost(parsed.hostname)) { return null; } return describeSecureMobilePairingFix(source);