fix: add explicit tailnet gateway bind

This commit is contained in:
Peter Steinberger
2026-01-21 20:35:39 +00:00
parent 45c1ccdfcf
commit b5fd66c92d
30 changed files with 143 additions and 71 deletions

View File

@@ -5,22 +5,19 @@ Docs: https://docs.clawd.bot
## 2026.1.21
### Changes
- Caching: make tool-result pruning TTL-aware so cache reuse stays stable and token usage drops.
- CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output.
- CLI: exec approvals mutations render tables instead of raw JSON.
- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
- Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution.
- Nodes: run always uses exec approvals + defaults, with raw shell mode and ask/security overrides. https://docs.clawd.bot/cli/nodes
- CLI: flatten node service commands under `clawdbot node` and remove `service node` docs.
- CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability.
- Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot.
### Fixes
- Embedded runner: drop obsolete pi-mono transcript workarounds now handled upstream.
- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
- Embedded runner: persist injected history images so attachments arent reloaded each turn. (#1374) Thanks @Nicell.
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
- macOS: exec approvals now respect wildcard agent allowlists (`*`).
- macOS: bundle and cache the model catalog instead of reading from a local pi-mono checkout.
- macOS: allow SSH agent auth when no identity file is set. (#1384) Thanks @ameno-.
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
- Typing: start instant typing indicators at run start so DMs and mentions show immediately.

View File

@@ -40,7 +40,7 @@ Notes:
### Options
- `--port <port>`: WebSocket port (default comes from config/env; usually `18789`).
- `--bind <loopback|lan|tailnet|auto>`: listener bind mode.
- `--bind <loopback|lan|tailnet|auto|custom>`: listener bind mode.
- `--auth <token|password>`: auth mode override.
- `--token <token>`: token override (also sets `CLAWDBOT_GATEWAY_TOKEN` for the process).
- `--password <password>`: password override (also sets `CLAWDBOT_GATEWAY_PASSWORD` for the process).

View File

@@ -310,7 +310,7 @@ Options:
- `--minimax-api-key <key>`
- `--opencode-zen-api-key <key>`
- `--gateway-port <port>`
- `--gateway-bind <loopback|lan|tailnet|auto>`
- `--gateway-bind <loopback|lan|tailnet|auto|custom>`
- `--gateway-auth <off|token|password>`
- `--gateway-token <token>`
- `--gateway-password <password>`
@@ -596,7 +596,7 @@ Run the WebSocket Gateway.
Options:
- `--port <port>`
- `--bind <loopback|tailnet|lan|auto>`
- `--bind <loopback|tailnet|lan|auto|custom>`
- `--token <token>`
- `--auth <token|password>`
- `--password <password>`

View File

@@ -2647,6 +2647,13 @@ Defaults:
- bind: `loopback`
- port: `18789` (single port for WS + HTTP)
Bind modes:
- `loopback`: `127.0.0.1` (local-only)
- `lan`: `0.0.0.0` (all interfaces)
- `tailnet`: Tailscale IPv4 address (100.64.0.0/10)
- `auto`: prefer loopback, fall back to LAN if loopback cannot bind
- `custom`: `gateway.customBindHost` (IPv4), fallback to LAN if unavailable
```json5
{
gateway: {
@@ -2677,7 +2684,7 @@ Notes:
- OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
- OpenResponses endpoint: **disabled by default**; enable with `gateway.http.endpoints.responses.enabled: true`.
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
- Non-loopback binds (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
- The onboarding wizard generates a gateway token by default (even on loopback).
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.

View File

@@ -112,7 +112,7 @@ Runbook: [macOS remote access](/platforms/mac/remote).
Short version: **keep the Gateway loopback-only** unless youre sure you need a bind.
- **Loopback + SSH/Tailscale Serve** is the safest default (no public exposure).
- **Non-loopback binds** (`lan`/`tailnet`/`auto`) must use auth tokens/passwords.
- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use auth tokens/passwords.
- `gateway.remote.token` is **only** for remote CLI calls — it does **not** enable local auth.
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
- **Tailscale Serve** can authenticate via identity headers when `gateway.auth.allowTailscale: true`.

View File

@@ -237,7 +237,7 @@ The Gateway multiplexes **WebSocket + HTTP** on a single port:
Bind mode controls where the Gateway listens:
- `gateway.bind: "loopback"` (default): only local clients can connect.
- Non-loopback binds (`"lan"`, `"tailnet"`, `"auto"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall.
- Non-loopback binds (`"lan"`, `"tailnet"`, `"custom"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall.
Rules of thumb:
- Prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback, and Tailscale handles access).

View File

@@ -46,6 +46,25 @@ force `gateway.auth.mode: "password"`.
Open: `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
### Tailnet-only (bind to Tailnet IP)
Use this when you want the Gateway to listen directly on the Tailnet IP (no Serve/Funnel).
```json5
{
gateway: {
bind: "tailnet",
auth: { mode: "token", token: "your-token" }
}
}
```
Connect from another Tailnet device:
- Control UI: `http://<tailscale-ip>:18789/`
- WebSocket: `ws://<tailscale-ip>:18789`
Note: loopback (`http://127.0.0.1:18789`) will **not** work in this mode.
### Public internet (Funnel + shared password)
```json5
@@ -73,6 +92,8 @@ clawdbot gateway --tailscale funnel --auth password
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
- Set `gateway.tailscale.resetOnExit` if you want Clawdbot to undo `tailscale serve`
or `tailscale funnel` configuration on shutdown.
- `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
- `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only.
- Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic
uses the separate bridge port (default `18790`) and is **not** proxied by Serve.

View File

@@ -112,7 +112,7 @@ the Gateway likely refused to bind.
- `gateway.mode` must be `local` for `clawdbot gateway` and the service.
- If you set `gateway.mode=remote`, the **CLI defaults** to a remote URL. The service can still be running locally, but your CLI may be probing the wrong place. Use `clawdbot gateway status` to see the services resolved port + probe target (or pass `--url`).
- `clawdbot gateway status` and `clawdbot doctor` surface the **last gateway error** from logs when the service looks running but the port is closed.
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth:
- Non-loopback binds (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) require auth:
`gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
- `gateway.remote.token` is for remote CLI calls only; it does **not** enable local auth.
- `gateway.token` is ignored; use `gateway.auth.token`.
@@ -127,7 +127,7 @@ the Gateway likely refused to bind.
- Fix: run `clawdbot doctor` to update it (or `clawdbot gateway install --force` for a full rewrite).
**If `Last gateway error:` mentions “refusing to bind … without auth”**
- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`auto`) but left auth off.
- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but left auth off.
- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service.
**If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found**

View File

@@ -1415,7 +1415,7 @@ Fix:
- Start Tailscale on that host (so it has a 100.x address), or
- Switch to `gateway.bind: "loopback"` / `"lan"`.
Note: `tailnet` is legacy and is migrated to `auto` by Doctor. Prefer `gateway.bind: "auto"` when using Tailscale.
Note: `tailnet` is explicit. `auto` prefers loopback; use `gateway.bind: "tailnet"` when you want a tailnet-only bind.
### Can I run multiple Gateways on the same host?

View File

@@ -46,7 +46,7 @@ export function pickProbeHostForBind(
if (bindMode === "custom" && customBindHost?.trim()) {
return customBindHost.trim();
}
if (bindMode === "auto") return tailnetIPv4 ?? "127.0.0.1";
if (bindMode === "tailnet") return tailnetIPv4 ?? "127.0.0.1";
return "127.0.0.1";
}

View File

@@ -172,7 +172,8 @@ export async function gatherDaemonStatus(
| "auto"
| "lan"
| "loopback"
| "custom";
| "custom"
| "tailnet";
const customBindHost = daemonCfg.gateway?.customBindHost;
const bindHost = await resolveGatewayBindHost(bindMode, customBindHost);
const tailnetIPv4 = pickPrimaryTailnetIPv4();

View File

@@ -174,11 +174,15 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
}
const bindRaw = toOptionString(opts.bind) ?? cfg.gateway?.bind ?? "loopback";
const bind =
bindRaw === "loopback" || bindRaw === "lan" || bindRaw === "auto" || bindRaw === "custom"
bindRaw === "loopback" ||
bindRaw === "lan" ||
bindRaw === "auto" ||
bindRaw === "custom" ||
bindRaw === "tailnet"
? bindRaw
: null;
if (!bind) {
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "auto", or "custom")');
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "tailnet", "auto", or "custom")');
defaultRuntime.exit(1);
return;
}
@@ -304,7 +308,7 @@ export function addGatewayRunCommand(cmd: Command): Command {
.option("--port <port>", "Port for the gateway WebSocket")
.option(
"--bind <mode>",
'Bind mode ("loopback"|"tailnet"|"lan"|"auto"). Defaults to config gateway.bind (or loopback).',
'Bind mode ("loopback"|"lan"|"tailnet"|"auto"|"custom"). Defaults to config gateway.bind (or loopback).',
)
.option(
"--token <token>",

View File

@@ -76,7 +76,7 @@ export function registerOnboardCommand(program: Command) {
.option("--synthetic-api-key <key>", "Synthetic API key")
.option("--opencode-zen-api-key <key>", "OpenCode Zen API key")
.option("--gateway-port <port>", "Gateway port")
.option("--gateway-bind <mode>", "Gateway bind: loopback|lan|auto|custom")
.option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom")
.option("--gateway-auth <mode>", "Gateway auth: off|token|password")
.option("--gateway-token <token>", "Gateway token (token auth)")
.option("--gateway-password <password>", "Gateway password (password auth)")

View File

@@ -31,21 +31,26 @@ export async function promptGatewayConfig(
await select({
message: "Gateway bind mode",
options: [
{
value: "loopback",
label: "Loopback (Local only)",
hint: "Bind to 127.0.0.1 - secure, local-only access",
},
{
value: "tailnet",
label: "Tailnet (Tailscale IP)",
hint: "Bind to your Tailscale IP only (100.x.x.x)",
},
{
value: "auto",
label: "Auto (Tailnet → LAN)",
hint: "Prefer Tailnet IP, fall back to all interfaces if unavailable",
label: "Auto (Loopback → LAN)",
hint: "Prefer loopback; fall back to all interfaces if unavailable",
},
{
value: "lan",
label: "LAN (All interfaces)",
hint: "Bind to 0.0.0.0 - accessible from anywhere on your network",
},
{
value: "loopback",
label: "Loopback (Local only)",
hint: "Bind to 127.0.0.1 - secure, local-only access",
},
{
value: "custom",
label: "Custom IP",
@@ -54,7 +59,7 @@ export async function promptGatewayConfig(
],
}),
runtime,
) as "auto" | "lan" | "loopback" | "custom";
) as "auto" | "lan" | "loopback" | "custom" | "tailnet";
let customBindHost: string | undefined;
if (bind === "custom") {

View File

@@ -71,4 +71,24 @@ describe("resolveControlUiLinks", () => {
expect(links.httpUrl).toBe("http://127.0.0.1:18789/");
expect(links.wsUrl).toBe("ws://127.0.0.1:18789");
});
it("uses tailnet IP for tailnet bind", () => {
mocks.pickPrimaryTailnetIPv4.mockReturnValueOnce("100.64.0.9");
const links = resolveControlUiLinks({
port: 18789,
bind: "tailnet",
});
expect(links.httpUrl).toBe("http://100.64.0.9:18789/");
expect(links.wsUrl).toBe("ws://100.64.0.9:18789");
});
it("keeps loopback for auto even when tailnet is present", () => {
mocks.pickPrimaryTailnetIPv4.mockReturnValueOnce("100.64.0.9");
const links = resolveControlUiLinks({
port: 18789,
bind: "auto",
});
expect(links.httpUrl).toBe("http://127.0.0.1:18789/");
expect(links.wsUrl).toBe("ws://127.0.0.1:18789");
});
});

View File

@@ -366,7 +366,7 @@ export const DEFAULT_WORKSPACE = DEFAULT_AGENT_WORKSPACE_DIR;
export function resolveControlUiLinks(params: {
port: number;
bind?: "auto" | "lan" | "loopback" | "custom";
bind?: "auto" | "lan" | "loopback" | "custom" | "tailnet";
customBindHost?: string;
basePath?: string;
}): { httpUrl: string; wsUrl: string } {
@@ -378,7 +378,7 @@ export function resolveControlUiLinks(params: {
if (bind === "custom" && customBindHost && isValidIPv4(customBindHost)) {
return customBindHost;
}
if (bind === "auto" && tailnetIPv4) return tailnetIPv4 ?? "127.0.0.1";
if (bind === "tailnet" && tailnetIPv4) return tailnetIPv4 ?? "127.0.0.1";
return "127.0.0.1";
})();
const basePath = normalizeControlUiBasePath(params.basePath);

View File

@@ -91,7 +91,7 @@ export async function runNonInteractiveOnboardingLocal(params: {
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
if (!opts.skipHealth) {
const links = resolveControlUiLinks({
bind: gatewayResult.bind as "auto" | "lan" | "loopback" | "custom",
bind: gatewayResult.bind as "auto" | "lan" | "loopback" | "custom" | "tailnet",
port: gatewayResult.port,
customBindHost: nextConfig.gateway?.customBindHost,
basePath: undefined,

View File

@@ -33,7 +33,7 @@ export type AuthChoice =
| "skip";
export type GatewayAuthChoice = "off" | "token" | "password";
export type ResetScope = "config" | "config+creds+sessions" | "full";
export type GatewayBind = "loopback" | "lan" | "auto" | "custom";
export type GatewayBind = "loopback" | "lan" | "auto" | "custom" | "tailnet";
export type TailscaleMode = "off" | "serve" | "funnel";
export type NodeManagerChoice = "npm" | "pnpm" | "bun";
export type ChannelChoice = ChannelId;

View File

@@ -218,14 +218,14 @@ describe("legacy config detection", () => {
expect(res.config?.gateway?.auth?.mode).toBe("token");
expect((res.config?.gateway as { token?: string })?.token).toBeUndefined();
});
it("migrates gateway.bind from 'tailnet' to 'auto'", async () => {
it("keeps gateway.bind tailnet", async () => {
vi.resetModules();
const { migrateLegacyConfig } = await import("./config.js");
const res = migrateLegacyConfig({
gateway: { bind: "tailnet" as const },
});
expect(res.changes).toContain("Migrated gateway.bind from 'tailnet' to 'auto'.");
expect(res.config?.gateway?.bind).toBe("auto");
expect(res.changes).not.toContain("Migrated gateway.bind from 'tailnet' to 'auto'.");
expect(res.config?.gateway?.bind).toBe("tailnet");
});
it('rejects telegram.dmPolicy="open" without allowFrom "*"', async () => {
vi.resetModules();

View File

@@ -143,21 +143,4 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
delete raw.identity;
},
},
{
id: "bind-tailnet->auto",
describe: "Remap gateway bind 'tailnet' to 'auto'",
apply: (raw, changes) => {
const migrateBind = (obj: Record<string, unknown> | null | undefined, key: string) => {
if (!obj) return;
const bind = obj.bind;
if (bind === "tailnet") {
obj.bind = "auto";
changes.push(`Migrated ${key}.bind from 'tailnet' to 'auto'.`);
}
};
const gateway = getRecord(raw.gateway);
migrateBind(gateway, "gateway");
},
},
];

View File

@@ -1,4 +1,4 @@
export type GatewayBindMode = "auto" | "lan" | "loopback" | "custom";
export type GatewayBindMode = "auto" | "lan" | "loopback" | "custom" | "tailnet";
export type GatewayTlsConfig = {
/** Enable TLS for the gateway server. */
@@ -189,9 +189,10 @@ export type GatewayConfig = {
mode?: "local" | "remote";
/**
* Bind address policy for the Gateway WebSocket + Control UI HTTP server.
* - auto: Tailnet IPv4 if available, else 0.0.0.0 (fallback to all interfaces)
* - auto: Loopback (127.0.0.1) if available, else 0.0.0.0 (fallback to all interfaces)
* - lan: 0.0.0.0 (all interfaces, no fallback)
* - loopback: 127.0.0.1 (local-only)
* - tailnet: Tailnet IPv4 if available (100.64.0.0/10), else loopback
* - custom: User-specified IP, fallback to 0.0.0.0 if unavailable (requires customBindHost)
* Default: loopback (127.0.0.1).
*/

View File

@@ -270,7 +270,13 @@ export const ClawdbotSchema = z
port: z.number().int().positive().optional(),
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
bind: z
.union([z.literal("auto"), z.literal("lan"), z.literal("loopback"), z.literal("custom")])
.union([
z.literal("auto"),
z.literal("lan"),
z.literal("loopback"),
z.literal("custom"),
z.literal("tailnet"),
])
.optional(),
controlUi: z
.object({

View File

@@ -72,14 +72,14 @@ describe("callGateway url resolution", () => {
closeReason = "";
});
it("uses tailnet IP when local bind is auto and tailnet is present", async () => {
it("keeps loopback when local bind is auto even if tailnet is present", async () => {
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "auto" } });
resolveGatewayPort.mockReturnValue(18800);
pickPrimaryTailnetIPv4.mockReturnValue("100.64.0.1");
await callGateway({ method: "health" });
expect(lastClientOptions?.url).toBe("ws://100.64.0.1:18800");
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18800");
});
it("falls back to loopback when local bind is auto without tailnet IP", async () => {
@@ -92,6 +92,16 @@ describe("callGateway url resolution", () => {
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18800");
});
it("uses tailnet IP when local bind is tailnet and tailnet is present", async () => {
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "tailnet" } });
resolveGatewayPort.mockReturnValue(18800);
pickPrimaryTailnetIPv4.mockReturnValue("100.64.0.1");
await callGateway({ method: "health" });
expect(lastClientOptions?.url).toBe("ws://100.64.0.1:18800");
});
it("uses url override in remote mode even when remote url is missing", async () => {
loadConfig.mockReturnValue({
gateway: { mode: "remote", bind: "loopback", remote: {} },

View File

@@ -63,7 +63,7 @@ export function buildGatewayConnectionDetails(
const localPort = resolveGatewayPort(config);
const tailnetIPv4 = pickPrimaryTailnetIPv4();
const bindMode = config.gateway?.bind ?? "loopback";
const preferTailnet = bindMode === "auto" && !!tailnetIPv4;
const preferTailnet = bindMode === "tailnet" && !!tailnetIPv4;
const scheme = tlsEnabled ? "wss" : "ws";
const localUrl =
preferTailnet && tailnetIPv4

View File

@@ -33,7 +33,8 @@ export function isLocalGatewayAddress(ip: string | undefined): boolean {
* Modes:
* - loopback: 127.0.0.1 (rarely fails, but handled gracefully)
* - lan: always 0.0.0.0 (no fallback)
* - auto: Tailnet IPv4 if available, else 0.0.0.0
* - tailnet: Tailnet IPv4 if available, else loopback
* - auto: Loopback if available, else 0.0.0.0
* - custom: User-specified IP, fallback to 0.0.0.0 if unavailable
*
* @returns The bind address to use (never null)
@@ -50,6 +51,13 @@ export async function resolveGatewayBindHost(
return "0.0.0.0"; // extreme fallback
}
if (mode === "tailnet") {
const tailnetIP = pickPrimaryTailnetIPv4();
if (tailnetIP && (await canBindTo(tailnetIP))) return tailnetIP;
if (await canBindTo("127.0.0.1")) return "127.0.0.1";
return "0.0.0.0";
}
if (mode === "lan") {
return "0.0.0.0";
}
@@ -64,8 +72,7 @@ export async function resolveGatewayBindHost(
}
if (mode === "auto") {
const tailnetIP = pickPrimaryTailnetIPv4();
if (tailnetIP && (await canBindTo(tailnetIP))) return tailnetIP;
if (await canBindTo("127.0.0.1")) return "127.0.0.1";
return "0.0.0.0";
}

View File

@@ -97,7 +97,7 @@ export type GatewayServerOptions = {
* - loopback: 127.0.0.1
* - lan: 0.0.0.0
* - tailnet: bind only to the Tailscale IPv4 address (100.64.0.0/10)
* - auto: prefer tailnet, else LAN
* - auto: prefer loopback, else LAN
*/
bind?: import("../config/config.js").GatewayBindMode;
/**

View File

@@ -87,11 +87,15 @@ async function main() {
cfg.gateway?.bind ??
"loopback";
const bind =
bindRaw === "loopback" || bindRaw === "lan" || bindRaw === "auto" || bindRaw === "custom"
bindRaw === "loopback" ||
bindRaw === "lan" ||
bindRaw === "auto" ||
bindRaw === "custom" ||
bindRaw === "tailnet"
? bindRaw
: null;
if (!bind) {
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "auto", or "custom")');
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "tailnet", "auto", or "custom")');
process.exit(1);
}

View File

@@ -52,12 +52,13 @@ export async function configureGatewayForOnboarding(
message: "Gateway bind",
options: [
{ value: "loopback", label: "Loopback (127.0.0.1)" },
{ value: "lan", label: "LAN" },
{ value: "auto", label: "Auto" },
{ value: "lan", label: "LAN (0.0.0.0)" },
{ value: "tailnet", label: "Tailnet (Tailscale IP)" },
{ value: "auto", label: "Auto (Loopback → LAN)" },
{ value: "custom", label: "Custom IP" },
],
})) as "loopback" | "lan" | "auto" | "custom")
) as "loopback" | "lan" | "auto" | "custom";
})) as "loopback" | "lan" | "auto" | "custom" | "tailnet")
) as "loopback" | "lan" | "auto" | "custom" | "tailnet";
let customBindHost = quickstartGateway.customBindHost;
if (bind === "custom") {

View File

@@ -177,7 +177,11 @@ export async function runOnboardingWizard(
const bindRaw = baseConfig.gateway?.bind;
const bind =
bindRaw === "loopback" || bindRaw === "lan" || bindRaw === "auto" || bindRaw === "custom"
bindRaw === "loopback" ||
bindRaw === "lan" ||
bindRaw === "auto" ||
bindRaw === "custom" ||
bindRaw === "tailnet"
? bindRaw
: "loopback";
@@ -213,10 +217,11 @@ export async function runOnboardingWizard(
})();
if (flow === "quickstart") {
const formatBind = (value: "loopback" | "lan" | "auto" | "custom") => {
const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => {
if (value === "loopback") return "Loopback (127.0.0.1)";
if (value === "lan") return "LAN";
if (value === "custom") return "Custom IP";
if (value === "tailnet") return "Tailnet (Tailscale IP)";
return "Auto";
};
const formatAuth = (value: GatewayAuthChoice) => {

View File

@@ -5,7 +5,7 @@ export type WizardFlow = "quickstart" | "advanced";
export type QuickstartGatewayDefaults = {
hasExisting: boolean;
port: number;
bind: "loopback" | "lan" | "auto" | "custom";
bind: "loopback" | "lan" | "auto" | "custom" | "tailnet";
authMode: GatewayAuthChoice;
tailscaleMode: "off" | "serve" | "funnel";
token?: string;
@@ -16,7 +16,7 @@ export type QuickstartGatewayDefaults = {
export type GatewayWizardSettings = {
port: number;
bind: "loopback" | "lan" | "auto" | "custom";
bind: "loopback" | "lan" | "auto" | "custom" | "tailnet";
customBindHost?: string;
authMode: GatewayAuthChoice;
gatewayToken?: string;