沙盒化代理无法连接到浏览器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.network,network: 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 浏览器自动化架构涉及两个不同的运行时上下文:
- 浏览器边车容器:一个运行启用了 CDP 的 Chromium 的 Docker 容器,连接到 `openclaw-sandbox-browser` Docker 网络。
- 代理沙箱容器:一个可选的 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 的流量永远不会离开该命名空间。
缺失的逻辑
代码缺少条件逻辑来检测:
- 代理是否在 Docker 容器内运行
- 代理容器是否与浏览器容器共享同一 Docker 网络
- 因此 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 refused | CDP 主机无法到达时的网络级故障 |
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 执行或交互式点击流程的网站。