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? {
|
||||
let host = gateway.tailnetDns ?? gateway.lanHost
|
||||
let host = self.sanitizedTailnetHost(gateway.tailnetDns) ?? gateway.lanHost
|
||||
guard let host else { return nil }
|
||||
let user = NSUserName()
|
||||
var target = "\(user)@\(host)"
|
||||
@@ -96,6 +96,16 @@ struct GatewayDiscoveryInlineList: View {
|
||||
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 {
|
||||
if selected { return Color.accentColor.opacity(0.12) }
|
||||
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)
|
||||
- `bridgePort=<port>` (only when bridge is enabled)
|
||||
- `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
|
||||
|
||||
@@ -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).
|
||||
- `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_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
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Troubleshooting and beacon details: `docs/bonjour.md`.
|
||||
- `gatewayPort=18789` (loopback WS port; informational)
|
||||
- `bridgePort=18790` (when bridge is enabled)
|
||||
- `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:
|
||||
- `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).
|
||||
- `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_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)
|
||||
|
||||
For London/Vienna style setups, Bonjour won’t help. The recommended “direct” target is:
|
||||
- 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
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
pickPrimaryTailnetIPv4,
|
||||
pickPrimaryTailnetIPv6,
|
||||
} from "../infra/tailnet.js";
|
||||
import { getTailnetHostname } from "../infra/tailscale.js";
|
||||
import {
|
||||
defaultVoiceWakeTriggers,
|
||||
loadVoiceWakeConfig,
|
||||
@@ -89,6 +90,7 @@ import {
|
||||
import { logError, logInfo, logWarn } from "../logger.js";
|
||||
import { getChildLogger, getResolvedLoggerSettings } from "../logging.js";
|
||||
import { setCommandLaneConcurrency } from "../process/command-queue.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { monitorTelegramProvider } from "../telegram/monitor.js";
|
||||
@@ -174,6 +176,20 @@ function formatBonjourInstanceName(displayName: string) {
|
||||
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 = {
|
||||
model: string | null;
|
||||
contextTokens: number | null;
|
||||
@@ -2048,12 +2064,7 @@ export async function startGatewayServer(
|
||||
? sshPortParsed
|
||||
: undefined;
|
||||
|
||||
const tailnetDnsEnv = process.env.CLAWDIS_TAILNET_DNS?.trim();
|
||||
const tailnetDns = wideAreaDiscoveryEnabled
|
||||
? WIDE_AREA_DISCOVERY_DOMAIN
|
||||
: tailnetDnsEnv && tailnetDnsEnv.length > 0
|
||||
? tailnetDnsEnv
|
||||
: undefined;
|
||||
const tailnetDns = await resolveTailnetDnsHint();
|
||||
|
||||
const bonjour = await startGatewayBonjourAdvertiser({
|
||||
instanceName: formatBonjourInstanceName(machineDisplayName),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import chalk from "chalk";
|
||||
import { promptYesNo } from "../cli/prompt.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) {
|
||||
// Derive tailnet hostname (or IP fallback) from tailscale status JSON.
|
||||
const { stdout } = await exec("tailscale", ["status", "--json"]);
|
||||
const parsed = stdout ? (JSON.parse(stdout) as Record<string, unknown>) : {};
|
||||
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");
|
||||
const candidates = [
|
||||
"tailscale",
|
||||
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
|
||||
];
|
||||
let lastError: unknown;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.startsWith("/") && !existsSync(candidate)) continue;
|
||||
try {
|
||||
const { stdout } = await exec(candidate, ["status", "--json"]);
|
||||
const parsed = stdout
|
||||
? (JSON.parse(stdout) as Record<string, unknown>)
|
||||
: {};
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user