Docker 网络模式配置指南

概述

OpenSandbox Server 在 Docker 运行时下支持两种网络模式:host 模式 和 bridge 模式。选择合适的网络模式,并正确配置端口范围,是保证多沙箱安全隔离和 SDK 正常通信的关键。

配置入口

在 ~/.sandbox.toml 配置文件的 [docker] 段中设置:

[docker]
# 可选值: "host" | "bridge" | "<自定义网络名>"
network_mode = "host"

# 可选:宿主机端口分配范围(默认 40000-60000)
# bridge 模式使用 Docker 端口映射
# host 模式使用进程级动态端口
port_range_min = 40000
port_range_max = 60000

# 当 server 运行在 Docker 容器内时,需要设置宿主机 IP
# bridge 模式下必须配置
host_ip = "host.docker.internal"

Host 模式

原理

  ┌──────────────────────────────────────────────────┐
  │                    宿主机                          │
  │                                                    │
  │  沙箱 C (无网络隔离)    沙箱 D (无网络隔离)         │
  │  ┌─────────────────┐  ┌─────────────────┐          │
  │  │ execd: 40001     │  │ execd: 40003     │         │
  │  │ HTTP:  40002     │  │ HTTP:  40004     │         │
  │  │ 磁盘: /mnt/disk1 │  │ 磁盘: /mnt/disk2 │         │
  │  └─────────────────┘  └─────────────────┘          │
  │                                                    │
  │  OpenSandbox Server (8080)                          │
  │                                                    │
  └──────────────────────────────────────────────────┘

沙箱容器直接使用宿主机的网络命名空间,没有网络隔离层。execd 进程直接绑定宿主机的 IP 和端口。

配置

[docker]
network_mode = "host"

# 端口范围配置(可选,默认 40000-60000)
port_range_min = 40000
port_range_max = 60000

# host 模式下不需要 host_ip
# host_ip = "host.docker.internal"

特点

特性说明
网络性能⭐ 最高——零 NAT 开销,直接使用宿主机网络栈
延迟最低——无 bridge 转发开销
端口管理每个沙箱动态分配唯一端口,通过 OPENSANDBOX_EXECD_PORT 注入 execd
SDK 通信直接访问 host:{allocated_port}/proxy/{port}
egress 网络策略❌ 不支持(networkPolicy 在 host 模式下被拒绝)
端口冲突✅ 动态分配避免(通过 socket.bind() 探测可用性)

端口分配

Host 模式下,OpenSandbox 不再使用 Docker 的端口映射,而是在创建沙箱时:

  1. 调用 allocate_port_bindings(["44772", "8080"]) 在 40000-60000 范围内分配两个可用端口
  2. 将分配端口写入容器标签(embedding-proxy-port、http-port)
  3. 将 execd 端口通过环境变量 OPENSANDBOX_EXECD_PORT 注入容器
  4. execd 进程启动时读取环境变量,绑定到分配端口而非默认 44772

端口冲突处理

# port_allocator.py 核心逻辑
def allocate_host_port(min_port, max_port):
    for _ in range(50):                    # 最多尝试 50 次
        port = random.randint(min_port, max_port)
        with socket.socket() as sock:
            try:
                sock.bind(("0.0.0.0", port))  # 探测端口是否空闲
                return port                   # 空闲 → 分配
            except OSError:
                continue                      # 被占用 → 重试
    return None  # 范围内无可用端口 → 报错

端口收回:沙箱删除后,execd 进程退出 → TCP socket 关闭 → 端口自动归还 OS 端口池。

向后兼容

旧沙箱(在本次改动前创建)没有 embedding-proxy-port label,endpoint 解析自动回退到原始格式:

if labels.get(SANDBOX_EMBEDDING_PROXY_PORT_LABEL):
    return self._resolve_host_mapped_endpoint(public_host, labels, port)
# 无 label → 向后兼容
endpoint = Endpoint(endpoint=f"{public_host}:{port}")

Bridge 模式

原理

  ┌──────────────────────────────────────────────────────┐
  │                    宿主机                              │
  │                                                        │
  │  OpenSandbox Server (8080)                             │
  │                                                        │
  │  ┌────── Docker Bridge (docker0) ─────────────────┐   │
  │  │                                                  │   │
  │  │  沙箱 C                       沙箱 D             │   │
  │  │  ┌─────────────────────┐    ┌─────────────────┐  │   │
  │  │  │ 容器内: 44772, 8080 │    │ 44772, 8080     │  │   │
  │  │  │ 宿主机映射:          │    │ 宿主机映射:      │  │   │
  │  │  │   40001→44772       │    │   40003→44772   │  │   │
  │  │  │   40002→8080        │    │   40004→8080    │  │   │
  │  │  └─────────────────────┘    └─────────────────┘  │   │
  │  └──────────────────────────────────────────────────┘   │
  │                                                        │
  └──────────────────────────────────────────────────────────┘

每个沙箱容器运行在独立的 Docker bridge 网络上,有独立的网络命名空间和 IP 地址。通过 Docker 端口映射将容器端口暴露到宿主机。

