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 顶层:

  1. s.tunnel.Deactivate(...) — 停用旧的 tunnel(旧的网络接入层)
  2. s.endPreviousActivation(...) — 结束之前的激活(如果有旧 VM 还在跑,停掉它)
  3. 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、IP 169.254.17.2/30。
    • atunnel 的 egress/DNS sockets,以及把出站 TCP 重定向到 atunnel 的 nftables 规则(installEgressRedirect)。

固定 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)做了两件事:

  1. admitActor:如果 actor 已 hosted(有旧网络),取出 stale = old.network。
  2. 如果 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 行起):

  1. tunnel.PrepareEgress(第 222 行):为这次 restore 签发新的 actor 证书、建立 egress gateway client(ActorEgress)。这是"新的出向能力"。

  2. cleanupSandboxState(第 226 行):kill 残留的 cgroup 进程 + kata.CleanupSandboxState(清理旧的 host 侧状态)。

  3. 读取快照的 net 设备 ch.SnapshotNetDevices(restoreDir)(第 336 行):解析快照 config.json 里的每个 virtio-net 设备(id、queue pairs、MAC)。

  4. 对每个 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 → 设置固定 MAC gatewayMAC(保证和 guest 冻结的 ARP 条目匹配)→ 赋 HostVethAddr → 设 up。返回 tap 的 FD。
    • 关键:快照的 virtio-net 是 fd-backed,所以 CH restore 需要全新的 tap FD(net_fds)。
  5. 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 用 GatewayNetNS FD 进入命名空间拨号)。
  • 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 侧网络状态在整个生命周期中的来源:

  1. 冷启动(cold boot / RunWorkload):configureGuestNetwork 通过 kata-agent 在 guest 内配置网络——UpdateInterface(设置 eth0 的 MAC actorGuestMAC、IP 169.254.17.2/30、MTU)、UpdateRoutes(连通路由 + 默认路由到 gateway 169.254.17.1)、AddARPNeighbors(把 gateway 的 ARP 条目钉死为 gatewayMAC,状态 NUD_PERMANENT)。

  2. 快照(CheckpointWorkload):guest 整块内存(含上述所有网络栈状态)被冻结到快照里。

  3. 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,造成黑洞。

所以"旧的网络"在 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、MTU
  • UpdateRoutes:连通路由 + 默认路由到 gateway 169.254.17.1
  • AddARPNeighbors:把 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,复用固定 MAC gatewayMAC
  • RestoreWithNetFDs 把新 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 对比。让我基于已经收集到的代码事实来组织回答。

我已经掌握的关键事实:

  1. 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 一个独立命名空间
  2. 原生 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 转发方案(用 clsact qdisc + 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

数据面差异非常明显:

  1. 转发发生在用户态 atunnel,而不是内核路由/NAT。关键在 installEgressRedirect(internal/ateomnet/sandbox.go):用 nftables NAT prerouting 把沙箱内 actor 的出站 TCP 全部重定向到 atunnel 的 egress 端口,同时保留 SO_ORIGINAL_DST。atunnel 这个用户态进程接管流量,做 mTLS 隧道到远端 egress gateway。也就是说,网络策略/隧道的决策点从"内核 netfilter"上移到了"用户态代理"。

  2. Ingress 也是用户态代理拨号:tunnel.Activate 后,入向流量由 atunnel 的 ingress server 接收,再通过 sandboxDialer(Session.Dialer,用 netns FD 进入沙箱命名空间拨号)拨进 guest 的 eth0。这是反向代理/拨号模型,不是内核转发。

  3. 单命名空间、无 bridge/veth 二层桥:microVM 用一个 tap(tap0_kata)直接挂在 guest virtio-net 上,GatewayNetNS == RuntimeNetNS(SetupSandboxNetwork 里 atunnelNS := actorNS),没有 CNI bridge、没有 veth pair。网络是点对点:guest 只有 eth0 ↔ tap,没有二层桥。

  4. 固定 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:guest 169.254.17.2,gateway/tap 169.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 内的应用]

具体代码链:

  1. atunnel.Start 启动 Ingress HTTPS 反向代理(internal/atunnel/ingress.go 的 Server),监听 worker 的 :443 / CONNECT :8443。
  2. tunnel.Activate(ateomtunnel.go)调用 Ingress.Activate(atespace, name, uid, dial),把这个 actor 注册进 Ingress 的 active map。
  3. dial 是 sandboxDialer(actorUID)(hosted.go)→ Session.Dialer()(sandbox.go)→ netns.Dialer(GatewayNetNS):通过命名空间 FD 进入该 actor 的沙箱命名空间,在那里拨号到 169.254.17.2:80(ActorHTTPUpstream)。
  4. 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] --> 访问真实外部服务

具体代码链:

  1. installEgressRedirect(sandbox.go)在沙箱命名空间装 nftables NAT prerouting 规则:匹配目的地址不属于本沙箱 /30 子网(169.254.17.0/30)的 TCP 包,REDIRECT 到 EgressPort(保留 SO_ORIGINAL_DST)。
  2. serveSandboxEgress 用 netns.Listen(GatewayNetNS, EgressPort) 在该命名空间内监听 EgressPort,并 e.Bind(actorUID) 绑定激活。
  3. 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(GatewayNetNS FD)——每次 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):

  1. with(ns, ...)(第 95-134 行)把 ateom 进程的一个 OS 线程切进沙箱命名空间(vishnetns.Set(targetNS),第 109 行)。
  2. 在这个线程里,用 net.Dialer.DialContext(ctx, "tcp", "169.254.17.2:80") 创建 socket。
  3. ControlContext 回调(第 49-55 行)在 socket 刚创建成功后就 restore()——把线程切回 worker 原命名空间并解锁 OS 线程。也就是说,只有 socket 的创建需要命名空间,之后的 connect/读写线程已切回,socket 本身"记住"了它诞生时的命名空间。

拨号包如何抵达 guest eth0

socket 创建在沙箱命名空间里,所以这个 TCP 连接的路由决策和收发都发生在该命名空间的网络栈内:

  1. ateom Ingress 的 dial goroutine 调用 Dialer("169.254.17.2:80")。
  2. 命名空间内核的路由表里,169.254.17.2 落在连通子网 169.254.17.0/30,出接口是 tap0_kata(guest eth0 在 tap 对端)。所以内核通过 tap 这个出口设备发出到 169.254.17.2 的包。
  3. tap 把帧原样交给 guest 的 virtio-net → guest 内核 eth0 收包 → 送到 guest 内监听 :80 的应用(即 actor 的 HTTP 服务)。
  4. 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。

posted @ 2026-10-22 12:15  rincloud  阅读(6)  评论(0)    收藏  举报