沙箱间互访
一个沙箱访问另一个沙箱中的服务有两条数据面路径:CubeProxy Host 路由 (与公网 URL 相同的入口)和母机端口映射(CubeVS eBPF DNAT)。 方式一通过 HTTP Host 头部指定访问的沙箱 id 和 port (<port>-<sandboxID>.cube.app),由 CubeProxy 做路由转发代理到目标沙箱; 方式二则是直接访问目标沙箱的 nodeIP 和映射的 port,即 节点IP:hostPort。
方式一数据流程(CubeProxy 与沙箱不同机):
源沙箱 A CubeProxy 节点 目的节点
(源节点) CubeProxy :80 主网卡 ingress
│ │ │
│ ① Host: 8080-B.cube.app │
│ (--resolve 钉到 CubeProxy 节点 IP) │
│───────────────────────►│ │
│ │ ② 访问 hostIP:hostPort │
│ │──────────────────────────►│ from_world DNAT
│ │ │ 至 sandboxIP:8080
│ │ │──► 目的沙箱 B :8080方式二数据流程 · 同节点(源节点 = 目的节点):
源沙箱 A from_cube 主网卡 ingress
│ │ │
│ ① nodeIP:hostPort │ │
│──────────────────►│ ② SNAT 后软件环回 │
│ │ bpf_redirect │
│ │ (BPF_F_INGRESS) │
│ │──────────────────────────►│ from_world DNAT
│ │ │ 至 sandboxIP:8080
│ │ │──► 目的沙箱 B :8080
全程在本节点内完成,不依赖网卡/交换机 hairpin。方式二数据流程 · 跨节点:
源沙箱 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 仓库):
# 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:
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 查询(推荐):
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):
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 访问
curl -sS http://<节点IP>:<hostPort>/A、B 同节点时,沙箱 A 的报文在 from_cube 中先做 SNAT,再由软件环回进 节点网卡 ingress 触发 DNAT —— 不依赖网卡/交换机 hairpin;跨节点(A、B 在 不同节点)流量经物理链路到达同一 ingress 路径。
特点:
- 数据面直连,无代理开销;支持任意 TCP 协议(HTTP、数据库、消息 队列、自定义二进制协议均可)。
- 不经过 CubeProxy,因此不适用
allow_public_traffictoken 鉴权, 暴露面由网络策略与模板 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):
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)指向 沙箱自身,必须替换为节点可达地址。