跳到主要内容

网络与隔离:每台沙箱的独立网络栈与出站管控

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 个 IPvpeer = 第 idx*2+1 个 IPhost = 第 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:177,SlotIndexFromNamespace)。

能开多少个槽? GetVrtSlotsSize(slot.go:406-420)按 vrt CIDR 的大小除以每槽 2 个地址算出上限(默认 /16 → 约 3.2 万个槽)。NewSlot 开头就用它做边界检查(slot.go:89-91)。

3.2 池:预分配 + 复用,把建栈成本挪出热路径

建一套网络栈(建 netns、拉 veth、装十几条 iptables 规则)不快。如果每次创建沙箱都现建,延迟会很难看。于是有了(pool.go:109-141,Pool / NewPool),它维护两条 channel:

channel装什么容量常量
newSlots后台预建好、还没被用过的新槽NewSlotsPoolSize = 32(pool.go:25)
reusedSlots用过、已重置、可再用的槽ReusedSlotsPoolSize = 100(pool.go:26)

后台填充。 Populate(pool.go:160-193)是个循环:不停 createNetworkSlot(pool.go:143-158,先 Storage.Acquire 占位、再 CreateNetwork 真建栈),把成品塞进 newSlots

取槽。 Get(pool.go:195-234)优先从 reusedSlots(复用比新建便宜),拿不到再退到 newSlots。拿到后立刻调 slot.ConfigureInternet 把用户的出站策略装上去;装失败就把槽还回池(pool.go:222-231)。

为什么复用要延迟。 归还走 returnSlot(pool.go:239-260),中间等 ReturnDelay = 3s(pool.go:30)才让槽重新可用——给上一台沙箱的在途请求一点排空时间,避免 IP 被立刻复用导致串包。

3.3 槽状态存哪:三种 Storage

"哪个下标被占了"这件事需要一个存储后端。接口极简(storage.go,Storage:只有 Acquire / Release),有三种实现,按部署形态选:

实现占位状态存哪适用文件
StorageKVConsul KV(nodeID/slotIdx 键,用 CAS 抢占)多节点生产:跨进程/重启不撞槽storage_kv.go
StorageLocal直接看 /var/run/netns 下的 netns 文件单机:把已存在的 netns 当"已占",还会跳过外部程序占用的storage_local.go
StorageMemory进程内一个 []bool测试/最简单场景storage_memory.go

三者都靠 NewSlot 把下标变成实际的 SlotStorageKV.Acquire(storage_kv.go:58-129)先随机试 10 个下标(kv.CAS 抢空键),抢不到再全表扫描找空位——没有全局锁,靠 CAS 的原子性保证同一个槽只有一个赢家(storage_kv.go:96-98 注释明说)。StorageLocal.Acquire(storage_local.go:56-110)则用一把 acquiredNsMu 锁 + isNamespaceAvailable(看文件在不在)来判定空位。


4. 建栈:一次 CreateNetwork 都干了什么

这节讲:CreateNetwork(network.go:78-350)是整章最"体力活"的函数——它在正确的命名空间之间来回跳,用 netlink 拉设备、用 iptables 装规则。理解它就理解了隔离的物理基础。

关键前提:线程锁定。 netns 是线程级的东西。函数一开头就 runtime.LockOSThread()(network.go:80)把 goroutine 钉在一个 OS 线程上,并存下主机 netns,退出时无论如何都切回去(network.go:84-108)。不这么做,goroutine 被调度到别的线程就会"串命名空间"。

建栈步骤(在图里从左到右):

  1. 建 netns。 netns.NewNamed("ns-<Idx>")(network.go:126)。这个 netns 文件也是后面"回收泄漏槽"的锚点(见 §7)。
  2. 拉 veth 网线。veth-<Idx>eth0 一对(network.go:135-145);eth0 端留在 netns 当沙箱网卡,veth 端移回主机(network.go:168)。两端各配一个 vrt IP。
  3. 建 tap 设备。 在 netns 里建 tap0(network.go:204-230),这是 Firecracker microVM 真正挂的网卡。
  4. 装 NAT(netns 内)。 SNAT:出去的包源地址改成 HostIP;DNAT:进来到 HostIP 的包转给沙箱内网 IP(network.go:257-266)。
  5. (可选)打 DSCP 标记。SANDBOX_EGRESS_DSCP>0,给出站包打 DSCP 位(network.go:268-274),供下游 L3 防火墙按优先级识别沙箱流量。
  6. 装防火墙。 InitializeFirewall(network.go:276)—— nftables 那套,下一节详解。
  7. 切回主机 netns,装主机侧规则。 主机到沙箱的路由、FORWARD 放行、出公网的 MASQUERADE(network.go:287-311)。
  8. 装重定向。 把沙箱发往编排器内部 IP 的特定端口,REDIRECT 到本地代理服务:HTTP→hyperloop、111→portmapper、2049→NFS(network.go:313-341)。
  9. 挂出站代理钩子。 s.egressProxy.OnSlotCreate(s, tables)(network.go:344)——让 tcpfirewall 代理装它自己的 REDIRECT 规则(见 §6)。