配置

[docker]
network_mode = "bridge"

# 端口范围配置(可选,默认 40000-60000)
port_range_min = 40000
port_range_max = 60000

# 当 server 运行在 Docker 容器内时,必须设置 host_ip
# 让 SDK 能访问宿主机上的端口映射
host_ip = "host.docker.internal"  

特点

特性说明
网络性能⭐⭐——少量 NAT 转发开销(通常可忽略)
延迟低——Docker bridge 内核级转发
端口管理每个沙箱的容器端口映射到不同宿主机端口
SDK 通信直接访问 host:{mapped_port}/proxy/{port}
egress 网络策略✅ 支持(需要额外配置 [egress] image)
网络隔离✅ 沙箱之间网络完全隔离
host_ip 配置Server 在容器内时必须设置

端口分配

Bridge 模式下,Docker 负责端口映射:

  1. Server 调用 allocate_port_bindings(["44772", "8080"]) 分配宿主机端口
  2. 构造 Docker host_config 的 port_bindings:
    host_config_kwargs["port_bindings"] = {
        "44772": ("0.0.0.0", 40001),  # 宿主机:40001 → 容器:44772
        "8080":  ("0.0.0.0", 40002),  # 宿主机:40002 → 容器:8080
    }
    
  3. Docker 自动在 iptables NAT 规则中创建端口转发
  4. execd 在容器内始终绑定 44772(不变),Docker 负责宿主机→容器的映射

关于 host_ip

当 OpenSandbox Server 自身运行在 Docker 容器内时,host_ip 必须配置:

[docker]
network_mode = "bridge"
host_ip = "host.docker.internal"  # macOS / Docker Desktop
# 或
host_ip = "10.0.0.100"           # 宿主机 LAN IP

SDK 获取 endpoint 后,会使用 host_ip 作为宿主机地址连接端口映射。如果不设置,SDK 得到的 endpoint 是容器内部 IP,无法从外部访问。

host_ip 在 host 模式下不需要,因为 execd 直接绑定宿主机网卡。


端口配置详解

端口范围配置

[docker]
port_range_min = 40000   # 端口分配下限(默认 40000)
port_range_max = 60000   # 端口分配上限(默认 60000)

每个沙箱占用的端口数

场景容器端口宿主机端口数
默认(无 egress)44772 (execd), 8080 (HTTP)2
有 egress sidecar44772, 8080, 18080 (egress API)3
Windows 系统44772, 8080, 3389 (RDP), 8006 (noVNC)4

端口占用计算

端口数 = 沙箱数量 × 每个沙箱所需端口

示例:
  5 个 agent × 2 沙箱 = 10 沙箱(默认模式)
  → 所需端口 = 10 × 2 = 20

  10 个 agent × 3 沙箱 = 30 沙箱(默认模式)
  → 所需端口 = 30 × 2 = 60

最大并行沙箱数(估算):
  (port_range_max - port_range_min) ÷ 每个沙箱端口数
  默认: (60000 - 40000) ÷ 2 = 10000

端口范围选择建议

场景建议端口范围理由
开发/测试(≤10 沙箱)40000-40100100 个端口足够,防火墙规则更安全
小规模(≤50 沙箱)40000-40200200 个端口充裕
中规模(≤500 沙箱)40000-410001000 个端口
大规模(≤5000 沙箱)40000-50000保留范围
默认(不限制)保持默认 40000-60000最大灵活性

防火墙配置

根据你启用的端口范围配置防火墙规则:

# 如果设置 port_range_min=40000, port_range_max=40100
firewall-cmd --permanent --add-port=8080/tcp          # Server API
firewall-cmd --permanent --add-port=40000-40100/tcp   # 沙箱端口
firewall-cmd --reload
# 如果保持默认 40000-60000
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --permanent --add-port=40000-60000/tcp
firewall-cmd --reload

端口分配失败处理

当 allocate_host_port 在指定范围内连续 50 次随机尝试都找不到空闲端口时:

Error: Failed to allocate host ports for sandbox container.

常见原因:

  1. 端口范围太小:同时运行的沙箱数 × 2 > 端口范围大小
    • 解决:扩大 port_range_max - port_range_min
  2. 宿主机其他进程占用了范围内端口:如 Redis、MySQL 等
    • 解决:缩小范围避开已知端口,或 netstat -tln 排查

SDK 通信链路对比

Host 模式

SDK → Server API (8080):
  POST /v1/sandboxes             创建沙箱
  GET  /sandboxes/{id}/endpoints/44772  获取 endpoint

Server 返回: host:40001/proxy/44772
  ← 注意:40001 是动态分配的,每个沙箱不同

SDK → 沙箱 execd (直连):
  http://host:40001/proxy/44772/files/read/...  读取文件
  http://host:40001/proxy/44772/commands/run...  执行命令
  http://host:40001/proxy/44772/health           健康检查

Bridge 模式

SDK → Server API (8080):
  POST /v1/sandboxes             创建沙箱
  GET  /sandboxes/{id}/endpoints/44772  获取 endpoint

