kata 创建网络相关代码

进入指定的 Linux 网络命名空间,扫描里面所有网卡,筛选合法网卡,把符合条件的网卡包装成 Endpoint(网络端点)添加到sandbox ;失败时支持回滚,避免只添加一半端点。

func (n *LinuxNetwork) scanEndpointsInNs(ctx context.Context, s *Sandbox, nsPath string, hotplug bool) ([]Endpoint, error) {
    netnsHandle, err := netns.GetFromPath(nsPath)
    if err != nil {
        return nil, err
    }
    defer netnsHandle.Close()

    netlinkHandle, err := netlink.NewHandleAt(netnsHandle)
    if err != nil {
        return nil, err
    }
    defer netlinkHandle.Close()

    linkList, err := netlinkHandle.LinkList()
    if err != nil {
        return nil, err
    }

    epsBefore := len(n.eps)
    var added []Endpoint

    for _, link := range linkList {
        netInfo, err := networkInfoFromLink(netlinkHandle, link)
        if err != nil {
            // No rollback needed — no endpoints were added in this iteration yet.
            return nil, err
        }

        // Ignore unconfigured network interfaces. These are
        // either base tunnel devices that are not namespaced
        // like gre0, gretap0, sit0, ipip0, tunl0 or incorrectly
        // setup interfaces.
        if len(netInfo.Addrs) == 0 {
            continue
        }

        // Skip any loopback interfaces:
        if (netInfo.Iface.Flags & net.FlagLoopback) != 0 {
            continue
        }

        // Skip any interfaces that are already added
        if n.endpointAlreadyAdded(&netInfo) {
            networkLogger().WithField("endpoint", netInfo.Iface.Name).Info("already added")
            continue
        }

        if err := doNetNS(nsPath, func(_ ns.NetNS) error {
            ep, addErr := n.addSingleEndpoint(ctx, s, netInfo, hotplug)
            if addErr == nil {
                added = append(added, ep)
            }
            return addErr
        }); err != nil {
            // Rollback: remove any endpoints added during this scan
            // so that a failed scan does not leave partial side effects.
            n.eps = n.eps[:epsBefore]
            return nil, err
        }
    }

    return added, nil
}

addSingleEndpoint() 方法除了 context 共传入了三个参数,分别是 sandbox 实例(s)、设备信息(netInfo)和是否支持热插拔(hotplug)。它做了两件事:(1)根据设备类型创建一个对应的 Endpoint;(2)将这个 Endpoint 绑定到 sandbox 中。

