feat: add Tailscale binary detection, IP binding modes, and health probe password fix
This PR includes three main improvements:
1. Tailscale Binary Detection with Fallback Strategies
- Added findTailscaleBinary() with multi-strategy detection:
* PATH lookup via 'which' command
* Known macOS app path (/Applications/Tailscale.app/Contents/MacOS/Tailscale)
* find /Applications for Tailscale.app
* locate database lookup
- Added getTailscaleBinary() with caching
- Updated all Tailscale operations to use detected binary
- Added TUI warning when Tailscale binary not found for serve/funnel modes
2. Custom Gateway IP Binding with Fallback
- New bind mode "custom" allowing user-specified IP with fallback to 0.0.0.0
- Removed "tailnet" mode (folded into "auto")
- All modes now support graceful fallback: custom (if fail → 0.0.0.0), loopback (127.0.0.1 → 0.0.0.0), auto (tailnet → 0.0.0.0), lan (0.0.0.0)
- Added customBindHost config option for custom bind mode
- Added canBindTo() helper to test IP availability before binding
- Updated configure and onboarding wizards with new bind mode options
3. Health Probe Password Auth Fix
- Gateway probe now tries both new and old passwords
- Fixes issue where password change fails health check if gateway hasn't restarted yet
- Uses nextConfig password first, falls back to baseConfig password if needed
Files changed:
- src/infra/tailscale.ts: Binary detection + caching
- src/gateway/net.ts: IP binding with fallback logic
- src/config/types.ts: BridgeBindMode type + customBindHost field
- src/commands/configure.ts: Health probe dual-password try + Tailscale detection warning + bind mode UI
- src/wizard/onboarding.ts: Tailscale detection warning + bind mode UI
- src/gateway/server.ts: Use new resolveGatewayBindHost
- src/gateway/call.ts: Updated preferTailnet logic (removed "tailnet" mode)
- src/commands/onboard-types.ts: Updated GatewayBind type
- src/commands/onboard-helpers.ts: resolveControlUiLinks updated
- src/cli/*.ts: Updated bind mode casts
- src/gateway/call.test.ts: Removed "tailnet" mode test
This commit is contained in:
committed by
Peter Steinberger
parent
f94ad21f1e
commit
c851bdd47a
@@ -62,6 +62,7 @@ import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||
import { findTailscaleBinary } from "../infra/tailscale.js";
|
||||
import { listProviderPlugins } from "../providers/plugins/index.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -173,14 +174,15 @@ export async function runOnboardingWizard(
|
||||
baseConfig.gateway?.auth?.mode !== undefined ||
|
||||
baseConfig.gateway?.auth?.token !== undefined ||
|
||||
baseConfig.gateway?.auth?.password !== undefined ||
|
||||
baseConfig.gateway?.customBindHost !== undefined ||
|
||||
baseConfig.gateway?.tailscale?.mode !== undefined;
|
||||
|
||||
const bindRaw = baseConfig.gateway?.bind;
|
||||
const bind =
|
||||
bindRaw === "loopback" ||
|
||||
bindRaw === "lan" ||
|
||||
bindRaw === "tailnet" ||
|
||||
bindRaw === "auto"
|
||||
bindRaw === "auto" ||
|
||||
bindRaw === "custom"
|
||||
? bindRaw
|
||||
: "loopback";
|
||||
|
||||
@@ -212,15 +214,16 @@ export async function runOnboardingWizard(
|
||||
tailscaleMode,
|
||||
token: baseConfig.gateway?.auth?.token,
|
||||
password: baseConfig.gateway?.auth?.password,
|
||||
customBindHost: baseConfig.gateway?.customBindHost,
|
||||
tailscaleResetOnExit: baseConfig.gateway?.tailscale?.resetOnExit ?? false,
|
||||
};
|
||||
})();
|
||||
|
||||
if (flow === "quickstart") {
|
||||
const formatBind = (value: "loopback" | "lan" | "tailnet" | "auto") => {
|
||||
const formatBind = (value: "loopback" | "lan" | "auto" | "custom") => {
|
||||
if (value === "loopback") return "Loopback (127.0.0.1)";
|
||||
if (value === "lan") return "LAN";
|
||||
if (value === "tailnet") return "Tailnet";
|
||||
if (value === "custom") return "Custom IP";
|
||||
return "Auto";
|
||||
};
|
||||
const formatAuth = (value: GatewayAuthChoice) => {
|
||||
@@ -238,6 +241,10 @@ export async function runOnboardingWizard(
|
||||
"Keeping your current gateway settings:",
|
||||
`Gateway port: ${quickstartGateway.port}`,
|
||||
`Gateway bind: ${formatBind(quickstartGateway.bind)}`,
|
||||
...(quickstartGateway.bind === "custom" &&
|
||||
quickstartGateway.customBindHost
|
||||
? [`Gateway custom IP: ${quickstartGateway.customBindHost}`]
|
||||
: []),
|
||||
`Gateway auth: ${formatAuth(quickstartGateway.authMode)}`,
|
||||
`Tailscale exposure: ${formatTailscale(
|
||||
quickstartGateway.tailscaleMode,
|
||||
@@ -396,11 +403,39 @@ export async function runOnboardingWizard(
|
||||
options: [
|
||||
{ value: "loopback", label: "Loopback (127.0.0.1)" },
|
||||
{ value: "lan", label: "LAN" },
|
||||
{ value: "tailnet", label: "Tailnet" },
|
||||
{ value: "auto", label: "Auto" },
|
||||
{ value: "custom", label: "Custom IP" },
|
||||
],
|
||||
})) as "loopback" | "lan" | "tailnet" | "auto")
|
||||
) as "loopback" | "lan" | "tailnet" | "auto";
|
||||
})) as "loopback" | "lan" | "auto" | "custom")
|
||||
) as "loopback" | "lan" | "auto" | "custom";
|
||||
|
||||
let customBindHost = quickstartGateway.customBindHost;
|
||||
if (bind === "custom") {
|
||||
const needsPrompt = flow !== "quickstart" || !customBindHost;
|
||||
if (needsPrompt) {
|
||||
const input = await prompter.text({
|
||||
message: "Custom IP address",
|
||||
placeholder: "192.168.1.100",
|
||||
initialValue: customBindHost ?? "",
|
||||
validate: (value) => {
|
||||
if (!value) return "IP address is required for custom bind mode";
|
||||
const trimmed = value.trim();
|
||||
const parts = trimmed.split(".");
|
||||
if (parts.length !== 4)
|
||||
return "Invalid IPv4 address (e.g., 192.168.1.100)";
|
||||
if (
|
||||
parts.every((part) => {
|
||||
const n = parseInt(part, 10);
|
||||
return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n);
|
||||
})
|
||||
)
|
||||
return undefined;
|
||||
return "Invalid IPv4 address (each octet must be 0-255)";
|
||||
},
|
||||
});
|
||||
customBindHost = typeof input === "string" ? input.trim() : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let authMode = (
|
||||
flow === "quickstart"
|
||||
@@ -445,6 +480,23 @@ export async function runOnboardingWizard(
|
||||
})) as "off" | "serve" | "funnel")
|
||||
) as "off" | "serve" | "funnel";
|
||||
|
||||
// Detect Tailscale binary before proceeding with serve/funnel setup
|
||||
if (tailscaleMode !== "off") {
|
||||
const tailscaleBin = await findTailscaleBinary();
|
||||
if (!tailscaleBin) {
|
||||
await prompter.note(
|
||||
[
|
||||
"Tailscale binary not found in PATH or /Applications.",
|
||||
"Ensure Tailscale is installed from:",
|
||||
" https://tailscale.com/download/mac",
|
||||
"",
|
||||
"You can continue setup, but serve/funnel will fail at runtime.",
|
||||
].join("\n"),
|
||||
"Tailscale Warning",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let tailscaleResetOnExit =
|
||||
flow === "quickstart" ? quickstartGateway.tailscaleResetOnExit : false;
|
||||
if (tailscaleMode !== "off" && flow !== "quickstart") {
|
||||
@@ -470,6 +522,7 @@ export async function runOnboardingWizard(
|
||||
"Note",
|
||||
);
|
||||
bind = "loopback";
|
||||
customBindHost = undefined;
|
||||
}
|
||||
|
||||
if (authMode === "off" && bind !== "loopback") {
|
||||
@@ -538,6 +591,7 @@ export async function runOnboardingWizard(
|
||||
...nextConfig.gateway,
|
||||
port,
|
||||
bind,
|
||||
...(bind === "custom" && customBindHost ? { customBindHost } : {}),
|
||||
tailscale: {
|
||||
...nextConfig.gateway?.tailscale,
|
||||
mode: tailscaleMode,
|
||||
@@ -747,6 +801,7 @@ export async function runOnboardingWizard(
|
||||
const links = resolveControlUiLinks({
|
||||
bind,
|
||||
port,
|
||||
customBindHost,
|
||||
basePath: controlUiBasePath,
|
||||
});
|
||||
const tokenParam =
|
||||
|
||||
Reference in New Issue
Block a user