fix: auto-detect tailnet DNS hint
This commit is contained in:
@@ -86,7 +86,7 @@ struct GatewayDiscoveryInlineList: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func suggestedSSHTarget(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
|
private func suggestedSSHTarget(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
|
||||||
let host = gateway.tailnetDns ?? gateway.lanHost
|
let host = self.sanitizedTailnetHost(gateway.tailnetDns) ?? gateway.lanHost
|
||||||
guard let host else { return nil }
|
guard let host else { return nil }
|
||||||
let user = NSUserName()
|
let user = NSUserName()
|
||||||
var target = "\(user)@\(host)"
|
var target = "\(user)@\(host)"
|
||||||
@@ -96,6 +96,16 @@ struct GatewayDiscoveryInlineList: View {
|
|||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func sanitizedTailnetHost(_ host: String?) -> String? {
|
||||||
|
guard let host else { return nil }
|
||||||
|
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.isEmpty { return nil }
|
||||||
|
if trimmed.hasSuffix(".internal.") || trimmed.hasSuffix(".internal") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
private func rowBackground(selected: Bool, hovered: Bool) -> Color {
|
private func rowBackground(selected: Bool, hovered: Bool) -> Color {
|
||||||
if selected { return Color.accentColor.opacity(0.12) }
|
if selected { return Color.accentColor.opacity(0.12) }
|
||||||
if hovered { return Color.secondary.opacity(0.08) }
|
if hovered { return Color.secondary.opacity(0.08) }
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ The Gateway advertises small non-secret hints to make UI flows convenient:
|
|||||||
- `gatewayPort=<port>` (informational; the Gateway WS is typically loopback-only)
|
- `gatewayPort=<port>` (informational; the Gateway WS is typically loopback-only)
|
||||||
- `bridgePort=<port>` (only when bridge is enabled)
|
- `bridgePort=<port>` (only when bridge is enabled)
|
||||||
- `canvasPort=<port>` (only when the canvas host is running; enabled by default; default `18793`)
|
- `canvasPort=<port>` (only when the canvas host is running; enabled by default; default `18793`)
|
||||||
- `tailnetDns=<magicdns>` (optional hint; may be absent)
|
- `tailnetDns=<magicdns>` (optional hint; auto-detected from Tailscale when available; may be absent)
|
||||||
|
|
||||||
## Debugging on macOS
|
## Debugging on macOS
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ Bonjour/DNS-SD often escapes bytes in service instance names as decimal `\\DDD`
|
|||||||
- `bridge.bind` / `bridge.port` in `~/.clawdis/clawdis.json` control bridge bind/port (preferred).
|
- `bridge.bind` / `bridge.port` in `~/.clawdis/clawdis.json` control bridge bind/port (preferred).
|
||||||
- `CLAWDIS_BRIDGE_HOST` / `CLAWDIS_BRIDGE_PORT` still work as a back-compat override when `bridge.bind` / `bridge.port` are not set.
|
- `CLAWDIS_BRIDGE_HOST` / `CLAWDIS_BRIDGE_PORT` still work as a back-compat override when `bridge.bind` / `bridge.port` are not set.
|
||||||
- `CLAWDIS_SSH_PORT` overrides the SSH port advertised in `_clawdis-bridge._tcp`.
|
- `CLAWDIS_SSH_PORT` overrides the SSH port advertised in `_clawdis-bridge._tcp`.
|
||||||
- `CLAWDIS_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS) in `_clawdis-bridge._tcp` (wide-area discovery uses `clawdis.internal.` automatically when enabled).
|
- `CLAWDIS_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS) in `_clawdis-bridge._tcp`. If unset, the gateway auto-detects Tailscale and publishes the MagicDNS name when possible.
|
||||||
|
|
||||||
## Related docs
|
## Related docs
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Troubleshooting and beacon details: `docs/bonjour.md`.
|
|||||||
- `gatewayPort=18789` (loopback WS port; informational)
|
- `gatewayPort=18789` (loopback WS port; informational)
|
||||||
- `bridgePort=18790` (when bridge is enabled)
|
- `bridgePort=18790` (when bridge is enabled)
|
||||||
- `canvasPort=18793` (when the canvas host is running; enabled by default)
|
- `canvasPort=18793` (when the canvas host is running; enabled by default)
|
||||||
- `tailnetDns=<magicdns>` (optional hint)
|
- `tailnetDns=<magicdns>` (optional hint; auto-detected when Tailscale is available)
|
||||||
|
|
||||||
Disable/override:
|
Disable/override:
|
||||||
- `CLAWDIS_DISABLE_BONJOUR=1` disables advertising.
|
- `CLAWDIS_DISABLE_BONJOUR=1` disables advertising.
|
||||||
@@ -63,14 +63,14 @@ Disable/override:
|
|||||||
- `bridge.bind` / `bridge.port` in `~/.clawdis/clawdis.json` control bridge bind/port (preferred).
|
- `bridge.bind` / `bridge.port` in `~/.clawdis/clawdis.json` control bridge bind/port (preferred).
|
||||||
- `CLAWDIS_BRIDGE_HOST` / `CLAWDIS_BRIDGE_PORT` still work as a back-compat override when `bridge.bind` / `bridge.port` are not set.
|
- `CLAWDIS_BRIDGE_HOST` / `CLAWDIS_BRIDGE_PORT` still work as a back-compat override when `bridge.bind` / `bridge.port` are not set.
|
||||||
- `CLAWDIS_SSH_PORT` overrides the SSH port advertised in the bridge beacon (defaults to 22).
|
- `CLAWDIS_SSH_PORT` overrides the SSH port advertised in the bridge beacon (defaults to 22).
|
||||||
- `CLAWDIS_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS) in the bridge beacon.
|
- `CLAWDIS_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS) in the bridge beacon (auto-detected if unset).
|
||||||
|
|
||||||
### 2) Tailnet (cross-network)
|
### 2) Tailnet (cross-network)
|
||||||
|
|
||||||
For London/Vienna style setups, Bonjour won’t help. The recommended “direct” target is:
|
For London/Vienna style setups, Bonjour won’t help. The recommended “direct” target is:
|
||||||
- Tailscale MagicDNS name (preferred) or a stable tailnet IP.
|
- Tailscale MagicDNS name (preferred) or a stable tailnet IP.
|
||||||
|
|
||||||
If the gateway can detect it is running under Tailscale, it can publish `tailnetDns` as an optional hint for clients.
|
If the gateway can detect it is running under Tailscale, it publishes `tailnetDns` as an optional hint for clients.
|
||||||
|
|
||||||
### 3) Manual / SSH target
|
### 3) Manual / SSH target
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import {
|
|||||||
pickPrimaryTailnetIPv4,
|
pickPrimaryTailnetIPv4,
|
||||||
pickPrimaryTailnetIPv6,
|
pickPrimaryTailnetIPv6,
|
||||||
} from "../infra/tailnet.js";
|
} from "../infra/tailnet.js";
|
||||||
|
import { getTailnetHostname } from "../infra/tailscale.js";
|
||||||
import {
|
import {
|
||||||
defaultVoiceWakeTriggers,
|
defaultVoiceWakeTriggers,
|
||||||
loadVoiceWakeConfig,
|
loadVoiceWakeConfig,
|
||||||
@@ -89,6 +90,7 @@ import {
|
|||||||
import { logError, logInfo, logWarn } from "../logger.js";
|
import { logError, logInfo, logWarn } from "../logger.js";
|
||||||
import { getChildLogger, getResolvedLoggerSettings } from "../logging.js";
|
import { getChildLogger, getResolvedLoggerSettings } from "../logging.js";
|
||||||
import { setCommandLaneConcurrency } from "../process/command-queue.js";
|
import { setCommandLaneConcurrency } from "../process/command-queue.js";
|
||||||
|
import { runExec } from "../process/exec.js";
|
||||||
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
|
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { monitorTelegramProvider } from "../telegram/monitor.js";
|
import { monitorTelegramProvider } from "../telegram/monitor.js";
|
||||||
@@ -174,6 +176,20 @@ function formatBonjourInstanceName(displayName: string) {
|
|||||||
return `${trimmed} (Clawdis)`;
|
return `${trimmed} (Clawdis)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveTailnetDnsHint(): Promise<string | undefined> {
|
||||||
|
const envRaw = process.env.CLAWDIS_TAILNET_DNS?.trim();
|
||||||
|
const env = envRaw && envRaw.length > 0 ? envRaw.replace(/\.$/, "") : "";
|
||||||
|
if (env) return env;
|
||||||
|
|
||||||
|
const exec: typeof runExec = (command, args) =>
|
||||||
|
runExec(command, args, { timeoutMs: 1500, maxBuffer: 200_000 });
|
||||||
|
try {
|
||||||
|
return await getTailnetHostname(exec);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type GatewaySessionsDefaults = {
|
type GatewaySessionsDefaults = {
|
||||||
model: string | null;
|
model: string | null;
|
||||||
contextTokens: number | null;
|
contextTokens: number | null;
|
||||||
@@ -2048,12 +2064,7 @@ export async function startGatewayServer(
|
|||||||
? sshPortParsed
|
? sshPortParsed
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const tailnetDnsEnv = process.env.CLAWDIS_TAILNET_DNS?.trim();
|
const tailnetDns = await resolveTailnetDnsHint();
|
||||||
const tailnetDns = wideAreaDiscoveryEnabled
|
|
||||||
? WIDE_AREA_DISCOVERY_DOMAIN
|
|
||||||
: tailnetDnsEnv && tailnetDnsEnv.length > 0
|
|
||||||
? tailnetDnsEnv
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const bonjour = await startGatewayBonjourAdvertiser({
|
const bonjour = await startGatewayBonjourAdvertiser({
|
||||||
instanceName: formatBonjourInstanceName(machineDisplayName),
|
instanceName: formatBonjourInstanceName(machineDisplayName),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { promptYesNo } from "../cli/prompt.js";
|
import { promptYesNo } from "../cli/prompt.js";
|
||||||
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
|
import { danger, info, isVerbose, logVerbose, warn } from "../globals.js";
|
||||||
@@ -7,20 +8,37 @@ import { ensureBinary } from "./binaries.js";
|
|||||||
|
|
||||||
export async function getTailnetHostname(exec: typeof runExec = runExec) {
|
export async function getTailnetHostname(exec: typeof runExec = runExec) {
|
||||||
// Derive tailnet hostname (or IP fallback) from tailscale status JSON.
|
// Derive tailnet hostname (or IP fallback) from tailscale status JSON.
|
||||||
const { stdout } = await exec("tailscale", ["status", "--json"]);
|
const candidates = [
|
||||||
const parsed = stdout ? (JSON.parse(stdout) as Record<string, unknown>) : {};
|
"tailscale",
|
||||||
const self =
|
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
|
||||||
typeof parsed.Self === "object" && parsed.Self !== null
|
];
|
||||||
? (parsed.Self as Record<string, unknown>)
|
let lastError: unknown;
|
||||||
: undefined;
|
|
||||||
const dns =
|
for (const candidate of candidates) {
|
||||||
typeof self?.DNSName === "string" ? (self.DNSName as string) : undefined;
|
if (candidate.startsWith("/") && !existsSync(candidate)) continue;
|
||||||
const ips = Array.isArray(self?.TailscaleIPs)
|
try {
|
||||||
? (self.TailscaleIPs as string[])
|
const { stdout } = await exec(candidate, ["status", "--json"]);
|
||||||
: [];
|
const parsed = stdout
|
||||||
if (dns && dns.length > 0) return dns.replace(/\.$/, "");
|
? (JSON.parse(stdout) as Record<string, unknown>)
|
||||||
if (ips.length > 0) return ips[0];
|
: {};
|
||||||
throw new Error("Could not determine Tailscale DNS or IP");
|
const self =
|
||||||
|
typeof parsed.Self === "object" && parsed.Self !== null
|
||||||
|
? (parsed.Self as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const dns =
|
||||||
|
typeof self?.DNSName === "string" ? (self.DNSName as string) : undefined;
|
||||||
|
const ips = Array.isArray(self?.TailscaleIPs)
|
||||||
|
? (self.TailscaleIPs as string[])
|
||||||
|
: [];
|
||||||
|
if (dns && dns.length > 0) return dns.replace(/\.$/, "");
|
||||||
|
if (ips.length > 0) return ips[0];
|
||||||
|
throw new Error("Could not determine Tailscale DNS or IP");
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError ?? new Error("Could not determine Tailscale DNS or IP");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureGoInstalled(
|
export async function ensureGoInstalled(
|
||||||
|
|||||||
Reference in New Issue
Block a user