网络请求是如何进入沙箱的:CubeProxy、端口映射与 CubeVS 协作
很多人第一次接触 CubeSandbox 时,会把“访问沙箱”理解成一句简单的话:
- SDK 请求某个域名
- 代理转发一下
- 沙箱收到请求
源码级看,这条路径其实穿过了 3 层不同职责的组件:
- CubeProxy:负责识别请求属于哪个
sandboxID、哪个服务端口 - 控制面路由元数据:负责告诉系统这个
sandboxID当前落在哪台节点、有哪些端口映射 - CubeVS / 节点网络层:负责把落到宿主端口的流量 DNAT 回目标沙箱 TAP 设备
这篇文章重点解释:
一个来自外部客户端的 HTTP / WebSocket / SDK 请求,是怎么一步步进入目标沙箱的?
1. 先看结论:入站路径不是单一代理,而是“代理 + 元数据 + 内核转发”
可以先记住这个简化版结论:
CubeProxy 负责“认路”,CubeMaster 维护的路由元数据负责“知道路在哪”,CubeVS 负责在节点内核里把流量真正送进对应沙箱”。
如果只看 CubeProxy,你会误以为它像普通 Nginx upstream 转发。
如果只看 CubeVS,你会误以为访问沙箱只是一条端口映射规则。
实际上,这条链路是分层协作的:
- L7 入口层:CubeProxy 解析 Host 或路径
- 控制面状态层:根据
sandboxID找节点与端口映射 - L3/L4 节点网络层:在宿主机网卡处把包导回对应沙箱 TAP
2. 第一站:客户端看到的并不是节点地址,而是沙箱域名
在 SDK 与文档里,对外暴露的地址格式是:
<port>-<sandboxID>.<domain>例如:
49999-sb-123.cube.app
49983-sb-123.cube.app
9000-sb-123.cube.app这里的三个部分分别承载不同含义:
port:要访问的沙箱服务端口sandboxID:目标沙箱身份domain:指向 CubeProxy 的统一域名后缀
这意味着客户端并不需要知道:
- 该沙箱在哪台节点
- 节点上为这个端口分配了哪个宿主端口
- 节点内部 TAP 设备叫什么
这些都被藏在系统内部了。
3. 第二站:CubeProxy 先做“请求归属判断”
根据现有文档与 SDK 约定,CubeProxy 支持两种入口形式:
3.1 Host 模式
最常见的是:
49999-<sandboxID>.<domain>CubeProxy 会从 Host 中解析出:
- 目标服务端口:如
49999 - 目标沙箱:如
sb-123
这也是 SDK 默认采用的方式。
3.2 Path 模式
在 https-and-domain 指南里还能看到路径模式:
/sandbox/<sandbox-id>/<container-port>/...它更适合:
- 内网演示
- 不方便配泛域名 DNS 的场景
- 临时分享
这两种模式虽然入口不同,但最终都要归一到同一件事:
先找到
sandboxID与目标容器端口,再继续向下游转发。
4. 第三站:为什么 CubeProxy 不能单独完成转发
很多反向代理系统只要有 upstream 列表就能转发,但 CubeSandbox 不行。
原因是沙箱是动态创建和销毁的,且每个沙箱的宿主端口也可能不同。
所以 CubeProxy 并不能把配置静态写死成:
sandbox A -> 10.0.0.2:12345sandbox B -> 10.0.0.3:23456
而必须依赖实时路由元数据。
这就是 CubeMaster/pkg/service/sandbox/sandbox_run.go 里 setProxyToRedis() 的意义所在。
创建成功后,Master 会把这些信息登记成 SandboxProxyMap:
HostIPSandboxIDSandboxIPCreatedAtContainerToHostPorts
因此,CubeProxy 本质上依赖的是:
sandboxID -> hostIPcontainerPort -> hostPort
这样的动态映射,而不是静态 upstream。
5. 第四站:控制面维护的是“逻辑路由表”
从架构角度看,这张逻辑路由表回答两个问题:
- 这个沙箱当前在哪台节点上?
- 这个沙箱暴露的某个容器端口,在那台节点上对应哪个宿主端口?
也就是说,外部访问路径其实会经历一次逻辑翻译:
49999-sb-123.cube.app
=> sandboxID = sb-123, container port = 49999
=> hostIP = 10.0.0.8, hostPort = 21437到这里为止,系统才刚刚知道“该把请求送到哪台节点的哪个宿主端口”。
注意,这仍然只是节点入口,还没有真正进入沙箱。
6. 第五站:节点内部真正入沙箱,靠的是 CubeVS 的端口映射
到了节点侧,流量并不是直接进某个用户态代理进程,而是还要经过 CubeVS 的数据面机制。
在 CubeVS 的设计里,入站流量关键依赖两张 BPF map:
remote_port_mappinglocal_port_mapping
其中与外部访问最相关的是:
remote_port_mapping:把宿主端口映射到(TAP ifindex, sandbox listen port)
在 nodenic.bpf.c 的 from_world 路径上,如果一个包不是某个已有 NAT 会话的回包,它就会尝试按目标宿主端口查这张表。
查到后,内核会做两件事:
- 把目标从宿主端口 DNAT 成沙箱实际监听端口
- 把包重定向到对应 TAP 设备
因此,真正“进入沙箱”的最后一步,不是 CubeProxy 本身,而是:
节点内核中的 CubeVS 端口映射把入站包送进对应沙箱 TAP。
7. 这条路径里,L7 转发与 L3/L4 转发是如何分工的
这里最容易混淆的点是:CubeProxy 和 CubeVS 看起来都在做“转发”。
但两者分工不同:
7.1 CubeProxy 负责 L7 路由语义
它理解的是:
- Host 头
- URL 路径
- WebSocket 升级
- Cookie / Location 改写
- HTTPS / 证书 / 域名
所以它更像“访问入口层”。
7.2 CubeVS 负责节点内核的数据面落地
它理解的是:
- 宿主端口
- TAP ifindex
- 沙箱监听端口
- NAT / DNAT / redirect
- 会话状态
所以它更像“节点内部数据面层”。
这两层叠起来,才能让外部看到一个稳定的 sandbox domain,而内部仍保持高性能、低耦合的内核级转发。
8. 一次典型 HTTP 请求的完整落点过程
以下面这个请求为例:
GET https://49999-sb-123.cube.app/health它在系统里的路径可以拆成 8 步:
- 客户端把请求发给
*.cube.app,DNS 解析到 CubeProxy 所在机器。 - CubeProxy 读取 Host:
49999-sb-123.cube.app。 - CubeProxy 提取出
sandboxID=sb-123与目标容器端口49999。 - CubeProxy 查询控制面维护的路由元数据,得到:
- 节点 IP,例如
10.0.0.8 - 宿主端口,例如
21437
- 节点 IP,例如
- CubeProxy 把请求发往该节点入口。
- 节点网卡收到目标为
21437的流量。 - CubeVS 的
from_world程序查remote_port_mapping[21437],获得目标沙箱 TAP 与容器监听端口49999。 - 包被 DNAT 并重定向到目标沙箱 TAP,最终由沙箱内
49999服务处理。
如果是 WebSocket,CubeProxy 负责保留 upgrade 语义;CubeVS 仍然只负责把对应 TCP 流送到正确沙箱。
9. 为什么这套设计适合 Agent / Sandbox 场景
相对于“每个沙箱独占一个公网 IP”或“每个实例挂一个独立 sidecar 网关”的方案,这套设计更适合大规模沙箱系统,原因有三个:
9.1 外部访问地址稳定且统一
客户端只需要记住:
- 一个统一域名后缀
- 一个固定格式的 Host 规则
不需要感知节点漂移与端口重分配。
9.2 节点内部转发成本更低
CubeVS 使用 eBPF 与 TAP 映射直接在内核完成 DNAT / redirect,避免了传统 bridge / iptables 在大规模多租户下的规则膨胀。
9.3 控制面与数据面职责清晰
- 控制面负责“谁在哪、谁暴露了什么端口”
- 数据面负责“如何把包送进去”
这使得调度、创建、销毁和网络转发可以独立演进。
10. 最常见的三个误解
10.1 误解一:CubeProxy 直接知道所有上游地址
不准确。
CubeProxy 的转发建立在控制面维护的动态路由元数据之上,而不是写死的 upstream 列表。
10.2 误解二:进入节点就等于进入沙箱
也不对。
进入节点之后,还要通过 remote_port_mapping 被导入对应 TAP,才能真正进入目标沙箱。
10.3 误解三:CubeVS 只负责出网
不对。
CubeVS 不只处理出站 SNAT,也负责:
- 入站会话回包 reverse NAT
- 端口映射入站流量
- 节点内部到沙箱的最终转发
11. 用一句话概括这条入站路径
如果只记一句话,可以记这个:
外部请求先由 CubeProxy 解析为
sandboxID + containerPort,再通过控制面路由元数据定位到具体节点和宿主端口,最后由 CubeVS 在节点内核里根据remote_port_mapping将流量 DNAT 并重定向进目标沙箱 TAP。
这就是“请求如何进入沙箱”的完整答案。