func (n *LinuxNetwork) addSingleEndpoint(ctx context.Context, s *Sandbox, netInfo NetworkInfo, hotplug bool) (Endpoint, error) {
    var endpoint Endpoint
    // TODO: This is the incoming interface
    // based on the incoming interface we should create
    // an appropriate EndPoint based on interface type
    // This should be a switch

    // Check if interface is a physical interface. Do not create
    // tap interface/bridge if it is.
    isPhysical, err := isPhysicalIface(netInfo.Iface.Name)
    if err != nil {
        return nil, err
    }

    if isPhysical {
        if s.config.HypervisorConfig.ColdPlugVFIO == config.NoPort {
            // When `cold_plug_vfio` is set to "no-port", the PhysicalEndpoint's VFIO device cannot be attached to the guest VM.
            // Fail early to prevent the VF interface from being unbound and rebound to the VFIO driver.
            return nil, fmt.Errorf("unable to add PhysicalEndpoint %s because cold_plug_vfio is disabled", netInfo.Iface.Name)
        }
        networkLogger().WithField("interface", netInfo.Iface.Name).Info("Physical network interface found")
        endpoint, err = createPhysicalEndpoint(netInfo)
    } else {
        var socketPath string
        idx := len(n.eps)

        // Avoid endpoint naming conflicts
        // When creating a new endpoint, we check existing endpoint names and automatically adjust the naming of the new endpoint to ensure uniqueness.
        lastIdx := -1
        if len(n.eps) > 0 {
            lastEndpoint := n.eps[len(n.eps)-1]
            re := regexp.MustCompile("[0-9]+")
            matchStr := re.FindString(lastEndpoint.Name())
            n, err := strconv.ParseInt(matchStr, 10, 64)
            if err != nil {
                return nil, err
            }
            lastIdx = int(n)
        }
        if idx <= lastIdx {
            idx = lastIdx + 1
        }
        // Check if this is a dummy interface which has a vhost-user socket associated with it
        socketPath, err = vhostUserSocketPath(netInfo)
        if err != nil {
            return nil, err
        }

        if socketPath != "" {
            networkLogger().WithField("interface", netInfo.Iface.Name).Info("VhostUser network interface found")
            endpoint, err = createVhostUserEndpoint(netInfo, socketPath)
        } else if netInfo.Iface.Type == "macvlan" {
            networkLogger().Infof("macvlan interface found")
            endpoint, err = createMacvlanNetworkEndpoint(idx, netInfo.Iface.Name, n.interworkingModel)
        } else if netInfo.Iface.Type == "macvtap" {
            networkLogger().Infof("macvtap interface found")
            endpoint, err = createMacvtapNetworkEndpoint(netInfo)
        } else if netInfo.Iface.Type == "tap" {
            networkLogger().Info("tap interface found")
            endpoint, err = createTapNetworkEndpoint(idx, netInfo.Iface.Name)
        } else if netInfo.Iface.Type == "tuntap" {
            if netInfo.Link != nil {
                switch netInfo.Link.(*netlink.Tuntap).Mode {
                case 0:
                    // mount /sys/class/net to get links
                    return nil, fmt.Errorf("Network device mode not determined correctly. Mount sysfs in caller")
                case 1:
                    return nil, fmt.Errorf("tun networking device not yet supported")
                case 2:
                    networkLogger().Info("tuntap tap interface found")
                    endpoint, err = createTuntapNetworkEndpoint(idx, netInfo.Iface.Name, netInfo.Iface.HardwareAddr, n.interworkingModel)
                default:
                    return nil, fmt.Errorf("tuntap network %v mode unsupported", netInfo.Link.(*netlink.Tuntap).Mode)
                }
            }
        } else if netInfo.Iface.Type == "veth" {
            networkLogger().Info("veth interface found")
            endpoint, err = createVethNetworkEndpoint(idx, netInfo.Iface.Name, n.interworkingModel)
        } else if netInfo.Iface.Type == "ipvlan" {
            networkLogger().Info("ipvlan interface found")
            endpoint, err = createIPVlanNetworkEndpoint(idx, netInfo.Iface.Name)
        } else if netInfo.Iface.Type == "device" {
            // "device" is the generic netlink type for interfaces whose
            // drivers do not register a more specific kind. This includes
            // mlx5 Scalable Functions (SFs), some SR-IOV VFs on arm64,
            // and other non-PCI backed netdevs that isPhysicalIface()
            // cannot classify via ethtool BusInfo. Handle them the same
            // way as veth endpoints, following the configured interworking
            // model (TC-filter by default).
            networkLogger().Info("device interface found")
            endpoint, err = createVethNetworkEndpoint(idx, netInfo.Iface.Name, n.interworkingModel)
        } else {
            return nil, fmt.Errorf("Unsupported network interface: %s", netInfo.Iface.Type)
        }
    }

    if err != nil {
        return nil, err
    }

    endpoint.SetProperties(netInfo)

    networkLogger().WithField("endpoint-type", endpoint.Type()).WithField("hotplug", hotplug).Info("Attaching endpoint")
    if hotplug {
        if err := endpoint.HotAttach(ctx, s); err != nil {
            return nil, err
        }
    } else {
        if err := endpoint.Attach(ctx, s); err != nil {
            return nil, err
        }
    }

    if !s.hypervisor.IsRateLimiterBuiltin() {
        rxRateLimiterMaxRate := s.hypervisor.HypervisorConfig().RxRateLimiterMaxRate
        if rxRateLimiterMaxRate > 0 {
            networkLogger().Info("Add Rx Rate Limiter")
            if err := addRxRateLimiter(endpoint, rxRateLimiterMaxRate); err != nil {
                return nil, err
            }
        }
        txRateLimiterMaxRate := s.hypervisor.HypervisorConfig().TxRateLimiterMaxRate
        if txRateLimiterMaxRate > 0 {
            networkLogger().Info("Add Tx Rate Limiter")
            if err := addTxRateLimiter(endpoint, txRateLimiterMaxRate); err != nil {
                return nil, err
            }
        }
    }

    n.eps = append(n.eps, endpoint)

    return endpoint, nil
}
func createVethNetworkEndpoint(idx int, ifName string, interworkingModel NetInterworkingModel) (*VethEndpoint, error) {
    if idx < 0 {
        return &VethEndpoint{}, fmt.Errorf("invalid network endpoint index: %d", idx)
    }

    netPair, err := createNetworkInterfacePair(idx, ifName, interworkingModel)
    if err != nil {
        return nil, err
    }

    endpoint := &VethEndpoint{
        // TODO This is too specific. We may need to create multiple
        // end point types here and then decide how to connect them
        // at the time of hypervisor attach and not here
        NetPair:      netPair,
        EndpointType: VethEndpointType,
    }
    if ifName != "" {
        endpoint.NetPair.VirtIface.Name = ifName
    }

    return endpoint, nil
}

