数据截至 (上游 commit d7bc4affdac9)
网络与隔离:每台沙箱的独立网络栈与出站管控
30 秒导读: 每台 Firecracker 沙箱要有自己的、和邻居互不干扰的网络,还要能被出站策略管住(只准访问某些域名 / CIDR)。E2B 的做法是:给每个沙箱分一个网络槽(network slot)——一套预先算好的 IP + 一个独立的 Linux 网络命名空间 + veth/tap 设备 + iptables NAT。槽由一个池预分配、复用。出站包先过槽内的 nftables 防火墙(放行已建连接、按 IP set 允许/拒绝),TCP 再被 iptables
REDIRECT到节点级的 tcpfirewall 代理,由代理按 HTTP Host / TLS SNI / CIDR 决定放不放行。本章只讲网络与隔离;生命周期看 02,秒级恢复看 03,存储看 04。
1. 这是什么(零基础也能懂)
一句话定义: 一个"网络槽"就是一整套预先算好的、彼此隔离的网络身份——一台沙箱插进这个槽,就自动拥有了一个私有的 IP、一条通往外网的路,以及一圈防火墙。
它要解决的问题。 一个物理节点上同时跑着成百上千台沙箱。它们:
- 不能看见彼此的流量,也不能互相冒充 IP;
- 每台都要能上网(否则装不了包、 调不了 API);
- 但上网要可控——平台或用户可以规定"这台沙箱只准访问
github.com,别的一律禁"。
直觉类比。 把节点想成一栋大楼,每台沙箱是一个房间。网络槽就是给房间配好的一整套门禁:一个独立门牌号(IP)、一条走廊到大门(路由 + NAT)、一个门口保安(防火墙)。保安还会把所有想出门的人先带到大楼统一的前台安检(节点级出站代理),核对访客名单(允许的域名/网段)后才放行。
用起来什么样。 用户创建沙箱时可以在请求里带一段网络策略(下面是 API 层面的示意,非源码):
POST /sandboxes
{
"network": {
"allowOut": ["github.com", "api.openai.com"], // 允许出站的域名/网段
"denyOut": ["0.0.0.0/0"] // ALL_TRAFFIC:先全禁,再按 allowOut 放行
}
}
这段 策略最终会被翻译成沙箱那台 microVM 网络命名空间里的内核规则。本章的主线,就是追这段策略"从 API 请求一路落到内核规则"的旅程。
2. 顶层全景(它大概怎么转)
怎么读下面这张图: 从左(沙箱内部)到右(公网),跟着一个出站包走。竖线是隔离边界——左边是沙箱的独立网络命名空间(netns),右边是节点的主命名空间。
沙箱内 (Firecracker microVM) │ 节点主机 (host netns)
│
guest app │
│ 发一个包,目标=某公网 IP:443 │
▼ │
tap0 ──► [nftables 防火墙 filter 链] │
(169.254.0.22) · 已建连接 → 放行 │
· predefined allow → 放行 │
· predefined deny → 丢弃 │ ← 私有网段(10/8、metadata…)默认禁
· 非TCP user allow/deny │
· 默认 ACCEPT │
│ (TCP 命中 iptables REDIRECT) │
▼ │
[iptables nat PREROUTING] │
dport 80/443/其它 → REDIRECT 到 │
tcpfirewall 代理端口 (5016/5017/5018) │
│ │
▼ │
tcpfirewall 代理 (节点级,一个进程管所有槽) ───┼──► 核对 allowedDomains / CIDR
· HTTP:看 Host 头 │ 放行 → 拨号上游(公网)
· TLS:看 SNI │ 拒绝 → 关连接
· 其它:只看 CIDR │
│
veth-N ◄──────── vpeer(eth0, netns 内) │ ← veth 一端在 netns,一端在 host
SNAT → HostIP(10.11.0.N) ── MASQUERADE ──────┼──► 默认网关 ──► 公网
部件一句话职责:
| 部件 | 干什么 | 在哪(相对克隆根) |
|---|---|---|
Slot | 一个网络槽:算好 IP、建/拆网络栈、装/卸出站规则 | packages/orchestrator/pkg/sandbox/network/slot.go |
Pool | 预分配、复用、回收网络槽的池 | .../network/pool.go |
Storage(KV/local/memory) | 槽的"占位"状态存哪:Consul KV / 本地 netns / 纯内存 | .../network/storage*.go |
CreateNetwork | 真正用 netlink 建 veth/tap、用 iptables 装 NAT | .../network/network.go |
Firewall | 槽内的 nftables 规则集(放行/拒绝的最终裁决) | .../network/firewall.go |
EgressProxy(接口) | 让槽挂上"出站代理"的钩子 + CA bundle + BYOP 能力查询 | .../network/egressproxy.go |
tcpfirewall.Proxy | 节点级 TCP 出站代理:按域名/SNI/CIDR 裁决 | packages/orchestrator/pkg/tcpfirewall/ |
| 用户策略校验 | 把 API 请求里的 allow/deny 规则校验后下发 | packages/api/internal/handlers/sandbox_create.go |
主线走一遍(高层): 用户策略在 API 层被校验 → 随"创建沙箱" gRPC 下发 → 编排器从池里 Get 一个槽 → 槽的 ConfigureInternet 把 allow/deny 翻成 nftables set → 沙箱跑起来,出站包在 netns 里过防火墙、TCP 被 REDIRECT 到 tcpfirewall 代理做域名级裁决 → 沙箱销毁时槽 ResetInternet 后归还池复用。
3. 网络槽:IP 是怎么算出来的
这节讲分配:一个槽的那几个 IP 不是运行时随便拿的,而是按槽的整数下标 Idx 纯算术算出来的。这是整套隔离方案最"便宜"的地方——不需要中心分配器,只要下标不撞,IP 就不会撞。
3.1 一个槽有哪几个地址
Slot 结构体持有一个槽的全部网络身份(slot.go:60-86,Slot):
- HostIP —— 从主机侧访问这台沙箱用的 IP(默认
10.11.0.0/16里的第Idx个)。 - vPeerIp / vEthIp —— veth 网线两端的地址(默认
10.12.0.0/16,每槽占一个/31)。 - tapIp —— netns 里给 Firecracker 用的 tap 设备地址(固定
169.254.0.22/30)。
地址布局的注释写得很清楚(slot.go:46-59)。核心公式在 NewSlot(slot.go:88-148):veth = 第 idx*2 个 IP、vpeer = 第 idx*2+1 个 IP、host = 第 idx 个 IP,全靠 netutils.GetIndexedIP 在 CIDR 块里按下标取。
真实片段(slot.go:93-101,NewSlot):
vEthIp, err := netutils.GetIndexedIP(vrtNetworkCIDR, idx*vrtAddressPerSlot)
// ...
vPeerIp, err := netutils.GetIndexedIP(vrtNetworkCIDR, idx*vrtAddressPerSlot+1)
这两行就是"IP = 下标的函数"的全部秘密:给定 Idx,IP 唯一确定,反过来也能从 netns 名字 ns-<Idx> 反推 Idx(storage_local.go:234,SlotIndexFromNamespace)。
能开多少个槽? GetVrtSlotsSize(slot.go:406-420)按 vrt CIDR 的大小除以每槽 2 个地址算出上限(默认 /16 → 约 3.2 万个槽)。NewSlot 开头就用它做边界检查(slot.go:89-91)。