April 17, 2026 • 版本: 2026.4.5

沙盒化代理无法连接到浏览器CDP — ensureSandboxBrowser中硬编码了127.0.0.1

当沙盒模式设置为"all"时,CDP WebSocket连接失败,因为ensureSandboxBrowser将CDP端口映射到主机127.0.0.1,而在Docker沙盒化代理容器内部无法访问该地址。

🔍 症状

主要错误表现

当代理运行在 Docker 容器内且配置为 agents.defaults.sandbox.mode: "all" 时,任何对 browser 工具的调用都会导致连接失败:

Error: Chrome CDP websocket for profile "openclaw" is not reachable after start.
    at BrowserTool._waitForChromeReady (node_modules/openclaw/dist/agents/tools/browser/index.js:XXXX:XX)
    at BrowserTool.open (node_modules/openclaw/dist/agents/tools/browser/index.js:XXXX:XX)
    at ...

网络诊断证据

从沙箱化代理容器内部进行连接测试,揭示了根本问题:

# Attempting to reach the CDP port via host loopback
$ curl -v http://127.0.0.1:9222
curl: (7) Failed to connect to 127.0.0.1 port 9222 after 0ms: Connection refused

# Confirming 127.0.0.1 is the container's own loopback
$ ip route show
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2

# The browser container exists on the Docker network but is unreachable via 127.0.0.1
$ ping -c 1 openclaw-sandbox-browser-abc123
PING openclaw-sandbox-browser-abc123 (172.18.0.3) 56(84) bytes of data.
64 bytes from openclaw-sandbox-browser-abc123 (172.18.0.3): icmp_seq=1 ttl=64 time=0.1 ms

触发问题的配置

以下最小配置可复现该故障:

{
  "agents": {
    "defaults": {
      "sandbox": {
        "mode": "all",
        "scope": "agent",
        "workspaceAccess": "rw"
      }
    }
  },
  "tools": {
    "sandbox": {
      "tools": {
        "alsoAllow": ["browser"]
      }
    }
  }
}

已记录的变通方案失败情况