现在我有了完整的上下文。让我给出详细分析。

分析 createVethNetworkEndpoint

函数作用

createVethNetworkEndpoint 是 Kata 网络模型中创建 veth 端点(VethEndpoint)的工厂函数。它把 CNI/网络插件在容器 netns 里创建好的那个 veth 接口(如 eth0)封装成一个 VethEndpoint,为后续把该接口连接到 VM 做准备。

它接收三个参数:

  • idx int — 端点索引,用于生成网络对(pair)里各种接口的名称(如 eth0、tap0_kata、br0_kata)
  • ifName string — 原始接口名(如 eth0),如果非空则覆盖默认命名
  • interworkingModel NetInterworkingModel — VM 与容器网络之间的连接模型(macvtap / tcfilter / none)

逐步逻辑

1. 校验索引

if idx < 0 {
    return &VethEndpoint{}, fmt.Errorf("invalid network endpoint index: %d", idx)
}

索引必须 ≥ 0,否则返回错误。这里的 idx 来自 addSingleEndpoint 里的 idx := len(n.eps)(已有端点数量),正常情况下都是非负的,这是防御性校验。

2. 创建网络对(NetworkInterfacePair)

netPair, err := createNetworkInterfacePair(idx, ifName, interworkingModel)
if err != nil {
    return nil, err
}

调用 createNetworkInterfacePair(network.go:351)生成一个 NetworkInterfacePair。让我们看看这个函数生成的内容:

  • 生成一个 UUID 作为网络对的唯一 ID
  • 生成一个随机的私有 MAC 地址(generateRandomPrivateMacAddr,把第一个字节的 local bit 置位,范围 x2/x6/xA/xE,即本地管理的单播 MAC)
  • 构造 NetworkInterfacePair:
    • TapInterface(宿主机侧):
      • Name = "br<idx>_kata"(桥接口名,如 br0_kata)
      • TAPIface.Name = "tap<idx>_kata"(tap 接口名,如 tap0_kata)
    • VirtIface(VM 侧 / 原始 veth):
      • Name = "eth<idx>"(如 eth0)
      • HardAddr = <随机 MAC>
    • NetInterworkingModel = interworkingModel

如果 ifName != "",则把 VirtIface.Name 覆盖为 ifName(即原始接口名)。

3. 构造 VethEndpoint

endpoint := &VethEndpoint{
    NetPair:      netPair,
    EndpointType: VethEndpointType,
}
if ifName != "" {
    endpoint.NetPair.VirtIface.Name = ifName
}

把 netPair 和端点类型 VethEndpointType 封装进 VethEndpoint。再次用 ifName 覆盖 VirtIface.Name(注意这里重复了一次 createNetworkInterfacePair 里已经做过的覆盖,属于冗余但无害)。

4. 返回端点

返回 *VethEndpoint,由调用方(addSingleEndpoint)继续做 SetProperties 和 Attach/HotAttach。

