import { startGatewayBonjourAdvertiser } from "../infra/bonjour.js"; import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js"; import { resolveWideAreaDiscoveryDomain, writeWideAreaGatewayZone } from "../infra/widearea-dns.js"; import { formatBonjourInstanceName, resolveBonjourCliPath, resolveTailnetDnsHint, } from "./server-discovery.js"; export async function startGatewayDiscovery(params: { machineDisplayName: string; port: number; gatewayTls?: { enabled: boolean; fingerprintSha256?: string }; canvasPort?: number; wideAreaDiscoveryEnabled: boolean; wideAreaDiscoveryDomain?: string | null; tailscaleMode: "off" | "serve" | "funnel"; /** mDNS/Bonjour discovery mode (default: minimal). */ mdnsMode?: "off" | "minimal" | "full"; logDiscovery: { info: (msg: string) => void; warn: (msg: string) => void }; }) { let bonjourStop: (() => Promise) | null = null; const mdnsMode = params.mdnsMode ?? "minimal"; // mDNS can be disabled via config (mdnsMode: off) or env var. const bonjourEnabled = mdnsMode !== "off" && process.env.OPENCLAW_DISABLE_BONJOUR !== "1" && process.env.NODE_ENV !== "test" && !process.env.VITEST; const mdnsMinimal = mdnsMode !== "full"; const tailscaleEnabled = params.tailscaleMode !== "off"; const needsTailnetDns = bonjourEnabled || params.wideAreaDiscoveryEnabled; const tailnetDns = needsTailnetDns ? await resolveTailnetDnsHint({ enabled: tailscaleEnabled }) : undefined; const sshPortEnv = mdnsMinimal ? undefined : process.env.OPENCLAW_SSH_PORT?.trim(); const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN; const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0 ? sshPortParsed : undefined; const cliPath = mdnsMinimal ? undefined : resolveBonjourCliPath(); if (bonjourEnabled) { try { const bonjour = await startGatewayBonjourAdvertiser({ instanceName: formatBonjourInstanceName(params.machineDisplayName), gatewayPort: params.port, gatewayTlsEnabled: params.gatewayTls?.enabled ?? false, gatewayTlsFingerprintSha256: params.gatewayTls?.fingerprintSha256, canvasPort: params.canvasPort, sshPort, tailnetDns, cliPath, minimal: mdnsMinimal, }); bonjourStop = bonjour.stop; } catch (err) { params.logDiscovery.warn(`bonjour advertising failed: ${String(err)}`); } } if (params.wideAreaDiscoveryEnabled) { const wideAreaDomain = resolveWideAreaDiscoveryDomain({ configDomain: params.wideAreaDiscoveryDomain ?? undefined, }); if (!wideAreaDomain) { params.logDiscovery.warn( "discovery.wideArea.enabled is true, but no domain was configured; set discovery.wideArea.domain to enable unicast DNS-SD", ); return { bonjourStop }; } const tailnetIPv4 = pickPrimaryTailnetIPv4(); if (!tailnetIPv4) { params.logDiscovery.warn( "discovery.wideArea.enabled is true, but no Tailscale IPv4 address was found; skipping unicast DNS-SD zone update", ); } else { try { const tailnetIPv6 = pickPrimaryTailnetIPv6(); const result = await writeWideAreaGatewayZone({ domain: wideAreaDomain, gatewayPort: params.port, displayName: formatBonjourInstanceName(params.machineDisplayName), tailnetIPv4, tailnetIPv6: tailnetIPv6 ?? undefined, gatewayTlsEnabled: params.gatewayTls?.enabled ?? false, gatewayTlsFingerprintSha256: params.gatewayTls?.fingerprintSha256, tailnetDns, sshPort, cliPath: resolveBonjourCliPath(), }); params.logDiscovery.info( `wide-area DNS-SD ${result.changed ? "updated" : "unchanged"} (${wideAreaDomain} → ${result.zonePath})`, ); } catch (err) { params.logDiscovery.warn(`wide-area discovery update failed: ${String(err)}`); } } } return { bonjourStop }; }