配置尝试观察到的结果
默认(无 docker.networknetwork: none代理无网络;CDP 无法访问
sandbox.docker.network: "openclaw-sandbox-browser"仍然失败——代码使用 127.0.0.1 而非容器 DNS
sandbox.docker.extraHosts: ["host.docker.internal:host-gateway"]127.0.0.1 仍然解析到容器回环
browser.profiles.remote-chrome.cdpUrl = "ws://127.0.0.1:9222"attachOnly: true"Browser attachOnly is enabled and profile 'remote-chrome' is not running."
相同配置但无 attachOnly"PortInUseError: Port 9222 is already in use."

🧠 根因分析

架构概述

OpenClaw 浏览器自动化架构涉及两个不同的运行时上下文:

  1. 浏览器边车容器:一个运行启用了 CDP 的 Chromium 的 Docker 容器,连接到 `openclaw-sandbox-browser` Docker 网络。
  2. 代理沙箱容器:一个可选的 Docker 容器,运行代理代码,也可能连接到 `openclaw-sandbox-browser` 网络。

有缺陷的端口映射策略

src/agents/sandbox/browser.ts 中的 ensureSandboxBrowser 函数使用以下模式创建浏览器容器并发布 CDP 端口:

// Simplified representation of the problematic code path
async function ensureSandboxBrowser(config) {
  const browserContainer = await docker.createContainer({
    // ... container config ...
    HostConfig: {
      PortBindings: {
        "9222/tcp": [{ HostIp: "127.0.0.1", HostPort: cdpPort }]  // ← THE BUG
      }
    }
  });

  // Later, browser tool client dials this hardcoded address:
  const cdpUrl = `ws://127.0.0.1:${cdpPort}`;
  return { container: browserContainer, cdpUrl };
}

为什么 127.0.0.1 在容器内会失败

网络拓扑根据客户端位置而有所不同:

客户端位置127.0.0.1 解析为浏览器容器可达?
主机主机回环接口是(通过端口绑定)
代理容器代理容器的回环否(独立的网络命名空间)

浏览器容器的端口绑定到主机网络命名空间上的 127.0.0.1。代理容器有自己的隔离回环接口——从代理容器内部发往 127.0.0.1 的流量永远不会离开该命名空间。

缺失的逻辑

代码缺少条件逻辑来检测:

  1. 代理是否在 Docker 容器内运行
  2. 代理容器是否与浏览器容器共享同一 Docker 网络
  3. 因此 CDP URL 应该使用 Docker 的内部 DNS 解析

Docker 的用户定义桥接网络通过容器名称在容器之间提供自动 DNS 解析。如果代理容器在 openclaw-sandbox-browser 上,而浏览器容器名称是 openclaw-sandbox-browser-abc123,则代理可以通过该 DNS 名称在任何端口上访问浏览器。

代码路径分析

agent.sandbox.run() └── ensureSandboxBrowser() ├── 在 openclaw-sandbox-browser 网络上创建浏览器容器 ├── 绑定端口到 127.0.0.1(仅限主机) └── 返回 cdpUrl = “ws://127.0.0.1:9222”

browser-tool.client.connect() └── 尝试连接到 cdpUrl └── 如果代理是容器化的:连接被拒绝(容器回环)

🛠️ 逐步修复

前置配置

在应用修复之前,确保代理沙箱配置为加入浏览器容器网络。将以下内容添加到 OpenClaw 配置中:

{
  "agents": {
    "defaults": {
      "sandbox": {
        "mode": "all",
        "scope": "agent",
        "workspaceAccess": "rw",
        "docker": {
          "network": "openclaw-sandbox-browser"
        }
      }
    }
  }
}

修复方案:修改 ensureSandboxBrowser 以使用容器 DNS

找到 src/agents/sandbox/browser.ts 并应用以下补丁:

修复前(有问题):

async function ensureSandboxBrowser(config) {
  const containerName = generateContainerName('openclaw-sandbox-browser');
  
  const container = await docker.createContainer({
    name: containerName,
    Image: config.browserImage,
    HostConfig: {
      NetworkMode: 'openclaw-sandbox-browser',
      PortBindings: {
        "9222/tcp": [{ HostIp: "127.0.0.1", HostPort: String(cdpPort) }]
      }
    }
  });

  const cdpUrl = `ws://127.0.0.1:${cdpPort}`;
  return { container, cdpUrl, containerName };
}

修复后(已修复):

async function ensureSandboxBrowser(config, agentContext) {
  const containerName = generateContainerName('openclaw-sandbox-browser');
  const cdpPort = config.cdpPort || 9222;
  
  // 根据运行时上下文确定正确的 CDP 主机
  let cdpHost = "127.0.0.1";
  
  if (agentContext?.isSandboxed && agentContext?.dockerNetwork === 'openclaw-sandbox-browser') {
    // 代理容器共享浏览器网络;使用内部 DNS
    cdpHost = containerName;
    // 当代理和浏览器共享网络时,不需要主机端口绑定
    logger.info(`检测到沙箱化代理;使用内部 DNS (${cdpHost}) 进行 CDP 连接`);
  }

  const createOptions = {
    name: containerName,
    Image: config.browserImage,
    HostConfig: {
      NetworkMode: 'openclaw-sandbox-browser',
      // 仅当代理不在同一网络时才绑定到主机端口
      PortBindings: cdpHost === "127.0.0.1" 
        ? { "9222/tcp": [{ HostIp: "127.0.0.1", HostPort: String(cdpPort) }] }
        : {}  // 内部网络访问不需要主机绑定
    }
  };

  const container = await docker.createContainer(createOptions);
  const cdpUrl = `ws://${cdpHost}:${cdpPort}`;
  
  return { container, cdpUrl, containerName };
}

替代方案:基于环境的配置(非代码修复)

如果无法修改源代码,请通过直接设置 CDP URL 来配置浏览器工具使用容器的内部端点:

{
  "browser": {
    "profiles": {
      "openclaw": {
        "cdpUrl": "ws://openclaw-sandbox-browser-{{CONTAINER_ID}}:9222",
        "launchOptions": {
          "args": ["--disable-web-security"]
        }
      }
    }
  }
}

注意:这需要在配置时知道浏览器容器名称。容器名称是动态生成的;使用一致的前缀或使用 docker ps --filter name=openclaw-sandbox-browser 检查。

Docker 网络验证

在启动 OpenClaw 之前确保网络存在:

# Create the shared network if it doesn't exist
$ docker network create openclaw-sandbox-browser 2>/dev/null || true

# Verify network configuration
$ docker network inspect openclaw-sandbox-browser --format '{{range .IPAM.Config}}Subnet: {{.Subnet}}{{end}}'
Subnet: 172.18.0.0/16

🧪 验证

步骤 1:验证浏览器容器成功启动

$ docker ps -a --filter "name=openclaw-sandbox-browser" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

NAMES                        STATUS          PORTS
openclaw-sandbox-browser-abc123   Up 2 minutes   0.0.0.0:9222->9222/tcp

步骤 2:确认代理容器在同一网络上

$ docker inspect openclaw-agent-xyz789 --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}'
openclaw-sandbox-browser

步骤 3:从代理容器测试 CDP 连接

$ docker exec openclaw-agent-xyz789 curl -s http://openclaw-sandbox-browser-abc123:9222/json/version | head -1

{
  "Browser": "Chromium/120.0.6099.109",
  "Protocol-Version": "1.3",
  "User-Agent": "Mozilla/5.0 ...",
  "V8-Version": "12.0.6099.109",
  "WebKit-Version": "537.36 ..."
}

步骤 4:验证 WebSocket 升级

$ docker exec openclaw-agent-xyz789 curl -v \
  --no-buffer \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  http://openclaw-sandbox-browser-abc123:9222 \
  2>&1 | grep -E "(HTTP|Upgrade: websocket)"

< HTTP/1.1 101 Switching Protocols
< Upgrade: websocket
< Connection: Upgrade

