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:
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user