Server 返回: host:40001/proxy/44772
  ← 40001 是 Docker 端口映射的宿主机端口

SDK → 宿主机 :40001:
  http://host:40001/proxy/44772/files/read/...

Docker iptables NAT:
  宿主机:40001 → 容器:44772 → execd

Server Proxy 模式

SDK 无法直连宿主机端口时,可启用 use_server_proxy=True:

config = ConnectionConfig(
    domain="server-host:8080",
    use_server_proxy=True,
)
sandbox = await Sandbox.create("python:3.11", connection_config=config)
SDK → Server (8080) → 沙箱 execd

Server 内部:
  /sandboxes/{sandbox_id}/proxy/{port}/*
    → get_endpoint(sandbox_id, port, resolve_internal=True)
    → 127.0.0.1:{allocated_port}/proxy/{port}
    → 转发请求到沙箱 execd

模式选择决策树

需要 egress 网络策略 (networkPolicy)?
├─ 是 → bridge 模式(必须)
├─ 否 → 需要最大网络性能?
│       ├─ 是 → host 模式
│       ├─ 否 → Server 在 Docker 容器内?
│       │       ├─ 是 → bridge 模式 + host_ip
│       │       └─ 否 → 两者均可,建议 bridge 模式

             ┌─────────────────────────────────────────┐
             │         推荐:生产环境用 bridge 模式       │
             │                                         │
             │  bridge 提供网络隔离,防止沙箱互相影响     │
             │  host 模式仅在对延迟极端敏感时使用         │
             └─────────────────────────────────────────┘

完整配置示例

Bridge 模式完整配置

[server]
host = "0.0.0.0"
port = 8080
api_key = "your-secret-key"

[log]
level = "INFO"

[runtime]
type = "docker"
execd_image = "opensandbox/execd:v1.0.19"

[docker]
network_mode = "bridge"
port_range_min = 40000
port_range_max = 50000
# 如果 server 在容器内,取消注释:
# host_ip = "host.docker.internal"
# 安全设置
drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"]
no_new_privileges = true
pids_limit = 4096

[egress]
image = "opensandbox/egress:v1.1.2"
mode = "dns"

[storage]
allowed_host_paths = ["/data/opensandbox"]

[store]
type = "sqlite"
path = "~/.opensandbox/opensandbox.db"

Host 模式完整配置

[server]
host = "0.0.0.0"
port = 8080
api_key = "your-secret-key"

[log]
level = "INFO"

[runtime]
type = "docker"
execd_image = "opensandbox/execd:v1.0.19"

[docker]
network_mode = "host"
port_range_min = 40000
port_range_max = 50000
# host 模式不需要 host_ip
drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"]
no_new_privileges = true
pids_limit = 4096

# 注意:host 模式不支持 egress sidecar
# [egress] 段可以省略

[storage]
allowed_host_paths = ["/data/opensandbox"]

[store]
type = "sqlite"
path = "~/.opensandbox/opensandbox.db"

附录:端口 FAQ

Q: Host 模式下 execd 如何知道绑定哪个端口?

通过环境变量 OPENSANDBOX_EXECD_PORT。Server 在创建容器时注入环境变量,execd 的 parser.go 读取该变量:

const serverPortEnv = "OPENSANDBOX_EXECD_PORT"

if portFromEnv := os.Getenv(serverPortEnv); portFromEnv != "" {
    port, err := strconv.Atoi(portFromEnv)
    ServerPort = port  // ← execd 绑定到这个端口
}

优先级:环境变量 > CLI --port > 默认 44772

Q: 端口用完会怎样?

allocate_host_port() 连续 50 次随机尝试都失败时,创建沙箱请求返回 500 错误。建议保持端口范围大于 预期最大沙箱数 × 2。

Q: 宿主机重启后端口会变化吗?

会。宿主机重启后所有沙箱消失,重新创建时端口会重新分配。没有持久性假设。

Q: Server 需要对外开放所有端口吗?

不。SDK 需要访问的是 execd 的端口,即 port_range_min ~ port_range_max 范围内的端口。Server API 端口(默认 8080)也需要开放。其他端口不需要。

Q: 可以用 use_server_proxy=True 避免开放端口范围吗?

可以。如果 SDK 无法直接访问宿主机端口范围(如跨网络、跨云),设置 use_server_proxy=True,SDK 所有请求都经过 Server 的 8080 端口代理转发,不再需要直接访问端口范围。

config = ConnectionConfig(use_server_proxy=True)

代价:多一层 HTTP 转发,延迟略增,Server 负载增加。

Q: 多大端口范围会被视为安全风险?

40000-60000 有 20000 个端口,确实较大。建议根据实际并发数缩小范围:

最大并发沙箱建议范围端口数
1040000-4006060
5040000-40100100
10040000-40200200

或者启用 use_server_proxy=True 完全不开放端口范围。

NOTE: docker 服务在 host 模式下 不支持多服务并发调用,详见:
https://blog.csdn.net/lichenyang810123/article/details/162733906?spm=1001.2014.3001.5501

更多推荐