关于 NetworkInterfacePair 的理解

一个 veth 网络对包含两个关键侧:

宿主机侧 (TapInterface)                 VM侧 / 原始veth (VirtIface)
  br<idx>_kata  (桥)                    eth<idx> / ifName (原始接口)
  tap<idx>_kata (tap)
  • VirtIface 代表容器 netns 里那个真实接口(veth 的一端,通常是 eth0)。HardAddr 是随机生成的 MAC,后续在 tapNetworkPair/setupTCFiltering 中会把这个 MAC 设置到真实接口上,以保证 VM 内外的流量用同一个 MAC,避免防火墙/ARP 问题。
  • TAPIface 是之后连接到 VM 的 tap/macvtap 设备,命名带 _kata 后缀避免与宿主冲突。

调用链回顾(结合上一问)

addSingleEndpoint (Iface.Type == "veth")
    ↓
createVethNetworkEndpoint(idx, ifName=netInfo.Iface.Name, interworkingModel)
    ↓
createNetworkInterfacePair(...)   // 生成命名 + 随机MAC
    ↓
endpoint.SetProperties(netInfo)   // 记录IP/路由等
endpoint.Attach/HotAttach(ctx, s)
    ↓
xConnectVMNetwork → tapNetworkPair / setupTCFiltering  // 真正建tap、做MAC/MTU设置、挂到hypervisor

几点观察

  1. MAC 地址:在 createNetworkInterfacePair 里给 VirtIface 预分配了随机 MAC,但 setProperties 时 CNI 传来的 netInfo.Iface.HardwareAddr 会通过 NetworkInfo 保存到 EndpointProperties。真正生效的 MAC 是在 tapNetworkPair/setupTCFiltering 里把 netPair.VirtIface.HardAddr 设置到物理 veth 上时使用的。

  2. 命名规则依赖 idx:eth<idx>、tap<idx>_kata、br<idx>_kata 都基于 idx,这与 addSingleEndpoint 里为规避命名冲突而调整 idx 的逻辑是配套的——保证每个端点名字唯一。

  3. ifName 覆盖:createNetworkInterfacePair 和 createVethNetworkEndpoint 两处都对 VirtIface.Name 做了相同的 ifName 覆盖,属于冗余代码,但结果是确定性的:有 ifName(CNI 场景恒有,即 eth0)时用原始名,否则用 eth<idx>。

  4. TODO 注释:代码里标注了一个 TODO——"这里创建端点类型太具体,将来可能需要创建多种端点类型,并在 hypervisor attach 时才决定如何连接"。说明当前设计是"提前确定端点类型 + 连接模型",有一定耦合。

简而言之,这个函数的核心职责就是根据索引生成一套规范的接口命名(桥/tap/eth),并把这个 veth 端点初始化好,使其能够被后续的 attach 流程连接到 VM。

HotAttach 代码
Attach() 方法主要做了两件事情,一个是调用 xConnectVMNetwork() 方法让 veth 设备连接到 tap0_kata 设备(aka 虚拟机网络,如绿色链路所示),一个是调用 hypervisor 的 AddDevice() 方法,让启动 QEMU 虚拟机时增加一条 netdev 设置,让 tap 设备连接到虚拟机

// HotAttach for the veth endpoint uses hot plug device
func (endpoint *VethEndpoint) HotAttach(ctx context.Context, s *Sandbox) error {
	span, ctx := vethTrace(ctx, "HotAttach", endpoint)
	defer span.End()

	h := s.hypervisor
	if err := xConnectVMNetwork(ctx, endpoint, h); err != nil {
		networkLogger().WithError(err).Error("Error bridging virtual ep")
		return err
	}

	if _, err := h.HotplugAddDevice(ctx, endpoint, NetDev); err != nil {
		networkLogger().WithError(err).Error("Error attach virtual ep")
		return err
	}
	return nil
}

xConnectVMNetwork 逻辑