步骤 5:使用 OpenClaw 进行功能测试

通过 CLI 或 API 执行基于浏览器的任务:

$ openclaw run --task "Navigate to example.com and return the page title"

[INFO] Sandbox mode: all
[INFO] Starting agent in container...
[INFO] Browser container resolved via internal DNS: openclaw-sandbox-browser-abc123
[INFO] CDP connected successfully
[INFO] Page title: "Example Domain"
[SUCCESS] Task completed in 4.2s

步骤 6:确认主机侧访问仍然正常

从主机(不在任何容器内):

$ curl -s http://127.0.0.1:9222/json/version | head -1

{
  "Browser": "Chromium/120.0.6099.109",

预期结果:主机(127.0.0.1)和内部容器 DNS(openclaw-sandbox-browser-*)均可访问。

⚠️ 常见陷阱

1. 网络模式不匹配

代理沙箱必须显式加入 openclaw-sandbox-browser 网络。如果省略,代理默认为 network: none(无网络)或默认桥接网络。

// 错误:缺少网络配置
"sandbox": {
  "mode": "all"
  // 代理将没有网络或网络错误
}

// 正确:显式网络连接
"sandbox": {
  "mode": "all",
  "docker": {
    "network": "openclaw-sandbox-browser"
  }
}

2. 容器启动时的竞态条件

浏览器容器可能尚未完全就绪时,代理就尝试 CDP 连接。实现就绪检查:

async function waitForCdpReady(host, port, maxAttempts = 30) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const response = await fetch(`http://${host}:${port}/json/version`);
      if (response.ok) return true;
    } catch (e) {
      await new Promise(r => setTimeout(r, 1000));
    }
  }
  throw new Error(`CDP 在 ${maxAttempts} 次尝试后仍未就绪`);
}

3. 动态容器名称

浏览器容器名称使用随机后缀生成。使用一致的命名前缀进行 DNS 解析:

// 在 browser.ts 中,尽可能使用确定性命名:
const containerName = config.customName || `openclaw-sandbox-browser`;

或者,使用 Docker 的 --network-alias 创建稳定别名:

HostConfig: {
  NetworkMode: 'openclaw-sandbox-browser',
  DNS: ['openclaw-sandbox-browser-alias']  // 使用此别名作为 CDP 主机
}

4. macOS Docker Desktop 特性

在带有 WSL2 的 Docker Desktop 上,host.docker.internal 额外主机映射到 WSL2 VM,而非实际主机。内部 DNS 方法仍然正常工作,因为它使用 Docker 的容器 DNS。

5. IPv6 回环

某些系统优先使用 ::1(IPv6 回环)而非 127.0.0.1(IPv4)。确保 CDP URL 显式使用 IPv4:

cdpUrl = "ws://127.0.0.1:9222";  // 显式 IPv4
// 而非: "ws://localhost:9222";   // 可能解析到 ::1

6. 共享环境中的端口冲突

如果同一主机上存在多个浏览器容器,请确保使用不同的 CDP 端口:

{
  "browser": {
    "profiles": {
      "openclaw": {
        "cdpPort": 29222  // 非标准端口以避免冲突
      }
    }
  }
}

🔗 相关错误

相关问题

  • #52662 — 外部 CDP 端点配置:建议暴露一个 `externalCdpEndpoint` 配置选项,允许附加外部管理的浏览器。attachOnly 模式失败,因为就绪探针是基于主机 PID 的,无法识别容器启动的 Chrome。
  • #58606 — 浏览器容器启动但 CDP 端口不可达:与本问题根本原因相同,但角度是从端口暴露而非沙箱网络的角度。
  • #64383 — 移除 socat CDP 中间层:关于消除 CDP 桥接代理层的讨论。如果解决,将简化网络模型,但不会直接解决 127.0.0.1 硬编码问题。

相关错误消息

错误代码/消息描述
Chrome CDP websocket for profile “X” is not reachable after start主要症状;表示 CDP 连接超时或被拒绝
PortInUseError: Port 9222 is already in use当未设置 attachOnly: true 但浏览器已在运行时发生
Browser attachOnly is enabled and profile ‘X’ is not running就绪探针无法检测到容器启动的 Chrome
Connection refusedCDP 主机无法到达时的网络级故障
ETIMEDOUT当代理容器没有到浏览器的网络访问权限时发生

受影响版本

  • OpenClaw 2026.4.x:已确认受影响;ensureSandboxBrowser 硬编码 127.0.0.1
  • OpenClaw 2026.3.x:可能受影响;相同的代码路径
  • OpenClaw 2026.5.x:修复待处理;预计在下一个次要版本中发布补丁

变通方案状态

在修复合并之前,唯一的完全可用变通方案是:

{
  "browser": {
    "enabled": false
  }
}

这将完全禁用浏览器工具,回退到使用 web_fetch 进行所有基于 HTTP 的 Web 访问。这不适用于需要 JavaScript 执行或交互式点击流程的网站。

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。