拆栈是镜像操作。 RemoveNetwork(network.go:352-441)把上面每条规则/设备逐一删掉,并对"本就不存在"的错误做容忍(ignoreExpectedAbsent 家族,network.go:31-76)。关键设计:netns 文件最后才删(network.go:425-438)——只有当所有主机侧状态都拆干净了,才删掉那个"锚点";任何一步失败就保留锚点,让下次重试(含启动时的 reclaim)能重新发现并接着拆,绝不半路孤儿化。

主机默认网关怎么找到的。 MASQUERADE 要知道往哪个网卡出。getDefaultGateway(host.go:35-57)用 netlink 列路由,找 0.0.0.0/0 那条的出口网卡名,进程启动时算一次存进包级变量 defaultGateway(host.go:21)。


5. 防火墙:槽内的 nftables 裁决

这节讲放行/拒绝的最终裁决。§4 里的 iptables 管的是"把 TCP 掰到代理",而谁能出、谁不能出的第一道内核裁决在 nftables 的 Firewall 里(firewall.go)。它每个槽一套(表名 slot-firewall),装在 netns 的 PREROUTING 钩子上。

5.1 四个 IP set + 一条 filter 链

Firewall(firewall.go:27-46)持有四个 nftables set:

set含义谁填的
predefinedAllowSet永远放行(编排器内部 IP + 运维配的 ALLOW_SANDBOX_INTERNAL_CIDRS)平台
predefinedDenySet永远禁(私有网段:10/8127/8169.254/16 含云 metadata、192.168/16 等)平台(shared/pkg/sandbox-network/firewall.go:19-33,DeniedSandboxCIDRs)
userAllowSet用户允许的 CIDR用户策略
userDenySet用户拒绝的 CIDR用户策略

predefinedDenySet防 SSRF 的地基:默认就把所有私有/本地/云 metadata 网段禁掉,沙箱出不去内网。

5.2 规则顺序(命中即停)

installRules(firewall.go:294-328)按固定顺序把规则灌进 filter 链:

PREROUTING filter 链(优先级 -150),匹配来自 tap 接口的包:
1. 已建连接 (ESTABLISHED/RELATED) → ACCEPT ← 保证回包能回来
2. predefinedAllowSet → ACCEPT (所有协议)
3. predefinedDenySet → DROP (所有协议;BYOP 模式下只 DROP 非 TCP)
4. 非TCP + userAllowSet → ACCEPT
5. 非TCP + userDenySet → DROP
6. 默认 → ACCEPT (TCP 交给 iptables REDIRECT → 代理)

为什么规则 3~5 对 TCP 网开一面? 因为 TCP 的裁决交给了 §6 的用户态代理(它能看域名/SNI,内核 set 只能看 IP)。内核这里只兜底非 TCP,并放行"已建连接"让回包顺利返回(addEstablishedAcceptRule,firewall.go:254-277)。规则 1 单列出来,就是为了即便源在 deny set 里,响应包也能回。

5.3 原子换规则:一次 flush,永不半装

用户策略一变,规则要整体替换且不能出现"新链配旧 set"的中间态。ApplyRules(firewall.go:368-389)的做法:先 FlushChain 清空、installRules 重建、bufferUserRules 重灌四个 set——全都只缓冲在连接的 buffer 里,最后一次 conn.Flush() 原子提交。任何失败都触发 resetConn(firewall.go:144-178)换一条干净的 netlink 连接,避免"中毒"的批次泄漏到下次 flush。

bufferUserRules(firewall.go:332-358)每次都把四个 set 全量重置(先恢复 predefined 两个,再用 clearAndReplaceCIDRs 换掉用户两个)。其中 0.0.0.0/0(ALL_TRAFFIC)是个特例:firewall_toolkit 会拒绝 0.0.0.0,所以直接构造 nftables 区间元素绕过(firewall.go:438-473,clearAndReplaceCIDRs)。

5.4 DenyEgress:一次性把沙箱完全断网

有一种特殊场景:编排器要跑一个用完就扔的沙箱(pause-resume 预取采集),它的 envd init 绝不能碰网络。DenyEgress(firewall.go:408-432)把 filter 链重建成只剩两条:已建连接放行 + tap 全丢(addTapDropRule,firewall.go:281-289)。

