fix(remote): derive tailscale target port from server-side request

Override the stored tailscale targetPort in the start handler using the
actual port the start request landed on (req.socket.localPort), instead
of trusting a value the browser supplied. window.location.port can be
wrong behind a reverse proxy, ssh forward, or split deployment, and
silently funneling the wrong process is worse than the prior
"port-not-configured" error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-29 13:39:08 -07:00
parent 79e4ce4617
commit ce37c61a79
2 changed files with 27 additions and 8 deletions

View File

@@ -4114,14 +4114,10 @@ export function SettingsModal({
remoteActiveProvider: activeProvider,
remoteTailscaleEnabled: activeProvider === "tailscale",
remoteTailscaleHostname: String(formState.remoteTailscaleHostname ?? ""),
remoteTailscaleTargetPort: (() => {
const livePort = typeof window !== "undefined" ? Number(window.location.port) : NaN;
if (Number.isFinite(livePort) && livePort > 0) return livePort;
const proto = typeof window !== "undefined" ? window.location.protocol : "";
if (proto === "https:") return 443;
if (proto === "http:") return 80;
return Number(formState.remoteTailscaleTargetPort ?? 4040);
})(),
// Server overrides this with req.socket.localPort
// when starting the tunnel; the value sent here is
// only a fallback if that override doesn't fire.
remoteTailscaleTargetPort: Number(formState.remoteTailscaleTargetPort ?? 4040),
remoteTailscaleAcceptRoutes: Boolean(formState.remoteTailscaleAcceptRoutes),
remoteCloudflareEnabled: activeProvider === "cloudflare",
remoteCloudflareQuickTunnel: Boolean(formState.remoteCloudflareQuickTunnel ?? true),

View File

@@ -473,6 +473,29 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
throw new ApiError(409, "No active provider configured", { code: "NO_ACTIVE_PROVIDER" });
}
// For tailscale we always funnel the dashboard's *actual* listen port
// — the port this very request landed on. The user-facing UI no
// longer collects a target port; relying on a stored value risks
// silently funneling the wrong process if the dashboard later binds
// a different port (EADDRINUSE fallback, daemon restart, etc.).
if (provider === "tailscale" && settings.remoteAccess) {
const livePort = req.socket?.localPort;
if (Number.isFinite(livePort) && (livePort ?? 0) > 0 && livePort !== settings.remoteAccess.providers.tailscale.targetPort) {
await scopedStore.updateSettings({
remoteAccess: {
...settings.remoteAccess,
providers: {
...settings.remoteAccess.providers,
tailscale: {
...settings.remoteAccess.providers.tailscale,
targetPort: livePort as number,
},
},
},
});
}
}
if (!engine) {
res.json({ state: "starting", provider });
return;