Skip to content

网络请求是如何进入沙箱的:CubeProxy、端口映射与 CubeVS 协作

很多人第一次接触 CubeSandbox 时,会把“访问沙箱”理解成一句简单的话:

  • SDK 请求某个域名
  • 代理转发一下
  • 沙箱收到请求

源码级看,这条路径其实穿过了 3 层不同职责的组件:

  1. CubeProxy:负责识别请求属于哪个 sandboxID、哪个服务端口
  2. 控制面路由元数据:负责告诉系统这个 sandboxID 当前落在哪台节点、有哪些端口映射
  3. CubeVS / 节点网络层:负责把落到宿主端口的流量 DNAT 回目标沙箱 TAP 设备

这篇文章重点解释:

一个来自外部客户端的 HTTP / WebSocket / SDK 请求,是怎么一步步进入目标沙箱的?


1. 先看结论:入站路径不是单一代理,而是“代理 + 元数据 + 内核转发”

可以先记住这个简化版结论:

CubeProxy 负责“认路”,CubeMaster 维护的路由元数据负责“知道路在哪”,CubeVS 负责在节点内核里把流量真正送进对应沙箱”。

如果只看 CubeProxy,你会误以为它像普通 Nginx upstream 转发。

如果只看 CubeVS,你会误以为访问沙箱只是一条端口映射规则。

实际上,这条链路是分层协作的:

  • L7 入口层:CubeProxy 解析 Host 或路径
  • 控制面状态层:根据 sandboxID 找节点与端口映射
  • L3/L4 节点网络层:在宿主机网卡处把包导回对应沙箱 TAP

2. 第一站:客户端看到的并不是节点地址,而是沙箱域名

在 SDK 与文档里,对外暴露的地址格式是:

text
<port>-<sandboxID>.<domain>

例如:

text
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 模式

最常见的是:

text
49999-<sandboxID>.<domain>

CubeProxy 会从 Host 中解析出:

  • 目标服务端口:如 49999
  • 目标沙箱:如 sb-123

这也是 SDK 默认采用的方式。

3.2 Path 模式

https-and-domain 指南里还能看到路径模式:

text
/sandbox/<sandbox-id>/<container-port>/...

它更适合:

  • 内网演示
  • 不方便配泛域名 DNS 的场景
  • 临时分享

这两种模式虽然入口不同,但最终都要归一到同一件事:

先找到 sandboxID 与目标容器端口,再继续向下游转发。


4. 第三站:为什么 CubeProxy 不能单独完成转发

很多反向代理系统只要有 upstream 列表就能转发,但 CubeSandbox 不行。

原因是沙箱是动态创建和销毁的,且每个沙箱的宿主端口也可能不同。

所以 CubeProxy 并不能把配置静态写死成:

  • sandbox A -> 10.0.0.2:12345
  • sandbox B -> 10.0.0.3:23456

而必须依赖实时路由元数据。

这就是 CubeMaster/pkg/service/sandbox/sandbox_run.gosetProxyToRedis() 的意义所在。

创建成功后,Master 会把这些信息登记成 SandboxProxyMap

  • HostIP
  • SandboxID
  • SandboxIP
  • CreatedAt
  • ContainerToHostPorts

因此,CubeProxy 本质上依赖的是:

  • sandboxID -> hostIP
  • containerPort -> hostPort

这样的动态映射,而不是静态 upstream。


5. 第四站:控制面维护的是“逻辑路由表”

从架构角度看,这张逻辑路由表回答两个问题:

  1. 这个沙箱当前在哪台节点上?
  2. 这个沙箱暴露的某个容器端口,在那台节点上对应哪个宿主端口?

也就是说,外部访问路径其实会经历一次逻辑翻译:

text
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_mapping
  • local_port_mapping

其中与外部访问最相关的是:

  • remote_port_mapping:把宿主端口映射到 (TAP ifindex, sandbox listen port)

nodenic.bpf.cfrom_world 路径上,如果一个包不是某个已有 NAT 会话的回包,它就会尝试按目标宿主端口查这张表。

查到后,内核会做两件事:

  1. 把目标从宿主端口 DNAT 成沙箱实际监听端口
  2. 把包重定向到对应 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 请求的完整落点过程

以下面这个请求为例:

text
GET https://49999-sb-123.cube.app/health

它在系统里的路径可以拆成 8 步:

  1. 客户端把请求发给 *.cube.app,DNS 解析到 CubeProxy 所在机器。
  2. CubeProxy 读取 Host:49999-sb-123.cube.app
  3. CubeProxy 提取出 sandboxID=sb-123 与目标容器端口 49999
  4. CubeProxy 查询控制面维护的路由元数据,得到:
    • 节点 IP,例如 10.0.0.8
    • 宿主端口,例如 21437
  5. CubeProxy 把请求发往该节点入口。
  6. 节点网卡收到目标为 21437 的流量。
  7. CubeVS 的 from_world 程序查 remote_port_mapping[21437],获得目标沙箱 TAP 与容器监听端口 49999
  8. 包被 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。

这就是“请求如何进入沙箱”的完整答案。


12. 建议继续阅读