妙处在于:这样沙箱一个出站连接都开不了(连编排器 IP 都不行),但编排器仍能驱动它——因为控制路径是编排器主动连进沙箱(envd /init、健康探测),回包命中"已建连接"规则(firewall.go:391-403 的长注释解释得很细)。槽层的入口是 Slot.DenyEgress(slot.go:320-349),由恢复选项 WithDenyEgress(sandbox.go:669)在 resume 时触发。


6. 出站代理:TCP 的域名级裁决

这节讲出站流量如何被节点级拦截。nftables 只能按 IP 裁决;而"只准访问 github.com"这种域名级策略,必须有个能看 HTTP Host / TLS SNI 的用户态代理。这就是 tcpfirewall 包。

6.1 EgressProxy 接口:槽和代理的解耦点

槽本身不认识具体代理,只认一个接口(egressproxy.go:9-18,EgressProxy):

  • OnSlotCreate/OnSlotDelete —— 建/拆槽时让代理装/卸自己的 iptables 规则。
  • CABundle() —— 若代理做 TLS 拦截(MITM),给沙箱注入的 CA 证书。
  • SupportsBYOP() —— 本构建能否把 TCP 出站隧穿到用户自带的 SOCKS5 代理。

默认实现 NoopEgressProxy(egressproxy.go:21-43)全是空操作、不支持 BYOP。生产实现是 tcpfirewall.Proxy(proxy.go:28-68),它同时实现了这个接口。

6.2 REDIRECT:把 TCP 掰进代理

tcpfirewall.ProxyOnSlotCreate(proxy.go:151-160)里给每个槽装三条 iptables REDIRECT 规则,按目标端口分流到三个不同的代理端口(proxy.go:59-63):

目标端口代理端口(默认)代理干什么
805016(HTTP)看 HTTP Host 头做域名匹配
4435017(TLS)看 TLS SNI 做域名匹配
其它5018(other)只按 CIDR(不做协议探测,避免卡住 SSH 这类 server-first 协议)

为什么分三个端口?注释说得明白(pool.go:76-83):对 SSH 这类"服务器先说话"的协议做协议探测会阻塞,所以非 80/443 一律只查 CIDR。

6.3 裁决逻辑:allow 优先,默认放行

一个连接进代理后,isEgressAllowed(handlers.go:209-251)按优先级判定:

1. 命中 allowedDomains(Host/SNI)或 allowedCidrs → 放行
2. 命中 deniedCidrs → 拒绝
3. 都不命中 → 默认放行

域名匹配 matchDomain(handlers.go:255-272)支持精确、* 全匹配、*.example.com 后缀通配。

关键安全细节:防 DNS 欺骗。 当靠域名放行时,代理不信沙箱解析出的 IP,而是自己重新拨号该域名(domainHandler,handlers.go:90-95),否则沙箱改 /etc/hosts 就能把允许的域名指向任意 IP。更进一步,proxyWithIPVerification(handlers.go:157-201)在 DNS 解析之后、TCP connect() 之前用 ControlContext 回调检查解析出的 IP 是否落在内网禁段(isIPInAlwaysDeniedCIDRs),挡住 DNS rebinding——握手都不会发生

6.4 BYOP:用户自带出站代理

若用户提供了自己的 SOCKS5 出站代理(BYOP,Bring Your Own Proxy),TCP 就隧穿到用户代理。此时防火墙进入 BYOP 模式:规则 3 只 DROP 非 TCP,TCP 全交给用户代理(firewall.go:311-315)。

BYOP 的端点校验ValidateEgressProxy(shared/pkg/sandbox-network/byop.go:128-182):代理地址必须是 host:port;解析出的每个 IP 都不能落在禁段(IsIPInDeniedSandboxCIDRs,byop.go:198-210,含 0.0.0.0/8 这种没法编进 nftables 的块),否则用户就能借 BYOP 探测 E2B 内网。SOCKS5 用户名/密码各限 255 字节(RFC 1929)。


7. 用户策略如何一路落到内核规则

现在把两头接起来:用户在 API 请求里写的 allow/deny,是怎么变成 §5 那些 nftables set 的

第一步:API 层校验(sandbox_create.go)。 validateNetworkConfig(sandbox_create.go:629)是入口,其中 validateEgressRules(sandbox_create.go:676-711)管出站规则:

  • denyOut 每一项必须是合法 IP/CIDR(不能是域名)(sandbox_create.go:677-685)。
  • allowOut 可以是 IP/CIDR 或域名(ParseAddressesAndDomains 分开)。
  • 关键约束: 如果 allowOut 里有域名,那 denyOut 必须包含 0.0.0.0/0(ALL_TRAFFIC)(sandbox_create.go:699-707,报错 ErrMsgDomainsRequireBlockAll)。为什么?域名放行靠的是 §6 的用户态代理,而内核 nftables 只认 IP——只有先用 0.0.0.0/0 在"其它端口"把默认出站堵死,域名白名单才真正是白名单,否则默认放行会让白名单形同虚设。

