diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index d5e1f4eaa..eb07f9ce2 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -48,6 +48,7 @@ type GatewayRunOpts = { rawStreamPath?: unknown; dev?: boolean; reset?: boolean; + bonjour?: boolean; }; const gatewayLog = createSubsystemLogger("gateway"); @@ -89,6 +90,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) { process.env.OPENCLAW_RAW_STREAM_PATH = rawStreamPath; } + // Handle --no-bonjour flag to disable mDNS/Bonjour advertising + if (opts.bonjour === false) { + process.env.OPENCLAW_DISABLE_BONJOUR = "1"; + } + if (devMode) { await ensureDevGatewayConfig({ reset: Boolean(opts.reset) }); } @@ -350,6 +356,7 @@ export function addGatewayRunCommand(cmd: Command): Command { .option("--compact", 'Alias for "--ws-log compact"', false) .option("--raw-stream", "Log raw model stream events to jsonl", false) .option("--raw-stream-path ", "Raw stream jsonl path") + .option("--no-bonjour", "Disable mDNS/Bonjour service advertising", false) .action(async (opts) => { await runGatewayCommand(opts); }); diff --git a/src/infra/bonjour-ciao.ts b/src/infra/bonjour-ciao.ts index 17df4e78c..401669df1 100644 --- a/src/infra/bonjour-ciao.ts +++ b/src/infra/bonjour-ciao.ts @@ -2,11 +2,31 @@ import { logDebug } from "../logger.js"; import { formatBonjourError } from "./bonjour-errors.js"; +// Error patterns from @homebridge/ciao that should not crash the gateway +// These are typically transient network/mDNS issues that resolve on their own +const BONJOUR_TRANSIENT_ERRORS = [ + "CIAO ANNOUNCEMENT CANCELLED", + "REACHED ILLEGAL STATE", // IPv4 address changes during network interface churn + "IPV4 ADDRESS CHANGED", + "IPV6 ADDRESS CHANGED", + "MDNSSERVER", + "NETWORK INTERFACE", // Network interface changes (sleep/wake, WiFi reconnect) +]; + export function ignoreCiaoCancellationRejection(reason: unknown): boolean { const message = formatBonjourError(reason).toUpperCase(); - if (!message.includes("CIAO ANNOUNCEMENT CANCELLED")) { + const errorName = reason instanceof Error ? reason.name?.toUpperCase() : ""; + + // Check for transient mDNS/Bonjour error patterns + const isTransientError = BONJOUR_TRANSIENT_ERRORS.some((pattern) => message.includes(pattern)); + + // Also catch AssertionError from MDNServer (common during network changes) + const isAssertionError = errorName === "ASSERTIONERROR" && message.includes("MDNS"); + + if (!isTransientError && !isAssertionError) { return false; } + logDebug(`bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`); return true; } diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index 4d2a48d23..1835849cb 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -34,6 +34,8 @@ const TRANSIENT_NETWORK_CODES = new Set([ "UND_ERR_SOCKET", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", + // mDNS/Bonjour errors from @homebridge/ciao + "ERR_ASSERTION", // IPv4 address changes during network interface churn ]); function getErrorCause(err: unknown): unknown { @@ -99,6 +101,23 @@ export function isTransientNetworkError(err: unknown): boolean { return err.errors.some((e) => isTransientNetworkError(e)); } + // Handle mDNS/Bonjour errors from @homebridge/ciao + // These are typically triggered by network interface changes (sleep/wake, WiFi reconnect) + if (err instanceof Error) { + const errorName = err.name?.toUpperCase() || ""; + const message = err.message?.toUpperCase() || ""; + + // AssertionError from MDNServer during network interface changes + if (errorName === "ASSERTIONERROR" && message.includes("IPV4 ADDRESS CHANGED")) { + return true; + } + + // Other mDNS-related transient errors + if (message.includes("MDNSSERVER") && message.includes("ILLEGAL STATE")) { + return true; + } + } + return false; }