CubeSandbox gRPC 接入实战:通过 CubeProxy 明文 gRPC 端口访问沙箱内服务
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
导读
本文基于 CubeSandbox 仓库中examples/grpc-ingress/示例,讲解如何让原生 gRPC 客户端(如grpcio、grpc-go)绕过泛域名 DNS 与 TLS 终止,直接通过 CubeProxy 的明文 gRPC 接入端口9090访问沙箱内部服务(默认 envd 端口49983)。读完本文你将掌握:SDK 内置 API 与原生 gRPC 客户端两条接入路径的适用边界、完整的示例代码与配置方法、以及限制公网访问场景下如何携带流量访问令牌(traffic token)完成鉴权。
为什么需要独立的 gRPC 接入端口
CubeSandbox Python SDK 的commands、files等 API 走的是 CubeProxy HTTP/HTTPS(80/443,或CUBE_PROXY_NODE_IP+CUBE_PROXY_PORT_HTTP配合Host头)上的HTTP/Connect流量。这些 API不需要改 SDK,属于开箱即用的路径。
当你持有的是原生 gRPC 客户端(grpcio、grpc-go等),并且存在以下两个约束之一时,就需要切换到9090端口:
- 无法使用泛域名
*.cube.app(例如在私有网络、受限 DNS 环境下); - 无法在 CubeProxy 上终止 TLS。
安全提示:
9090端口承载的是明文gRPC(示例使用grpc.insecure_channel)。不要直接暴露到不可信网络,除非上游另有 TLS 终结(如负载均衡)。需要 TLS 时,优先走 CubeProxy 的 HTTP/HTTPS(Connect 风格流量)。
这一设计在 CubeProxy 的 nginx.conf 中有清晰的实现证据:9090是一个独立的server块,声明listen 9090 http2 reuseport;,并通过set $cube_ingress_protocol "grpc";标记自己为明文 gRPC 接入端口。
接入流程全景
整个调用链路可以概括为下图:
grpc_plaintext.py │ ├─ Sandbox.create() ──► CubeAPI (控制面) │ └─ grpc.insecure_channel(proxy:9090, authority=<port>-<sandbox_id>) │ ▼ CubeProxy :9090 │ ▼ 沙箱内 envd (:49983)关键点在于:客户端拨号目标始终是CubeProxy IP + 9090,而具体的沙箱实例与端口信息编码在 HTTP/2 的:authority伪头(gRPC 客户端通过grpc.default_authority选项设置)中。CubeProxy 根据:authority解析出container_port与sandbox_id,再路由到对应沙箱后端的真实 IP 与端口。示例中:authority的格式固定为<port>-<sandbox_id>,例如49983-c0ffee123。
在 nginx.conf 的location /中可以看到实际转发动作:rewrite_by_lua_file lua/rewrite_phase.lua负责解析:authority与鉴权,随后grpc_pass grpc://grpc_backend;把请求代理到沙箱后端。
前置条件
运行本示例需要满足:
- 已部署 Cube Sandbox,且 CubeProxy 已开启 gRPC 接入(对应 Dockerfile 中的
EXPOSE 8080 8081 9090); - Python 3.8+;
- 模板暴露 envd 端口
49983(标准 Cube 模板即可)。
安装依赖并运行:
pip install -r requirements.txt cp .env.example .env # 编辑 .env python grpc_plaintext.py依赖清单见 requirements.txt,包含grpcio>=1.60、python-dotenv与cubesandbox。env_utils.py会以“就近优先”的方式加载.env(优先加载脚本所在目录的.env,其次当前工作目录),且不会覆盖系统已存在的环境变量。
环境变量一览
| 变量 | 必填 | 默认 | 说明 |
|---|---|---|---|
CUBE_API_URL | 是 | — | Cube API 地址 |
CUBE_TEMPLATE_ID | 是 | — | 沙箱模板 ID |
CUBE_PROXY_NODE_IP | 是 | — | CubeProxy IP(无需 DNS) |
CUBE_PROXY_GRPC_PORT | 否 | 9090 | 明文 gRPC 监听端口 |
ENVD_PORT | 否 | 49983 | :authority中的沙箱服务端口 |
其中CUBE_PROXY_NODE_IP与CUBE_TEMPLATE_ID在 grpc_plaintext.py 中做了必填校验,缺失时脚本会直接退出并给出提示。GRPC_PORT与ENVD_PORT分别通过os.environ.get(..., "9090")与os.environ.get(..., "49983")提供默认值,允许按需覆盖。
示例代码逐段解析
核心逻辑位于 grpc_plaintext.py:
- 创建沙箱:
with Sandbox.create(template=TEMPLATE_ID, timeout=300) as sandbox:以模板 ID 创建沙箱,退出with块时自动销毁。 - 构造
:authority:authority = f"{ENVD_PORT}-{sandbox.sandbox_id}",将沙箱服务端口与沙箱 ID 拼接。 - 建立明文通道:
channel = grpc.insecure_channel(target, options=(("grpc.default_authority", authority),)),其中target = f"{PROXY_IP}:{GRPC_PORT}"。 - 验证连通性:通过
grpc.channel_ready_future(channel).result(timeout=15)等待通道就绪。若超时,会提示检查 CubeProxy 是否在:9090上监听 gRPC 接入。
在真实业务中,将channel_ready_future替换为实际的 RPC 调用即可,例如stub.SomeMethod(request, metadata=metadata)。
限制公网访问的沙箱:携带流量令牌
若创建沙箱时设置network={"allow_public_traffic": False},所有入站请求(包括 gRPC 流量)都必须携带令牌,否则会被拒绝。每次 RPC 需在 gRPC metadata 中携带 token:
metadata = (("cube-traffic-access-token", sandbox.traffic_access_token),) # stub.SomeMethod(request, metadata=metadata)该机制在 docs/zh/guide/restrict-public-access.md 中有完整说明,核心事实包括:
- 默认(不传
network或allow_public_traffic=True)时不签发 token,sandbox.traffic_access_token为None,存量调用方无需改动即可继续工作; - 锁定访问时返回每沙箱独立的不透明 token,拒绝未携带正确 token 的请求(HTTP 403);
- CubeProxy 的所有入站监听(HTTP、HTTPS 以及默认明文 gRPC 端口
9090)执行相同校验; - gRPC 客户端应通过 metadata 发送
e2b-traffic-access-token或cube-traffic-access-token,二者接受相同形式的不透明 token,按 HTTP 规则不区分大小写,两个都存在时以e2b-为准; - Token 仅在下发一次、无 rotation API、暂停/恢复不会重发、销毁沙箱时自动清除。
从源码看,allow_public_traffic控制的是入站访问权限,与出站方向的网络策略(allow_out/deny_out/allow_internet_access)以及安全代理(出站七层规则)正交,可独立或叠加使用。典型的“私有 Agent”部署会三者叠加:网络策略限制出站、安全代理在出站端注入 API 凭证、本机制要求所有入站请求都携带凭证。
错误处理:HTTP 错误如何映射为 gRPC 状态码
原生 gRPC 客户端期望的是 gRPC status 语义,而非 HTTP 4xx/5xx。CubeProxy 在数据面做了两层处理(见 lua/utils.lua 与 nginx.conf):
- HTTP 状态 → gRPC 状态码映射:
400 → 3 (INVALID_ARGUMENT)、403 → 7 (PERMISSION_DENIED)、404 → 5 (NOT_FOUND)、410 → 9 (FAILED_PRECONDITION)、503 → 14 (UNAVAILABLE),未命中映射时回退到2 (UNKNOWN); - 统一终止路径:gRPC 接入路径返回
HTTP 200 + Content-Type: application/grpc + grpc-status / grpc-message 头(无响应体时以 trailers 形式呈现),符合 gRPC over HTTP/2 的 framing 规范——这也是为什么数据面拒绝时不能直接ngx.sayJSON 或裸ngx.exit,否则原生客户端会因缺少 trailers 而报错; - 上游故障映射:
error_page 502 503 504 = @grpc_upstream_error;把 upstream 的 HTTP 失败转成grpc-status 14 (UNAVAILABLE),让原生客户端看到统一的 UNAVAILABLE 而非裸 502/503。
同时,utils.lua中的is_grpc_request()只依据$cube_ingress_protocol == "grpc"判断(该变量仅在:9090server 块中置位),而不依据 Content-Type判断,避免向:80/:443上误发application/grpc的请求走错错误路径。这为排查“客户端连上了但报错格式不对”一类问题提供了线索:请确认请求确实落在9090端口。
创建带 envd 端口的模板
标准 Cube 模板已暴露 envd 端口49983;如需从自定义镜像创建模板,可使用cubemastercli:
cubemastercli tpl create-from-image \ --image cubesandbox-base:latest \ --expose-port 49983 \ --probe 49983 \ --probe-path /health其中--expose-port 49983声明沙箱内暴露的服务端口,--probe与--probe-path配置健康检查探针,使沙箱在服务就绪后才对外可用。
常见问题速查
| 现象 | 排查方向 |
|---|---|
| 通道 15 秒超时 | 确认CUBE_PROXY_NODE_IP从当前主机可达;确认 CubeProxy 的9090端口已启用(参考 Dockerfile 的 EXPOSE 与 nginx.conf 的 listen) |
| 沙箱未开放公网却未带 token | 每次 RPC 的 metadata 必须包含cube-traffic-access-token(或e2b-traffic-access-token),值为sandbox.traffic_access_token |
| 客户端报错而非标准 gRPC 状态 | 确认流量落在9090明文 gRPC 入口(:80/:443的 HTTP 错误路径不返回 gRPC trailers);检查grpc.default_authority是否为<ENVD_PORT>-<sandbox_id>格式 |
| 需要 TLS | 明文9090不提供 TLS;在上游(如负载均衡)终结 TLS,或改用 CubeProxy HTTP/HTTPS 的 Connect 风格流量 |
小结
examples/grpc-ingress/展示了 CubeSandbox 在 SDK 内置 HTTP/Connect 路径之外提供的原生 gRPC 接入能力:通过:9090明文入口 +:authority编码路由,任何遵循 gRPC over HTTP/2 的客户端都能直达沙箱内服务;配合allow_public_traffic=False的令牌机制与完善的 gRPC 错误映射,可以安全地把它接入私有网络或企业内网场景。相关参考:示例目录、限制公网访问、CubeProxy 配置、gRPC 错误映射实现。
【免费下载链接】CubeSandboxInstant, Concurrent, Secure & Lightweight Sandbox for AI Agents.项目地址: https://gitcode.com/GitHub_Trending/cu/CubeSandbox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考