BYOP 代理配置在这里也会过一遍 ValidateEgressProxy(sandbox_create.go:232-246),并受 BYOPProxyEnabledFlag 特性开关控制(sandbox_create.go:224)。

第二步:下发到槽。 校验通过的策略随创建请求进到编排器,Pool.Get 里调 slot.ConfigureInternet(slot.go:251-284):它先看有没有用户规则或 BYOP(slot.go:257-266),没有就直接返回(默认全放行,省事);有就进 netns 调 Firewall.ApplyRules,把 allowedCidrs/deniedCidrs 灌进 §5 的 set。域名不进内核 set——它们留给 tcpfirewall 代理在运行时按 Host/SNI 匹配。

第三步:运行中更新。 沙箱可以中途改网络策略:Slot.UpdateInternet(slot.go:287-314)同样进 netns 调 ApplyRules 原子换规则;编排器侧入口在 server/sandboxes.go:398,更新失败会回滚到旧配置(sandboxes.go:410)。

销毁时复位。 槽归还池前 ResetInternet(slot.go:351-378)把 ApplyRules(byop=false, nil, nil) 灌回去——注意它显式关掉 BYOP,免得下一个租户继承上一个的"只禁非 TCP"规则(slot.go:368-369 注释)。只有真设过自定义规则(firewallCustomRules 标志)才需要复位,省掉无谓的内核操作。


8. 边界、回收与坑

启动回收泄漏槽。 编排器崩溃重启后,上一轮的 netns/规则可能残留。ReclaimLeakedSlots(reclaim.go:15-42)扫 /var/run/netns,对每个 ns-<Idx> 重建 SlotRemoveNetwork 拆干净。这依赖 §4 的约定:netns 文件是每个泄漏槽的可靠记录(建栈时最先建、拆栈时最后删)。CreateNetwork 复用槽前也会先检测并清掉"陈旧锚点"(network.go:110-123)。

已知边界 / 坑:

  • 只支持 IPv4 的用户规则。 AddressStringToCIDR(shared/.../firewall.go:52-58)注释明说只处理 IPv4;禁段里虽列了 IPv6,但用户 allow/deny 面向 IPv4。
  • 域名白名单必须配 ALL_TRAFFIC。 见 §7,否则默认放行会架空白名单——这是最容易踩的配置坑。
  • KV 抢槽无全局锁。 StorageKV 靠 CAS 抢占,极端并发下会多试几次;设计上接受这种"乐观并发 + 重试"(storage_kv.go:96-98)。
  • 代理是节点级单点。 一个 tcpfirewall.Proxy 进程服务节点上所有槽,按 LifecycleID 做每沙箱连接数限流(proxy.go:246-258);代理挂了会影响该节点全部沙箱的 TCP 出站。
  • DenyEgress 沙箱不能碰 NFS。 完全断网的槽连编排器 IP 都禁,所以那台 throwaway 沙箱 resume 时必须同时抑制卷挂载,否则 /init 会卡在(已被禁的)NFS 挂载上(firewall.go:399-403)。

9. 代码地图(导航索引)

按符号名 grep 即可定位;行号 as-of sourceCommit

主题文件(相对克隆根)关键符号
槽的 IP 布局与分配packages/orchestrator/pkg/sandbox/network/slot.goSlotNewSlotGetVrtSlotsSize
装/卸/复位出站规则.../network/slot.goConfigureInternetUpdateInternetDenyEgressResetInternet
槽池(预分配/复用/回收).../network/pool.goPoolNewPoolPopulateGetreturnSlotConfig
槽占位存储.../network/storage.gostorage_kv.gostorage_local.gostorage_memory.goStorageStorageKV.AcquireStorageLocal.AcquireisNamespaceAvailable
建/拆网络栈(netlink+iptables).../network/network.goCreateNetworkRemoveNetworkignoreExpectedAbsent
主机默认网关.../network/host.gogetDefaultGatewaydefaultGateway
nftables 防火墙.../network/firewall.goFirewallNewFirewallinstallRulesApplyRulesDenyEgressclearAndReplaceCIDRs
出站代理接口.../network/egressproxy.goEgressProxyNoopEgressProxy
回收泄漏槽.../network/reclaim.goReclaimLeakedSlots
节点级 TCP 出站代理packages/orchestrator/pkg/tcpfirewall/proxy.gohandlers.goProxyOnSlotCreateisEgressAllowedmatchDomainproxyWithIPVerification
禁段 / BYOP 校验packages/shared/pkg/sandbox-network/firewall.gobyop.goDeniedSandboxCIDRsValidateEgressProxyIsIPInDeniedSandboxCIDRs
用户策略校验下发packages/api/internal/handlers/sandbox_create.govalidateNetworkConfigvalidateEgressRulesErrMsgDomainsRequireBlockAll