fix(gateway): auto-bind to 0.0.0.0 inside container environments

This commit is contained in:
openperf
2026-04-06 18:26:59 +08:00
committed by Peter Steinberger
parent 4a91b4f3a5
commit c857e93735
9 changed files with 445 additions and 32 deletions

View File

@@ -16,6 +16,7 @@ import type { ServiceConfigAudit } from "../../daemon/service-audit.js";
import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { trimToUndefined } from "../../gateway/credentials.js";
import { defaultGatewayBindMode } from "../../gateway/net.js";
import {
inspectBestEffortPrimaryTailnetIPv4,
resolveBestEffortGatewayBindHostForDisplay,
@@ -260,7 +261,9 @@ async function resolveGatewayStatusSummary(params: {
const portSource: GatewayStatusSummary["portSource"] = portFromArgs
? "service args"
: "env/config";
const bindMode: GatewayBindMode = params.daemonCfg.gateway?.bind ?? "loopback";
const statusTailscaleMode = params.daemonCfg.gateway?.tailscale?.mode ?? "off";
const bindMode: GatewayBindMode =
params.daemonCfg.gateway?.bind ?? defaultGatewayBindMode(statusTailscaleMode);
const customBindHost = params.daemonCfg.gateway?.customBindHost;
const { bindHost, warning: bindHostWarning } = await resolveBestEffortGatewayBindHostForDisplay({
bindMode,

View File

@@ -2,7 +2,11 @@ import fs from "node:fs";
import path from "node:path";
import type { Command } from "commander";
import { readSecretFromFile } from "../../acp/secret-file.js";
import type { GatewayAuthMode, GatewayTailscaleMode } from "../../config/config.js";
import type {
GatewayAuthMode,
GatewayBindMode,
GatewayTailscaleMode,
} from "../../config/config.js";
import {
CONFIG_PATH,
loadConfig,
@@ -12,6 +16,7 @@ import {
} from "../../config/config.js";
import { hasConfiguredSecretInput } from "../../config/types.secrets.js";
import { resolveGatewayAuth } from "../../gateway/auth.js";
import { defaultGatewayBindMode, isContainerEnvironment } from "../../gateway/net.js";
import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
import { setVerbose } from "../../globals.js";
@@ -294,20 +299,17 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
defaultRuntime.error("Invalid port");
defaultRuntime.exit(1);
}
const bindRaw = toOptionString(opts.bind) ?? cfg.gateway?.bind ?? "loopback";
const bind =
bindRaw === "loopback" ||
bindRaw === "lan" ||
bindRaw === "auto" ||
bindRaw === "custom" ||
bindRaw === "tailnet"
? bindRaw
: null;
if (!bind) {
// Only capture the *explicit* bind value here. The container-aware
// default is deferred until after Tailscale mode is known (see below)
// so that Tailscale's loopback constraint is respected.
const VALID_BIND_MODES = new Set<string>(["loopback", "lan", "auto", "custom", "tailnet"]);
const bindExplicitRawStr = (toOptionString(opts.bind) ?? cfg.gateway?.bind)?.trim() || undefined;
if (bindExplicitRawStr !== undefined && !VALID_BIND_MODES.has(bindExplicitRawStr)) {
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "tailnet", "auto", or "custom")');
defaultRuntime.exit(1);
return;
}
const bindExplicitRaw = bindExplicitRawStr as GatewayBindMode | undefined;
if (process.env.OPENCLAW_SERVICE_MARKER?.trim()) {
const stale = cleanStaleGatewayProcessesSync(port);
if (stale.length > 0) {
@@ -340,11 +342,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
}
// After killing, verify the port is actually bindable (handles TIME_WAIT).
const bindProbeHost =
bind === "loopback"
bindExplicitRaw === "loopback"
? "127.0.0.1"
: bind === "lan"
: bindExplicitRaw === "lan"
? "0.0.0.0"
: bind === "custom"
: bindExplicitRaw === "custom"
? toOptionString(cfg.gateway?.customBindHost)
: undefined;
const bindWaitMs = await waitForPortBindable(port, {
@@ -383,6 +385,15 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
defaultRuntime.exit(1);
return;
}
// Now that Tailscale mode is known, compute the effective bind mode.
const effectiveTailscaleMode = tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off";
const bind = (bindExplicitRaw ?? defaultGatewayBindMode(effectiveTailscaleMode)) as
| "loopback"
| "lan"
| "auto"
| "custom"
| "tailnet";
let passwordRaw: string | undefined;
try {
passwordRaw = resolveGatewayPasswordOption(opts);
@@ -487,7 +498,14 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
defaultRuntime.error(
[
`Refusing to bind gateway to ${bind} without auth.`,
"Set gateway.auth.token/password (or OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD) or pass --token/--password.",
...(isContainerEnvironment()
? [
"Container environment detected \u2014 the gateway defaults to bind=auto (0.0.0.0) for port-forwarding compatibility.",
"Set OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD, or pass --token/--password to start with auth.",
]
: [
"Set gateway.auth.token/password (or OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD) or pass --token/--password.",
]),
...authHints,
]
.filter(Boolean)