diff --git a/apps/server/src/environment/DirectEndpoints.test.ts b/apps/server/src/environment/DirectEndpoints.test.ts index def0434140c32..b5cab770235a1 100644 --- a/apps/server/src/environment/DirectEndpoints.test.ts +++ b/apps/server/src/environment/DirectEndpoints.test.ts @@ -213,6 +213,17 @@ describe("resolveBoundEndpoints", () => { ).toEqual([{ kind: "tailnet", httpBaseUrl: "http://100.101.102.103:3773/" }]); }); + it("reports an explicit bind to Tailscale's IPv6 address as the tailnet route", () => { + expect( + resolveBoundEndpoints({ + platform: "linux", + host: "fd7a:115c:a1e0::1", + port: 3773, + interfaces: INTERFACES, + }), + ).toEqual([{ kind: "tailnet", httpBaseUrl: "http://[fd7a:115c:a1e0::1]:3773/" }]); + }); + it("never reports an IPv4-mapped host, which can name the client's own loopback", () => { expect( resolveBoundEndpoints({ diff --git a/apps/server/src/environment/DirectEndpoints.ts b/apps/server/src/environment/DirectEndpoints.ts index 5b9ecc98299c7..c3eb5d4b75fda 100644 --- a/apps/server/src/environment/DirectEndpoints.ts +++ b/apps/server/src/environment/DirectEndpoints.ts @@ -39,6 +39,7 @@ export class DirectEndpoints extends Context.Service< >()("t3/environment/DirectEndpoints") {} // Credentials travel over HTTP; public IPs leak them and hostnames can resolve differently per client. +// Wildcard binds stay IPv4: the private-network listener check accepts only IPv4 peers. const isAdvertisableAddress = (address: string): boolean => NodeNet.isIPv4(address) && !address.startsWith("127.") && isPrivateNetworkAddress(address); @@ -78,7 +79,10 @@ export function resolveBoundEndpoints(input: { !(input.platform === "win32" && WINDOWS_HOST_ADAPTER_MAC.test(entry.mac)), ) .map((entry) => entry.address) - : [input.host!].filter(isAdvertisableAddress); + : [input.host!].filter( + (address) => + isAdvertisableAddress(address) || (NodeNet.isIPv6(address) && isTailnetHost(address)), + ); return [...new Set(addresses)] .filter((address) => !input.onCloudVm || tailscaleAddresses.has(address)) .map((address) => ({