Skip to content

沙箱间互访 ​

一个沙箱访问另一个沙箱中的服务有两条数据面路径:CubeProxy Host 路由 (与公网 URL 相同的入口)和母机端口映射(CubeVS eBPF DNAT)。 方式一通过 HTTP Host 头部指定访问的沙箱 id 和 port (<port>-<sandboxID>.cube.app),由 CubeProxy 做路由转发代理到目标沙箱; 方式二则是直接访问目标沙箱的 nodeIP 和映射的 port,即 节点IP:hostPort。

方式一数据流程(CubeProxy 与沙箱不同机):

text
  源沙箱 A                CubeProxy 节点               目的节点
 (源节点)               CubeProxy :80                主网卡 ingress
     │                        │                           │
     │  ① Host: 8080-B.cube.app                           │
     │    (--resolve 钉到 CubeProxy 节点 IP)             │
     │───────────────────────►│                           │
     │                        │  ② 访问 hostIP:hostPort    │
     │                        │──────────────────────────►│ from_world DNAT
     │                        │                           │ 至 sandboxIP:8080
     │                        │                           │──► 目的沙箱 B :8080

方式二数据流程 · 同节点(源节点 = 目的节点):

text
  源沙箱 A            from_cube                  主网卡 ingress
     │                   │                           │
     │ ① nodeIP:hostPort │                           │
     │──────────────────►│ ② SNAT 后软件环回          │
     │                   │    bpf_redirect           │
     │                   │    (BPF_F_INGRESS)        │
     │                   │──────────────────────────►│ from_world DNAT
     │                   │                           │ 至 sandboxIP:8080
     │                   │                           │──► 目的沙箱 B :8080

  全程在本节点内完成,不依赖网卡/交换机 hairpin。

方式二数据流程 · 跨节点:

text
  源沙箱 A            from_cube                    目的节点
 (源节点)           (源节点)                   主网卡 ingress
     │                   │                           │
     │ ① nodeIP:hostPort │                           │
     │──────────────────►│ ② SNAT,出物理网卡         │
     │                   │═══════════════════════════►│ from_world DNAT
     │                   │      (物理链路)          │ 至 sandboxIP:8080
     │                   │                           │──► 目的沙箱 B :8080

完整可运行的示例脚本见 examples/code-sandbox-quickstart 下的 sandbox-curl-sandbox.py(方式一)与 sandbox-port-mapping.py(方式二)。

如何选择 ​

维度方式一:CubeProxy Host 路由方式二:母机端口映射
协议仅 HTTP / HTTPS / gRPC(L7,按 Host 头路由)任意 TCP 协议(L4 DNAT)
数据面路径经 CubeProxy 转发,多一跳代理eBPF DNAT 直连沙箱,无代理开销
入口鉴权支持 allow_public_traffic=False 的 per-sandbox token不支持 token,由网络策略与 expose-port 控制暴露面
服务端口的来源URL 的 <port> 前缀指定 B 的容器端口info API 查询 B 的容器端口对应的 hostIP:hostPort
端点稳定性域名格式固定,暂停/恢复后不变hostIP:hostPort 可能在暂停/恢复后改变(见下文)
沙箱内 DNS不需要(--resolve 钉到 proxy IP)不需要(直接用节点 IP)

典型场景:

  • 方式一:沙箱内 Agent 调用另一个沙箱托管的 HTTP API / gRPC 服务; 需要复用 CubeProxy 的 token 鉴权;希望端点 URL 稳定、免去查询 hostPort。
  • 方式二:沙箱间跑任意 TCP 协议(数据库、消息队列、自定义二进制 协议);对时延敏感、想绕过代理;端口可在模板中预先声明。

模板构建:声明服务端口 ​

两种方式都以模板 --expose-port 声明的端口为服务端口契约 —— 调用方 只需知道 B 的容器端口,无需感知是否与 B 同节点。在标准模板基础上 追加服务端口即可(以 8080 为例;镜像仓库地址见 快速入门,中国大陆环境用 cube-sandbox-cn 仓库):

bash
# 49999/49983 是 sandbox-code 镜像自身的 code interpreter / envd 端口;
# 把 8080 换成你的服务端口。
cubemastercli tpl create-from-image \
  --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \
  --writable-layer-size 1G \
  --expose-port 49999 \
  --expose-port 49983 \
  --expose-port 8080 \
  --probe 49999

说明:

  • --expose-port 可重复多次;只有声明过的端口才会分配 hostPort 映射 (方式二与方式一的跨节点路径都依赖它)。
  • --probe 是模板就绪探针端口,sandbox-code 用 49999。
  • 命令会打印 template_id,并等待构建完成(READY);也可用 cubemastercli tpl list 查看状态。
  • 已有模板追加端口只能重新构建 —— expose-port 是模板属性,运行时不可改。

创建沙箱时使用新模板 ID 即可,端口映射由 Cubelet 在沙箱启动时自动完成, 无需在创建请求里额外传参。

方式一:CubeProxy Host 路由 ​

CubeProxy 按 <port>-<sandboxID>.<domain> 形式的 Host 头路由到对应沙箱。 沙箱 A 内用 curl --resolve 把域名钉到 CubeProxy 节点 IP,无需 DNS:

bash
curl -sS --resolve 8080-<sandboxID-B>.cube.app:80:<proxy节点IP> \
     http://8080-<sandboxID-B>.cube.app/

B 用按上文声明了服务端口的模板创建。调用方在 Host 头里指定 B 的容器 端口:同节点时 CubeProxy 直接拨 sandboxIP:容器端口;跨节点时经该端口 对应的 hostIP:hostPort 映射进入 B(未声明的端口跨节点返回 404)。