func xConnectVMNetwork(ctx context.Context, endpoint Endpoint, h Hypervisor) error {
	var err error

	span, ctx := networkTrace(ctx, "xConnectVMNetwork", endpoint)
	defer closeSpan(span, err)

	netPair := endpoint.NetworkPair()

	queues := 0
	caps := h.Capabilities(ctx)
	if caps.IsMultiQueueSupported() {
		queues = int(h.HypervisorConfig().NumVCPUs())
	}

	disableVhostNet := h.HypervisorConfig().DisableVhostNet

	if netPair.NetInterworkingModel == NetXConnectDefaultModel {
		netPair.NetInterworkingModel = DefaultNetInterworkingModel
	}

	switch netPair.NetInterworkingModel {
	case NetXConnectMacVtapModel:
		networkLogger().Info("connect macvtap to VM network")
		err = tapNetworkPair(ctx, endpoint, queues, disableVhostNet)
	case NetXConnectTCFilterModel:
		networkLogger().Info("connect TCFilter to VM network")
		err = setupTCFiltering(ctx, endpoint, queues, disableVhostNet)
	default:
		err = fmt.Errorf("Invalid internetworking model")
	}
	return err
}

现在我有了足够的上下文来全面分析这段代码。让我也看一下 veth_endpoint.go 的 Attach/HotAttach 调用链,以便完整说明 xConnectVMNetwork 在整个流程中的位置。

我已经有足够的上下文了。让我提供完整的分析。

xConnectVMNetwork 代码逻辑分析

函数定位

xConnectVMNetwork 是虚拟网络端点(veth、macvlan、ipvlan、tuntap 等)在宿主侧建立虚拟机网络连接的核心函数。它在各个端点的 Attach/HotAttach 方法中被调用(如 veth_endpoint.go:114、macvlan_endpoint.go:111、ipvlan_endpoint.go:114、tuntap_endpoint.go:93),负责在 VM 真正启动/热插拔前,把宿主侧的 tap 设备、文件描述符(fd)和 tc 过滤规则准备好。

逐段逻辑

1. 函数签名与追踪

