substrate 网络恢复部分实现
我已经有了完整的理解。现在让我总结 restore 过程中网络的完整处理逻辑。
关键要点:
microVM 的网络拓扑(单命名空间):与 gVisor 不同,microVM(cloud-hypervisor)使用单个命名空间(actorNS),tap 和 atunnel 的 sockets 共享同一个 namespace(GatewayNetNS == RuntimeNetNS,见 SetupSandboxNetwork 中 atunnelNS := actorNS,因为 cfg.Veth 为 false,microVM 用 tap 而不是 veth)。网络拓扑:
- 命名空间
ActorNetNSName(actorUID) - guest 的 eth0(virtio-net)MAC 固定为
actorGuestMAC = 02:a8:1e:00:00:02,IP 为169.254.17.2/30 - host 侧 tap(
tap0_kata)MAC 固定为gatewayMAC = 02:a8:1e:00:00:01,地址169.254.17.1/30(HostVethAddr) - nftables 在 gateway namespace 中把出站 TCP 重定向到 atunnel 的 egress port
现在让我完整梳理 restore 流程中的网络处理,回答用户的问题。
让我再确认一下 restore 流程中涉及网络的关键调用顺序。让我仔细看看 restore.go 的调用顺序,以及 hostActor/endPreviousActivation 的作用。
关键的网络处理流程在 RestoreWorkload 和 restoreMemoryFidelity 中:
RestoreWorkload 顶层:
s.tunnel.Deactivate(...)— 停用旧的 tunnel(旧的网络接入层)s.endPreviousActivation(...)— 结束之前的激活(如果有旧 VM 还在跑,停掉它)s.hostActor(ctx, attribution, resumesGuest)— 设置新的沙箱网络(ateomnet.ServeSandbox→SetupSandboxNetwork→ 创建命名空间、tap 配置等)
restoreMemoryFidelity:
s.tunnel.PrepareEgress(...)— 准备新的 egress(签发证书、建立 gateway client)s.cleanupSandboxState(...)— 清理残留状态- 读取快照的 net devices(
SnapshotNetDevices) - 对每个 net device 调用
setupActorTap(...)创建新的 tap(并获取 FD) ch.LaunchVMM+RestoreWithNetFDs(把新的 tap FD 通过 SCM_RIGHTS 传给 CH)s.tunnel.Activate(...)— 激活新的 tunnel(ingress + egress)
失败清理路径:
s.tunnel.Deactivate再次调用(失败时)s.unhostActor(...)(清理网络)
现在让我验证网络具体如何被"旧的"处理与"新的"添加。
我已经有足够信息来详细回答了。让我组织一下回答。
让我再确认 s.tunnel 在 RestoreWorkload 里的位置和 hostActor 里网络被替换的逻辑。hostActor 调用 admitActor,如果 actor 已经存在(旧网络),会 stale.Close(ctx) 关闭旧网络。这正是"旧网络被清理"的地方。
让我也看一下 restore 里 tap 是怎么重复利用命名空间的 —— s.sandboxNetNS(actorUID) 返回的是 hosted.network.Network.GatewayNetNS,这个就是 hostActor 新建的命名空间。tap 在里面创建。
我现在有完整信息了,可以给出详细分析。
我已经收集了所有必要的信息。下面给出详细分析。
网络整体架构(microVM)
在深入 restore 之前,先理解 microVM 的网络拓扑。与 gVisor 不同,microVM(cloud-hypervisor)使用单个网络命名空间:
SetupSandboxNetwork(internal/ateomnet/sandbox.go)里,microVM 不走 veth 分支(cfg.Veth=false),所以atunnelNS := actorNS——即 GatewayNetNS 与 RuntimeNetNS 是同一个命名空间(见SandboxNetwork注释 "For microVMs it shares RuntimeNetNS")。- 命名空间名为
nodepath.ActorNetNSName(actorUID),每次hostActor都会netns.CreateNamed新建,unhostActor/CleanupSandboxNetwork会netns.RemoveNamed删除。 - 该命名空间里住着:
- tap(
tap0_kata/tap{i}_kata),即 guest 的 virtio-net 在 host 侧的端点。tap 的 MAC 固定gatewayMAC=02:a8:1e:00:00:01、地址169.254.17.1/30。 - guest eth0 侧 MAC 固定
actorGuestMAC=02:a8:1e:00:00:02、IP169.254.17.2/30。 - atunnel 的 egress/DNS sockets,以及把出站 TCP 重定向到 atunnel 的 nftables 规则(
installEgressRedirect)。
- tap(
固定 MAC 的原因(net.go 注释)很关键:CH 快照会把 guest 内核的 ARP cache(含 gateway 的 MAC 条目)和接口配置冻结进快照。restore 时如果 tap 换了新 MAC,guest 已冻结的 ARP 条目会指向错误的 MAC,导致 egress 黑洞直到条目过期。所以 restore 重建的 tap 必须复用同一 MAC。
restore 中网络的完整处理流程
一、旧网络的"拆除/停用"分两步
第 1 步:tunnel.Deactivate(RestoreWorkload 最前面,第 141 行)
if err := s.tunnel.Deactivate(ctx, ateomstats.ActorAttributionFromRequest(req)); err != nil {
return nil, err
}
Deactivate(ateomtunnel.go)调用 Ingress.Deactivate 和 Egress.Deactivate——即停止向该 actor 接入新流量,并排空(drain)正在进行的连接。注意这一步只停用 atunnel 的流量接入层,不会删除网络命名空间、不关 tap。它的作用是:在真正拆网络前,先把入口/出口流量掐断并排空,保证拆网络时没有 in-flight 连接。
第 2 步:endPreviousActivation(第 150 行)
if err := s.endPreviousActivation(ctx, attribution.UID, req.GetActorDirs()); err != nil {
return nil, err
}
如果同一 actor 上一个激活还留着一个在跑的 micro-VM(比如上次 checkpoint 在 teardown 前失败、或 retry Run/Restore),endPreviousActivation 会:
- 调用
stopActorVM→teardownActor,把旧 VMM 进程 kill 掉(chCmd.Process.Kill)、关闭 kata-agent 连接、kill virtiofsd,并setGuestStats(nil)。 - 此时旧 VM 停止,其 tap 也就随之失效(tap FD 由 VM 持有,VM 死掉后 host 侧的 tap 链路不再有对端)。
但注意:teardownActor 只停 VM,不会删除命名空间。命名空间是这次 re-host 中被 hostActor 里 admitActor 返回的 stale 网络所替换/关闭的(见下)。
二、hostActor 建立"新的"沙箱网络
RestoreWorkload 第 158 行调用:
if _, err := s.hostActor(ctx, attribution, resumesGuest); err != nil {
return nil, err
}
hostActor(hosted.go)做了两件事:
admitActor:如果 actor 已 hosted(有旧网络),取出stale = old.network。- 如果
stale != nil,调用stale.Close(ctx)——这才是真正清理旧网络的地方(关闭旧 sockets、停 DNS、CleanupSandboxNetwork→netns.RemoveNamed删除旧命名空间,删掉就带走了旧的 veth/tap)。之后ateomnet.ServeSandbox→SetupSandboxNetwork会创建全新的命名空间,netns.CreateNamed,并为新命名空间:- 启动 atunnel egress 监听、DNS relay;
installEgressRedirect装新的 nftables 重定向规则。
所以 restore 的语义是:全新搭建一个沙箱网络,而不是"复用并修补旧网络"。旧命名空间被整体替换。
三、restoreMemoryFidelity 里"重建 tap 并交给新的 VMM"
网络在 restoreMemoryFidelity 里继续完善(第 220 行起):
-
tunnel.PrepareEgress(第 222 行):为这次 restore 签发新的 actor 证书、建立 egress gateway client(ActorEgress)。这是"新的出向能力"。 -
cleanupSandboxState(第 226 行):kill 残留的 cgroup 进程 +kata.CleanupSandboxState(清理旧的 host 侧状态)。 -
读取快照的 net 设备
ch.SnapshotNetDevices(restoreDir)(第 336 行):解析快照config.json里的每个 virtio-net 设备(id、queue pairs、MAC)。 -
对每个 net 设备重建 tap(第 339-350 行):
files, terr := setupActorTap(ctx, s.sandboxNetNS(actorUID), fmt.Sprintf("tap%d_kata", i), nd.QueuePairs)s.sandboxNetNS(actorUID)返回的是第 2 步hostActor刚新建的命名空间(hosted.network.Network.GatewayNetNS)。setupActorTap(net.go)在该命名空间里:删除同名旧 tap → 创建新 tap → 设置固定 MACgatewayMAC(保证和 guest 冻结的 ARP 条目匹配)→ 赋HostVethAddr→ 设 up。返回 tap 的 FD。- 关键:快照的 virtio-net 是 fd-backed,所以 CH restore 需要全新的 tap FD(
net_fds)。
-
ch.LaunchVMM+client.RestoreWithNetFDs(第 365-396 行):启动一个只有 api-socket 的裸 CH,然后vm.restore,把新的 tap FD 通过 SCM_RIGHTS(unix socket ancillary data)传给 CH(RestoreWithNetFDs里unix.UnixRights(fds...)),net_fds与快照的 net 设备 id 一一对应。CH 会在这些 FD 上重建 virtio-net 设备。之后client.Resume恢复 guest。guest 冻结的接口配置(eth0 的 IP/MAC/路由/ARP)随内存快照回来,正好匹配新的 tap。
四、激活"新的"隧道接入
第 485 行:
if err := s.tunnel.Activate(p.attribution(), s.sandboxDialer(p.actorUID), egress); err != nil {
return err
}
tunnel.Activate(ateomtunnel.go):
Ingress.Activate:开始通过sandboxDialer把入向流量拨进新的沙箱命名空间(Session.Dialer用GatewayNetNSFD 进入命名空间拨号)。Egress.Activate:激活第 1 步PrepareEgress准备好的 egress。
setRunningVM 之后 actor 正式对外提供服务。此时新旧网络交接完成。
五、失败路径的"回滚"(旧网络清理与恢复失败时的拆网络)
restore 失败时要拆除刚建到一半的网络,代码里有多处:
-
hostActor失败前:RestoreWorkload顶部有defer:若retErr != nil,用 detached 的 context 调s.unhostActor(cleanupCtx, attribution.UID),即删除刚建的沙箱网络(hosted.network.Close)。 -
restoreMemoryFidelity内(第 313 行起):失败时s.tunnel.Deactivate(cleanupCtx, ...)再次停用新网络;- kill 新起的 VMM(
chCmd.Process.Kill)、virtiofsd; - 关闭 tap FD(
tapFiles的 defer close); imagecache.UnmountAllUnder卸载 overlay。
-
另外,如果
hostActor之前就存在旧网络,stale.Close已经把旧的删掉了;新网络失败后由上述 cleanup 拆除,从而恢复到一个"没有网络"的干净状态,可重试。
新旧网络的处理总结
| 阶段 | 动作 | 位置 |
|---|---|---|
| 停用旧接入 | tunnel.Deactivate:掐断 ingress/egress 流量并排空 |
RestoreWorkload 开头 |
| 停旧 VM | endPreviousActivation → teardownActor:kill 旧 CH/virtiofsd/agent |
RestoreWorkload |
| 换新命名空间 | hostActor:admitActor 拿旧 stale 网络并 stale.Close()(删旧 netns/veth),再 ServeSandbox 新建 netns + 装 nftables + atunnel 监听 |
RestoreWorkload |
| 备新证书 | tunnel.PrepareEgress 签发新 actor 证书、建 egress client |
restoreMemoryFidelity |
| 重建 tap | SnapshotNetDevices + setupActorTap:在新 netns 建 tap,固定 gatewayMAC(匹配 guest 冻结 ARP),返回 FD |
restoreMemoryFidelity |
| 交 FD 给 CH | RestoreWithNetFDs:SCM_RIGHTS 把新 tap FD 传给 CH 的 vm.restore(net_fds) |
restoreMemoryFidelity |
| 激活新接入 | tunnel.Activate:ingress 经新 netns 拨号 + egress 激活 |
restoreMemoryFidelity |
| 失败回滚 | unhostActor / tunnel.Deactivate / kill 新 VMM / 关 tap FD |
各处 defer |
核心思想:restore 不沿用旧网络,而是整体拆除旧网络、在一个全新的命名空间里重建 tap 并交给新的 VMM,通过固定 MAC 保证与 guest 冻结的网络状态(ARP/接口配置)无缝衔接。
虚拟机内旧的网络没看到调用删除动作
现在我完全理解了。用户的问题非常到位,让我给出准确的回答。
关键点在于:restore 不是"运行一个全新的 guest",而是恢复 guest 的整块内存快照(包括 guest 内核的网络栈状态)。 所以:
guest 内部(虚拟机内)的网络并没有被"删除",而是整体被恢复了。 vm.restore 恢复的是 guest 的完整 RAM 镜像——包括 guest 内核里 eth0 接口、IP 地址 169.254.17.2/30、路由表、ARP 表、neighbor 缓存等所有网络栈状态。这些状态原封不动地随内存快照一起回来,根本不需要、也没有"删除旧网络"的动作。
让我梳理清楚 guest 侧网络状态在整个生命周期中的来源:
-
冷启动(cold boot / RunWorkload):
configureGuestNetwork通过 kata-agent 在 guest 内配置网络——UpdateInterface(设置 eth0 的 MACactorGuestMAC、IP169.254.17.2/30、MTU)、UpdateRoutes(连通路由 + 默认路由到 gateway169.254.17.1)、AddARPNeighbors(把 gateway 的 ARP 条目钉死为gatewayMAC,状态 NUD_PERMANENT)。 -
快照(CheckpointWorkload):guest 整块内存(含上述所有网络栈状态)被冻结到快照里。
-
restore(RestoreWorkload):
vm.restore把这块内存原样恢复回来。guest 内部的 eth0、IP、路由、ARP 全都"冻结着回来",无需重新配置、也无需删除任何旧的。这正是为什么:- 快照的 virtio-net 设备(eth0)的 MAC 必须保持不变(
actorGuestMAC),因为 guest 内核里 eth0 的接口配置(MAC、IP)是冻结的; - host 侧重建的 tap 必须复用固定 MAC
gatewayMAC,因为 guest 内核里 gateway 的 ARP 条目(映射到gatewayMAC)也是冻结的——如果 tap 换了个新 MAC,guest 已冻结的 ARP 会说"gateway 在gatewayMAC",而实际包会发到新的 tap MAC,造成黑洞。
- 快照的 virtio-net 设备(eth0)的 MAC 必须保持不变(
所以"旧的网络"在 guest 内根本不是被删除的——它是被整体快照并整体恢复的。真正被"替换/重建"的只有 host 侧的东西:命名空间、tap、atunnel 隧道接入层。而 host 侧的旧物(旧命名空间、旧 VM 的 tap)在 restore 前的 hostActor(stale.Close)和 endPreviousActivation/teardownActor(kill 旧 VM)里被清理。
不过,我需要更严谨地确认一个细节:CH 的 vm.restore 在恢复 virtio-net 设备时,会不会因为"快照里记录的是旧的 fd-backed net"而做某种重新初始化?让我确认 RestoreWithNetFDs 的语义——它明确说"CH reopens the device on the FDs we pass",即 CH 在新的 tap FD 上重建 virtio-net 设备,但guest 内部看到该设备的方式(MAC/IP/路由/ARP)来自内存快照,不会变。
让我也确认一下 restore 路径是否调用了 configureGuestNetwork——从 restore.go 的代码看,restoreMemoryFidelity 里没有调用 configureGuestNetwork(那是 cold boot 才调用的)。这正好印证:restore 不需要在 guest 里重新配置网络,因为 guest 网络状态随内存恢复。
让我最终确认 restore 里确实没有对 guest 内网络的任何 agent 调用(除了 reseed、stats、log forwarding)。
从 restore.go 可以看到,restore 后通过 kata-agent 做的调用只有:
reseedGuestCRNG(ReseedRandomDev)startActorLogForwarding(ReadStdout/ReadStderr)- stats
没有 UpdateInterface / UpdateRoutes / AddARPNeighbors。这完全印证了我的分析。
我现在可以给出完整、准确的回答。
你的质疑非常准确,这正是理解 restore 网络处理的关键。答案是:guest(虚拟机)内部的"旧网络"根本没有被删除,也不需要被删除——它是被整体冻结进快照、又整体恢复回来的。
为什么 guest 内看不到"删除旧网络"的调用
vm.restore 恢复的是 guest 的整块 RAM 镜像(RestoreWithNetFDs 里 vm.restore 从 sourceDir 读内存快照),而不是重新启动一个 guest。guest 内核里的整个网络栈状态——eth0 接口本身、它的 MAC/IP、路由表、ARP/neighbor 缓存——都是 RAM 的一部分,随快照原封不动地回来。
所以 guest 内部根本不存在"旧网络待删除"这回事。它就是同一个 guest 网络栈,只是暂停过、又继续跑而已。
证据链
1. 冷启动时,guest 网络是通过 kata-agent 显式配置的(run.go configureGuestNetwork):
UpdateInterface:eth0 设 MAC=actorGuestMAC、IP=169.254.17.2/30、MTUUpdateRoutes:连通路由 + 默认路由到 gateway169.254.17.1AddARPNeighbors:把 gateway 的 ARP 条目钉死为gatewayMAC(NUD_PERMANENT)
2. 快照把这些全部冻结: Checkpoint 时整个 guest RAM(含上面所有网络栈状态)落盘。
3. restore 时没有、也不需要任何 guest 内网络配置调用: 通读 restoreMemoryFidelity,resume 后通过 kata-agent 只做了三件事:
reseedGuestCRNG(重播随机数熵)startActorLogForwarding(stdout/stderr 转发)- stats
完全没有 UpdateInterface / UpdateRoutes / AddARPNeighbors——对比冷启动路径,restore 路径刻意跳过了 configureGuestNetwork。这正说明 guest 内网络状态是"冻着回来的",无需重配。
那 host 侧呢?host 侧的"旧网络"才是被真正删除/重建的
Guest 内网络不用动,是因为 host 侧把对端重建得和快照完全一致即可。真正被替换/清理的是 host 侧的东西:
| host 侧旧物 | 何时被清理 |
|---|---|
| 旧 VMM 进程(持有旧 tap FD) | endPreviousActivation → teardownActor(kill CH) |
| 旧沙箱命名空间(含旧 tap/veth、atunnel sockets、nftables) | hostActor 里 admitActor 返回的 stale.Close() → CleanupSandboxNetwork → netns.RemoveNamed |
| 旧 atunnel 接入 | RestoreWorkload 开头的 tunnel.Deactivate |
host 侧新物:
hostActor新建命名空间 + nftables + atunnel 监听setupActorTap在新命名空间重建 tap,复用固定 MACgatewayMACRestoreWithNetFDs把新 tap FD 通过 SCM_RIGHTS 交给 CH 的vm.restore(net_fds)
固定 MAC 的意义(正是"guest 内网络不动"的代价/前提)
因为 guest 内的 eth0 配置和 gateway 的 ARP 条目都是冻结的:
- guest eth0 的 MAC 冻结为
actorGuestMAC→ CH restore 必须在新的 virtio-net 上恢复同一 MAC(SnapshotNetDevices读回快照里的 MAC,guest 侧的配置才依然有效) - guest 认为 gateway 在
gatewayMAC→ host 重建的 tap 必须复用gatewayMAC(net.go 注释:否则 guest 冻结的 ARP 会指向旧 MAC,egress 黑洞直到条目过期)
一句话总结
guest 内没有"旧网络"要删——它是整个内存快照的一部分,vm.restore 原样把它恢复回来(所以 restore 路径连 configureGuestNetwork 都不调用);真正被"删除再重建"的是 host 侧的命名空间、tap 和 atunnel 隧道,而为了配合 guest 冻结的网络栈状态,host 重建的 tap 必须沿用固定的 gatewayMAC/actorGuestMAC。
我对代码库的理解已经比较充分了。现在让我整理一下对这个架构对比问题的回答。这是关于原生 Kata 的 tc filter 转发机制与这个 substrate ateom-microvm 方案的区别。
需要澄清的是:原生 Kata 实际上并不使用 tc filter 作为其默认的网络转发机制(kata 默认使用 CNI bridge + veth + iptables NAT,或者 macvtap 等)。用户提到的 "tc filter 转发" 可能指的是某些 CNI 插件(如基于 TC BPF 的转发)或特定的网络配置。不过,让我基于对这个项目代码的理解,以及 Kata 网络的通用机制来做架构对比。
我需要先理清这个 substrate 项目里 ateom-microvm 的网络方案到底是什么样子的,然后再与原生 Kata 对比。让我基于已经收集到的代码事实来组织回答。
我已经掌握的关键事实:
-
ateom-microvm 网络(本仓库):
- 单命名空间(RuntimeNetNS == GatewayNetNS)
- 一个 tap 设备(
tap0_kata)作为 guest virtio-net 的 host 端 - tap 固定 MAC
gatewayMAC,地址169.254.17.1/30 - guest eth0 固定 MAC
actorGuestMAC,地址169.254.17.2/30 - nftables NAT prerouting 重定向出站 TCP 到 atunnel 的 egress port(保留
SO_ORIGINAL_DST) - atunnel 的 ingress/egress/DNS 在这个命名空间里提供服务
- 每个 actor 一个独立命名空间
-
原生 Kata 网络:
- 通常由 kata shim 通过 CNI 配置
- 默认用 CNI bridge + veth pair + iptables NAT
- 每个 sandbox 有一个 netns
- guest 内通过 kata-agent 配置
让我组织回答,重点在架构层面的区别。我会诚实地说明:这个项目代码里没有 tc filter 的实现(无论是 ateom-microvm 还是它的 kata 集成),所以对比的是"原生 Kata 常见的 CNI/tc 转发模型"与"本仓库 ateom-microvm 的 atunnel+nftables 模型"。
让我确认一下 nftables 重定向的细节,以及 atunnel 是怎么做 ingress 的(这是和 Kata 差异最大的地方)。
我已经有足够信息了。让我给出回答。
让我再确认一下 ingress 是怎么进入沙箱的(sandboxDialer → Session.Dialer),以及 egress 是怎么出去的(nftables redirect → atunnel egress)。这些我已经在前面读到了。
现在我可以组织一个完整、准确的架构对比回答。
架构对比:原生 Kata 网络转发 vs. 本仓库 ateom-microvm 的 atunnel+nftables 模型
首先需要澄清一个事实:本仓库(substrate)的 ateom-microvm 并不使用 tc filter(我上一轮已经全面验证过代码库中没有 tc/qdisc 相关代码),它用的是 nftables netfilter 重定向 + atunnel 用户态隧道。所以这里对比的是"原生 Kata 常用的网络转发模型(含其 tc 场景)"与"substrate 的自研网络模型"。
原生 Kata(及其常见 CNI 转发模型)
Kata 的沙箱网络由 kata shim 通过 CNI 插件在宿主机上搭好,guest 侧再由 kata-agent 同步配置。典型链路(CNI bridge 模式):
[Guest eth0] --virtio-net--> [host tap/macvtap]
| |
kata-agent CNI 插件(host)
(UpdateInterface/ bridge + veth + iptables NAT
UpdateRoutes)
- 转发/隔离模型:内核数据面。CNI 插件在 host 的 netns 里建 bridge + veth pair + iptables NAT,guest 包从 virtio-net 进 tap/macvtap,再到 bridge/veth 走常规 L3 路由。每个 sandbox 一个 netns,包进出靠内核 netfilter(iptables/nftables)做 NAT/隔离。
- tc filter 的位置:在 Kata/CNI 语境下,tc(traffic control)主要被两类场景使用——(a) CNI 插件做带宽限速 / QoS(在 tap/veth 上挂 tbf/hfsc 等 qdisc 和 filter),或 (b) 某些 TC-BPF 转发方案(用
clsactqdisc + BPF filter 在 ingress/egress 钩子做转发/策略)。它不是 Kata 的默认转发路径,转发本身仍靠内核路由/NAT。 - guest 内配置:kata-agent 在 guest 里
UpdateInterface+UpdateRoutes配 eth0。 - 数据面处理位置:全部在内核(bridge/veth/iptables)。旁路代理/策略要么用 iptables REDIRECT/TPROXY,要么在 guest 内放代理。
substrate ateom-microvm 的模型(本仓库实际代码)
[Guest eth0 (169.254.17.2/30)] --virtio-net--> [host tap0_kata (169.254.17.1/30)]
| |
(冻结配置随快照恢复) atunnel(同一 netns)
| |
+------ nftables NAT prerouting -----------+
| (出站TCP -> atunnel egress port) |
| |
sandboxDialer --> 进 atunnel ingress 后回拨到 netns
数据面差异非常明显:
-
转发发生在用户态 atunnel,而不是内核路由/NAT。关键在
installEgressRedirect(internal/ateomnet/sandbox.go):用 nftables NAT prerouting 把沙箱内 actor 的出站 TCP 全部重定向到 atunnel 的 egress 端口,同时保留SO_ORIGINAL_DST。atunnel 这个用户态进程接管流量,做 mTLS 隧道到远端 egress gateway。也就是说,网络策略/隧道的决策点从"内核 netfilter"上移到了"用户态代理"。 -
Ingress 也是用户态代理拨号:
tunnel.Activate后,入向流量由 atunnel 的 ingress server 接收,再通过sandboxDialer(Session.Dialer,用 netns FD 进入沙箱命名空间拨号)拨进 guest 的 eth0。这是反向代理/拨号模型,不是内核转发。 -
单命名空间、无 bridge/veth 二层桥:microVM 用一个 tap(
tap0_kata)直接挂在 guest virtio-net 上,GatewayNetNS == RuntimeNetNS(SetupSandboxNetwork里atunnelNS := actorNS),没有 CNI bridge、没有 veth pair。网络是点对点:guest 只有 eth0 ↔ tap,没有二层桥。 -
固定 MAC + 冻结配置是 restore 的核心前提:因为 restore 恢复的是 guest 整块内存(含 eth0 的 IP/MAC/路由/ARP),host 端必须配合——tap 固定
gatewayMAC、guest eth0 固定actorGuestMAC(net.go)。而原生 Kata 的 checkpoint/restore 在这方面没有这种"每 actor 隔离的 atunnel + 固定 MAC"设计,其网络恢复通常依赖 kata-agent 重新配 guest 网络。
关键架构区别总结
| 维度 | 原生 Kata(常见 CNI 模型) | substrate ateom-microvm |
|---|---|---|
| 数据面处理 | 内核:bridge/veth + iptables/nftables NAT、路由 | 用户态 atunnel:nftables 重定向出站 TCP + 用户态隧道代理 |
| 出站策略点 | 内核 netfilter(REDIRECT/TPROXY)或 guest 内代理 | nftables prerouting 重定向到 atunnel egress 端口(保留 ORIGINAL_DST),mTLS 隧道 |
| 入站 | 内核转发到 guest(NAT/路由) | atunnel ingress server 经 sandboxDialer 拨号回沙箱 |
| 二层结构 | bridge + veth(多接口桥接) | 单 tap 点对点,RuntimeNetNS == GatewayNetNS,无 bridge |
| 隔离粒度 | 每 sandbox 一个 netns | 每 actor 一个 netns |
| tc filter | 部分 CNI 用于 QoS/限速(tbf/hfsc)或 TC-BPF 转发;非默认转发路径 | 本仓库完全不用 tc/qdisc |
| 快照/恢复适配 | kata-agent 重建 guest 网络 | 冻结配置随内存快照恢复 + host 侧固定 MAC tap 配合 |
| 地址 | 由 CNI 分配(每 pod 一段) | 固定 link-local 169.254.17.0/30(.1 gateway / .2 guest) |
一句话
原生 Kata 把网络留在内核(bridge/veth + netfilter NAT,tc 主要用于限速这类旁路),转发/隔离靠内核;而 substrate 的 ateom-microvm 把网络上收到用户态 atunnel——用一条 nftables NAT 规则把出站 TCP 摘到 atunnel 做 mTLS 隧道、入站由 atunnel 反向拨号进沙箱,并且为了配合"内存快照整体恢复 guest 网络栈",刻意用单 tap 点对点 + 固定 MAC 保证 guest 冻结的 ARP/接口配置在 restore 后依然有效,全程不碰 tc。
现在我已经掌握了完整的网络数据通路。让我最后确认一下 ingress 如何通过 Dialer 进入沙箱拨号,以及 netns.Listen/netns.Dialer 的工作方式,确保描述准确。
我已经掌握了足够的信息。让我查看 ingress 中 dial 是怎么用的(如何进入沙箱拨号到 guest),以及 DNS relay 的作用,然后组织回答。
关键代码事实我已经掌握:
serveSandboxEgress用netns.Listen(ctx, ns, ports)在沙箱命名空间内监听EgressPort,然后e.Bind(actorUID)绑定激活,e.serve处理连接(通过originalDestination读SO_ORIGINAL_DST得知真实目标,再用 egress 隧道拨到远端网关)。installEgressRedirect用 nftables 在 prerouting 把出站 TCP 重定向到该端口。- ingress 是 HTTPS 反向代理,
dial通过sandboxDialer→Session.Dialer→netns.Dialer(GatewayNetNS)进入沙箱命名空间拨号到 guest eth0 的ActorVethIP:80。
我现在可以给出完整的、基于代码事实的回答。让我组织好 ingress 拨号的具体目标端口。
从 net.go 已知 ActorHTTPUpstream = "http://" + ActorVethIP + ":80"。所以 ingress 反向代理到 guest eth0 的 80 端口。让我确认一下这个 upstream 的细节,看看 newActorProxy。
我已经有足够信息了。让我给出完整回答。
给出substrate 网络工作原理,流量是如何发给虚拟机的虚拟机如何发送给外部的?
我已经收集了足够的代码证据。下面给出 substrate ateom-microvm 网络的完整工作原理,以及流量的两条通路(入向:外部→VM;出向:VM→外部)。
substrate ateom-microvm 网络工作原理
一、整体网络布局(单命名空间 + 用户态隧道)
每个 actor 有一个独立的网络命名空间(RuntimeNetNS == GatewayNetNS),里面住着:
┌─────────────── 沙箱网络命名空间 (ActorNetNSName(actorUID)) ───────────────┐
│ │
│ [guest eth0 169.254.17.2/30] │
│ │ virtio-net │
│ [host tap0_kata 169.254.17.1/30] ←────── 点对点,无 bridge/veth │
│ │ │
│ ├── atunnel Egress 监听 (EgressPort, 在命名空间内) │
│ ├── atunnel DNS relay (端口 53) │
│ └── nftables NAT prerouting (出站TCP → EgressPort) │
└──────────────────────────────────────────────────────────────────────────┘
│
(Worker Pod 的宿主机网络)
- 每个 actor 一个 netns(
netns.CreateNamed(ActorNetNSName(uid)))。 - guest 只有一块网卡 eth0(virtio-net),host 端是 tap(
tap0_kata),点对点,没有 bridge/veth。 - 地址固定 link-local
/30:guest169.254.17.2,gateway/tap169.254.17.1。MAC 也固定(actorGuestMAC/gatewayMAC),以便 restore 时匹配 guest 冻结的 ARP。 - atunnel 的 Egress 监听、DNS relay、nftables 重定向都在这个命名空间里,由
ateomnet.ServeSandbox搭好(serveSandboxEgress+dns.Relay.Serve+installEgressRedirect)。
二、入向流量:外部 → 虚拟机
入向是 HTTPS 反向代理 + 沙箱内拨号(不是内核转发)。
[外部/atenet router] --HTTPS(mTLS)--> [atunnel Ingress server, Worker Pod:443]
│
(tunnel.Activate 时注册 actor)
│ dial = sandboxDialer(actorUID)
│ = Session.Dialer
│ = netns.Dialer(GatewayNetNS)
▼
[进入沙箱命名空间] --> 拨号到 guest eth0 169.254.17.2:80
│ (ActorHTTPUpstream)
▼
[guest 内的应用]
具体代码链:
atunnel.Start启动 Ingress HTTPS 反向代理(internal/atunnel/ingress.go的Server),监听 worker 的:443/ CONNECT:8443。tunnel.Activate(ateomtunnel.go)调用Ingress.Activate(atespace, name, uid, dial),把这个 actor 注册进 Ingress 的activemap。dial是sandboxDialer(actorUID)(hosted.go)→Session.Dialer()(sandbox.go)→netns.Dialer(GatewayNetNS):通过命名空间 FD 进入该 actor 的沙箱命名空间,在那里拨号到169.254.17.2:80(ActorHTTPUpstream)。newActorProxy用这个 dial 建httputil.ReverseProxy,把外部进来的 HTTPS 请求反向代理进 guest。
所以入站流量不经过任何内核转发/bridge,而是由用户态 atunnel Ingress 接收、再用 netns 拨号"亲手"把连接插进沙箱命名空间、连到 guest eth0。
三、出向流量:虚拟机 → 外部
出向是 透明 TCP 拦截(nftables NAT)+ 用户态 mTLS 隧道:
[guest 内应用发起 TCP 出站]
│ 目的: 任意公网 IP:port
▼
[guest 内核路由] --默认路由经 gateway 169.254.17.1--> [host tap0_kata]
│
▼
[nftables NAT prerouting (沙箱命名空间内)]
│ 规则: 目的不是本 /30 子网的 TCP
│ → REDIRECT 到 EgressPort
▼
[atunnel Egress 监听 (EgressPort, 绑定在沙箱命名空间)]
│ 用 SO_ORIGINAL_DST 读出真实目标地址
│ 经 egress 隧道 (mTLS, CONNECT) 拨到远端 egress gateway
▼
[远端 egress gateway] --> 访问真实外部服务
具体代码链:
installEgressRedirect(sandbox.go)在沙箱命名空间装 nftables NAT prerouting 规则:匹配目的地址不属于本沙箱/30子网(169.254.17.0/30)的 TCP 包,REDIRECT到EgressPort(保留SO_ORIGINAL_DST)。serveSandboxEgress用netns.Listen(GatewayNetNS, EgressPort)在该命名空间内监听 EgressPort,并e.Bind(actorUID)绑定激活。Egress.serve(egress.go)收到被拦截的连接,用TCPOriginalDestination读SO_ORIGINAL_DST得到真实目标,然后通过 CONNECT mTLS 隧道(PrepareEgress签发的 actor 证书)把流量隧道到远端 egress gateway,由它访问真实外部服务。
注意:只有 TCP 被重定向;DNS(UDP/TCP 53)由命名空间内的 atunnel DNS relay 应答(
dns.Relay.Serve),不到达 egress。docs/egress-traffic.md明确:UDP 默认被丢弃、非隧道 TCP 被拒绝。
四、两条通路与 restore 的关系
- 入向依赖
sandboxDialer→Session.Dialer(GatewayNetNSFD)——每次 dial 实时查s.actors[actorUID],所以 restore 后用setRunningVM发布新 VM、tunnel.Activate重新注册后,新连接就自动拨到新恢复的 guest。 - 出向依赖命名空间里的 nftables 规则 + Egress 监听——这些在
hostActor的ServeSandbox里随新命名空间重建,tunnel.Activate重新Egress.Activate后恢复生效。
五、一句话总结
substrate ateom-microvm 把网络数据面整体上收到用户态 atunnel:
- 入站:外部 HTTPS → atunnel Ingress 反向代理 → 通过 netns 拨号进入沙箱 → guest eth0(无内核转发/bridge)。
- 出站:guest → tap → 被 nftables NAT prerouting 透明重定向到命名空间内的 atunnel Egress 监听 → 用
SO_ORIGINAL_DST还原目标 → 经 mTLS CONNECT 隧道到远端 egress gateway 访问外部。
内核只负责"把包摘到 atunnel"(一条 nftables 规则)和"点对点 tap",真正的转发、隧道、策略都在用户态 atunnel 进程里完成。
现在我已经完全理解了机制。让我确认 tap 的二层转发语义——tap 设备把宿主机命名空间和 guest 的 virtio-net 连通。这是"netns 拨号到 guest eth0"能通的根本。
我已经掌握了所有关键代码。让我组织回答。
"netns 拨号进入沙箱 → guest eth0" 之间是如何连通的
核心:tap 就是那条"桥"
关键要理解:ateom 进程的 Ingress 拨号并没有"直接"连到 guest eth0,而是连到了宿主机命名空间里的 tap0_kata,再由 tap 的二层语义把帧送到 guest 的 eth0。
tap 设备(cmd/ateom-microvm/net.go 的 setupActorTap 创建)是一个二层网络设备,它在宿主机命名空间里有一个 IP(169.254.17.1/30),但它的本质是一个"管道的宿主机端"——另一端直接接在 guest 的 virtio-net(eth0) 上。任何写进 tap 的二层帧,会原样出现在 guest 的 eth0 接收队列;guest 发出的帧会出现在 tap 的接收侧。
所以拓扑是点对点二层链路:
[guest eth0 169.254.17.2/30] ⇄virtio-net⇄ [host tap0_kata 169.254.17.1/30]
(同一对帧, 二层直达)
在这个共享命名空间里,169.254.17.2(guest)对宿主机内核而言,就是通过 tap 这条"虚拟网线"可达的邻居。
netns.Dialer 做了什么
netns.Dialer(ns)(internal/ateomnet/netns/dial.go):
with(ns, ...)(第 95-134 行)把 ateom 进程的一个 OS 线程切进沙箱命名空间(vishnetns.Set(targetNS),第 109 行)。- 在这个线程里,用
net.Dialer.DialContext(ctx, "tcp", "169.254.17.2:80")创建 socket。 ControlContext回调(第 49-55 行)在 socket 刚创建成功后就restore()——把线程切回 worker 原命名空间并解锁 OS 线程。也就是说,只有 socket 的创建需要命名空间,之后的 connect/读写线程已切回,socket 本身"记住"了它诞生时的命名空间。
拨号包如何抵达 guest eth0
socket 创建在沙箱命名空间里,所以这个 TCP 连接的路由决策和收发都发生在该命名空间的网络栈内:
- ateom Ingress 的 dial goroutine 调用
Dialer("169.254.17.2:80")。 - 命名空间内核的路由表里,
169.254.17.2落在连通子网169.254.17.0/30,出接口是tap0_kata(guest eth0 在 tap 对端)。所以内核通过 tap 这个出口设备发出到169.254.17.2的包。 - tap 把帧原样交给 guest 的 virtio-net → guest 内核 eth0 收包 → 送到 guest 内监听
:80的应用(即 actor 的 HTTP 服务)。 - guest 回包反向走:guest eth0 → virtio-net → tap → 命名空间内该 socket。
注意没有 bridge、没有额外 NAT——就是 tap 的点对点二层链路 + 命名空间内普通路由。这也正是为什么 Ingress 的 ActorHTTPUpstream = "http://169.254.17.2:80" 能通:169.254.17.2 在宿主机命名空间里就是 tap 对端 guest 的地址。
补充:Egress 方向(反向)为什么也能通
同一套 tap 链路,出向是反向的:
- guest 应用发 TCP 到外部 → guest 内核默认路由
169.254.17.1(gateway/tap 地址)→ 帧从 eth0 → tap → 沙箱命名空间。 - 命名空间里的 nftables NAT prerouting 规则(
installEgressRedirect)把这个包重定向到EgressPort。 EgressPort上的监听 socket 是netns.Listen(GatewayNetNS, ...)(netns.go第 133 行)创建的——和 Dialer 对称,socket 在命名空间内创建、保留命名空间,所以能收到被重定向过来的连接。Egress 用SO_ORIGINAL_DST读回真实目标,再经 mTLS 隧道发往远端网关。
一句话总结
Ingress 的
netns.Dialer并没有"进入虚拟机",而是把 ateom 的一个 socket 创建到宿主机沙箱命名空间里;命名空间里的tap0_kata与 guest 的 virtio-net 是点对点二层直达(一条虚拟网线),169.254.17.2就是 tap 对端 guest eth0 的地址,所以命名空间内普通路由 + tap 二层转发就把包送到了 guest eth0。 反向出向同理:guest → tap → 命名空间,再被 nftables 重定向到 atunnel Egress。

浙公网安备 33010602011771号