实现细节:同节点时 CubeProxy 的同机直连不检查端口映射,未声明的端口 也能访问——但这是宽松行为,A、B 一旦被调度到不同节点就会 404, 不应依赖。

特点:

  • 与公网 URL 走同一入口,鉴权语义一致:B 以 network={"allow_public_traffic": False} 创建时,请求必须携带 e2b-traffic-access-token 头(见限制公开访问)。
  • 依赖 CubeProxy 可用。

限制:

  • 仅支持 L7 协议:HTTP / HTTPS / gRPC(gRPC 基于 HTTP/2,可经 Host 路由)。任意 TCP/UDP 服务(如数据库、Redis、自定义协议)无法用此方式, 请改用方式二。
  • 服务端口需经模板 --expose-port 声明(见上文"模板构建:声明服务端口")。

方式二:母机端口映射(hostIP:hostPort) ​

模板以 --expose-port 声明端口后,Cubelet 在创建沙箱时向 CubeVS 下发 端口映射:发往 <节点IP>:<hostPort> 的 TCP 流量由主网卡 ingress 上的 from_world eBPF 程序直接 DNAT 进沙箱,不经过 CubeProxy。

前提 ​

  • cubemaster 配置 cubelet_conf.enable_exposed_port: true(默认开启);
  • 模板已按上文"模板构建:声明服务端口"声明了服务端口。

查询分配的 hostPort ​

hostPort 按沙箱动态分配,用 SDK 查询(推荐):

python
endpoint = sandbox_b.get_exposed_port_endpoint(8080)
# "9.135.152.214:20006"——恒为 hostIP:hostPort 形式

该端点适用于跨节点和沙箱内调用方:同节点沙箱流量由 CubeVS hairpin 回环 到主网卡 ingress 完成投递。如果调用进程运行在目标节点本机,请改用 CubeProxy Host 路由——host-local 流量不经过 from_world/hairpin DNAT 路径。 Go / Node SDK 同样有 GetExposedPortEndpoint / getExposedPortEndpoint。

节点版本要求:同节点沙箱互访依赖含 hairpin 的 CubeVS(与本 API 同版本 发布)。旧节点没有同节点访问能力(既无 hostPort 也无 tap 直连),升级节点 即可获得,调用方无需改代码。

HTTP 兜底(CubeAPI 转发到 CubeMaster):

bash
curl -s 'http://<cubeapi>:3000/sandboxes/<B>/exposed-ports/8080'
# → {"sandboxID":"<B>","containerPort":8080,"mode":"host_port",
#    "endpoint":"9.135.152.214:20006","hostIP":"9.135.152.214","hostPort":20006}

沙箱 A 访问 ​

bash
curl -sS http://<节点IP>:<hostPort>/

A、B 同节点时,沙箱 A 的报文在 from_cube 中先做 SNAT,再由软件环回进 节点网卡 ingress 触发 DNAT —— 不依赖网卡/交换机 hairpin;跨节点(A、B 在 不同节点)流量经物理链路到达同一 ingress 路径。

特点:

  • 数据面直连,无代理开销;支持任意 TCP 协议(HTTP、数据库、消息 队列、自定义二进制协议均可)。
  • 不经过 CubeProxy,因此不适用 allow_public_traffic token 鉴权, 暴露面由网络策略与模板 expose-port 共同控制。

限制:

  • 仅 TCP,不支持 UDP。
  • 服务端口必须是模板 --expose-port 声明过的端口。
  • 暂停/恢复后端点可能变化:hostPort 在沙箱创建(含 resume 时的内部 重建)时动态分配,沙箱 pause → resume 后 hostIP:hostPort 可能不同 (跨节点调度时 hostIP 也会变)。调用方必须在 B 恢复后重新查询 info 接口获取新端点,不要缓存长期使用。

补充:控制面方式(SDK connect) ​

如果需求是"在 A 里操作 B"(执行命令、读写文件)而非访问 B 的服务端口, 可以在 A 内安装 cubesandbox SDK 并 Sandbox.connect(B的ID):

python
from cubesandbox import Sandbox
sandbox_b = Sandbox.connect("<sandboxID-B>")
sandbox_b.commands.run("echo hello")

A 内需设置 CUBE_API_URL(指向 CubeAPI 节点可达地址,注意 .env 里的 127.0.0.1 在沙箱内指沙箱自身)与 CUBE_PROXY_NODE_IP。 完整示例见 sandbox-in-sandbox.py。

测试验证 ​

两种方式都有可运行的端到端示例脚本(位于 examples/code-sandbox-quickstart), 脚本自动完成:创建沙箱 A/B → B 内起 HTTP 服务 → A 内 curl 访问 → 校验响应:

  • sandbox-curl-sandbox.py —— 方式一(服务端口固定 8080);
  • sandbox-port-mapping.py —— 方式二(端口可用 CUBE_SERVICE_PORT 覆盖,默认 8080)。

两者都应使用按上文"模板构建"声明了对应端口的模板(CUBE_TEMPLATE_ID)。

环境准备、运行命令与预期输出见该目录的 README。若 A 内 curl 返回 ECONNREFUSED(退出码 2),sandbox-port-mapping.py 会打印排查清单 (端口未 expose、enable_exposed_port 未开、出口策略拦截)。

通用注意点 ​

  • 出口策略:A 若配置了 allow_out / deny_out(见 网络策略),白名单需覆盖 CubeAPI / CubeProxy / 宿主节点 IP。
  • API 地址:沙箱内访问控制面时,loopback 地址(127.0.0.1)指向 沙箱自身,必须替换为节点可达地址。

延伸阅读 ​