func xConnectVMNetwork(ctx context.Context, endpoint Endpoint, h Hypervisor) error {
  • 输入是 Endpoint(要连接的虚拟网络端点)和 Hypervisor(用来查询能力/配置)。
  • 通过 networkTrace 创建 OpenTelemetry 追踪 span,用 closeSpan 在返回时记录错误。

2. 获取网络对(netPair)与多队列配置

netPair := endpoint.NetworkPair()
queues := 0
caps := h.Capabilities(ctx)
if caps.IsMultiQueueSupported() {
    queues = int(h.HypervisorConfig().NumVCPUs())
}
disableVhostNet := h.HypervisorConfig().DisableVhostNet
  • NetworkPair() 返回一个 NetworkInterfacePair,包含 TAPIface(宿主侧 tap)、VirtIface(VM 内虚拟网卡)、VMFds(tap 的 fd)、VhostFds(vhost-net 的 fd)等。
  • 若 hypervisor 支持多队列(IsMultiQueueSupported()),则将队列数设为 vCPU 数(即每个 vCPU 一个队列);否则为 0(后续会被强制为 1,见 createLink 中的注释)。
  • disableVhostNet 决定是否创建 /dev/vhost-net 的 fd(vhost 加速)。

3. 解析默认互连模型

if netPair.NetInterworkingModel == NetXConnectDefaultModel {
    netPair.NetInterworkingModel = DefaultNetInterworkingModel
}
  • 若端点未显式指定互连模型(NetXConnectDefaultModel),则回退到全局默认值 DefaultNetInterworkingModel(在 network.go:183 定义为 NetXConnectTCFilterModel)。

4. 按互连模型分派

switch netPair.NetInterworkingModel {
case NetXConnectMacVtapModel:
    err = tapNetworkPair(ctx, endpoint, queues, disableVhostNet)
case NetXConnectTCFilterModel:
    err = setupTCFiltering(ctx, endpoint, queues, disableVhostNet)
default:
    err = fmt.Errorf("Invalid internetworking model")
}

两种模型的核心区别:

模型 A:NetXConnectMacVtapModel → tapNetworkPair(macvtap 桥接)
  • 用 createMacVtap 在容器接口上创建 macvtap 设备(因为内核在 netns 里创建 macvtap 时链接索引有问题,所以通过 hostLinkOffset 偏移 + 随机索引 + 重试 128 次来规避——见 macvtapWorkaround)。
  • 把 veth 的 MAC 地址保存到 TAP(netPair.TAPIface.HardAddr),这样 VM 内网卡用的 MAC 与宿主侧一致,避免防火墙/bridge 的 MAC 问题。
  • 将 veth 的 MAC 设为 VirtIface.HardAddr,TAP 的 MAC 设为原 veth MAC(两者互换)。
  • 清空 veth 上的 IP 地址(clearIPs)以防止 ARP 冲突——IP 由 VM 内部负责配置。
  • 打开 macvtap 的 fd(createMacvtapFds)→ VMFds;若未禁用 vhost,再打开 /dev/vhost-net 的 fd → VhostFds。
  • 这是"直接桥接"方案:流量在数据链路层直接桥接,性能较好,但要求底层接口能被 macvtap 桥接。
模型 B:NetXConnectTCFilterModel → setupTCFiltering(tc 流量过滤,默认)
  • 用 createLink 创建 tuntap 设备(TAPIface),拿到 tap 的 fd → VMFds;同样可选创建 vhost fd。
  • 同样保存 veth MAC 到 TAP。
  • 设置 MTU、启用 TAP。
  • 然后建立 双向 tc 重定向:
    • 在 tap 和 veth 上都添加 ingress qdisc(addQdiscIngress,等价于 tc qdisc add dev X ingress)。
    • 用 addRedirectTCFilter 把 veth 的入站流量重定向到 tap(attrs.Index → tapAttrs.Index),再把 tap 的入站流量重定向到 veth(tapAttrs.Index → attrs.Index),形成双向转发。等价于 tc filter add ... action mirred egress redirect。
  • 这是"流量重定向"方案:通过 tc 在两层接口间互相转发,兼容性更好(ipvlan、macvlan 也能用),但有一点内核开销。这也是默认方案。

与 checkpoint/restore 的关联(结合上下文)

这段代码与你之前分析的 HotAttach 链直接相关:

  1. xConnectVMNetwork 只是宿主侧准备阶段——它创建 tap、vhost fd、tc 规则。
  2. 这些准备好的 VMFds/VhostFds 随后被 hypervisor 使用:
    • Cloud-Hypervisor:在 clh.go:2170 用 netPair.VMFds 通过 REST API 调用 vm.add-net 添加网络设备。
    • QEMU:在 qemu.go:2574 的 hotAddNetDevice 中通过 QMP ExecuteGetFD 传递 fd,再 netdev_add+device_add。

在 checkpoint/restore 场景下,这些 fd 和网络设备属于 VM 的运行状态。CLH 的 checkpoint/restore 会保存 VM 的内存和状态;恢复时 start_for_restore 重新启动 VM 并重新连接 agent,此时网络端点需要通过上述 Attach/HotAttach 链重新建立(重新走 xConnectVMNetwork 准备宿主侧资源,再通过 vm.add-net 重新接入)。这也就是之前会话中提到的"restore 时网络需要重新 hotplug"的原因。

小结

项 说明
作用 在宿主侧为 VM 网络端点建立连接(tap + fd + tc 规则)
分派依据 NetworkPair().NetInterworkingModel
默认模型 NetXConnectTCFilterModel(tc 重定向)
macvtap 模型 直接桥接 + 交换 MAC + 清空 IP
tc 模型 tuntap + ingress qdisc + 双向 mirred 重定向
多队列 受 IsMultiQueueSupported 控制,队列数 = vCPU 数
vhost 受 DisableVhostNet 控制,默认开启 /dev/vhost-net fd
产出 填充 netPair.VMFds / VhostFds,供 hypervisor 消费
反操作 xDisconnectVMNetwork → untapNetworkPair / removeTCFiltering

需要我进一步深入某个子函数(如 createLink、createMacVtap 的索引规避逻辑,或 tc 双向重定向的流量路径),或者继续推进 checkpoint/restore 相关的验证工作吗?

posted @ 2026-10-22 10:27  rincloud  阅读(3)  评论(0)    收藏  举报