创建容器时候如何 用/dev/net/tun实例化为一个具体的 TAP 设备

完美。这就是从 /dev/net/tun 实例化 TAP 设备的核心内核交互代码。现在我有完整的链路了。让我把整个流程讲清楚。

从 /dev/net/tun 实例化出具体 TAP 设备 —— 完整链路

你问的是"创建容器时如何用 /dev/net/tun 实例化一个具体的 TAP 设备"。这其实是两层的协作:Kata 业务代码(createLink)决定 TAP 的参数,netlink 库(linkModify)执行内核 ioctl 真正实例化。

第 1 步:Kata 组装 TAP 配置(createLink,network_linux.go:729-786)

Kata 在创建网络端点时(setupTCFiltering → createLink),构造一个 netlink.Tuntap 结构体描述想要的 TAP 设备:

// network_linux.go:734-754
case (&netlink.Tuntap{}).Type():
    flags := netlink.TUNTAP_VNET_HDR | netlink.TUNTAP_NO_PI   // 带 vnet header、无 packet info
    if queues > 0 {
        flags |= netlink.TUNTAP_MULTI_QUEUE_DEFAULTS          // 多队列
    } else {
        queues = 1                                            // 至少 1 个队列
    }
    newLink = &netlink.Tuntap{
        LinkAttrs: netlink.LinkAttrs{Name: name},             // 设备名(如 tapXXX)
        Mode:      netlink.TUNTAP_MODE_TAP,                   // = IFF_TAP (2),类型是 TAP
        Queues:    queues,                                    // 队列数
        Flags:     flags,
    }

然后调用 netHandle.LinkAdd(newLink)(network_linux.go:775)——这一步触发内核实例化。

LinkAdd 最终调用 linkModify。这里就是真正"把 /dev/net/tun 实例化成 TAP"的关键:

// link_linux.go:1430-1447 核心片段
req.Flags |= uint16(tuntap.Mode)            // req.Flags |= IFF_TAP,加上 IFF_NO_PI | IFF_VNET_HDR
const TUN = "/dev/net/tun"
for i := 0; i < queues; i++ {               // 每个队列一个 fd
    fd, err := unix.Open(TUN, os.O_RDWR|syscall.O_CLOEXEC, 0)   // ① 打开 /dev/net/tun
    if err != nil { ... }

    // ② 关键 ioctl:把 (ifreq{name, flags}) 发给内核
    _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.TUNSETIFF),
                                uintptr(unsafe.Pointer(&localReq)))
    if errno != 0 { ... }

    // ③ 可选:设置 owner/group 权限
    // ④ 设为非阻塞
    unix.SetNonblock(fd, true)
    ...
    fds = append(fds, os.NewFile(uintptr(fd), TUN))   // 把 fd 包装成 *os.File
}

内核侧发生了什么(TUNSETIFF 的作用)

ioctl(fd, TUNSETIFF, &ifreq) 是 Linux tun 驱动提供的关键操作。/dev/net/tun 本身只是一个字符设备,打开它得到的 fd 还"不知道"自己要变成什么。TUNSETIFF 通过 struct ifreq 告诉内核:

struct ifreq {
    char ifr_name[IFNAMSIZ];   // 想要创建的接口名(如 "tap0")
    short ifr_flags;           // IFF_TAP | IFF_NO_PI | IFF_VNET_HDR | IFF_MULTI_QUEUE
};

内核 tun_chr_ioctl → tun_set_iff() 会:

  1. 检查 CAP_NET_ADMIN 权限
  2. 用 ifr_name 和 IFF_TAP 在当前网络命名空间里查找或创建一个 TAP 网络接口(tun_net_xmit 挂接)
  3. 把这个 fd 绑定到该 TAP 接口 —— 从此 fd 就是该 TAP 设备的句柄

所以一个 /dev/net/tun fd + 一次 TUNSETIFF ioctl = 一个具体的 TAP 接口。同一个 /dev/net/tun 可以多次打开、每次用不同 name/flags 实例化出不同的 TAP。

第 3 步:拿到 fd 后传回给 VM(衔接前面的话题)

linkModify 返回 fds(每个队列一个 *os.File):

  • 在 createLink(network_linux.go:779-782)里取出 tuntapLink.Fds
  • 在 setupTCFiltering(network_linux.go:1100)里 netPair.VMFds = fds
  • 最后通过 addNet → clh.netDevicesFiles[mac] = netPair.VMFds(clh.go:2173)→ vmAddNetPutRequest 用 syscall.UnixRights(fds...)(clh.go:233)把 tap fd 传给 CLH

为什么这里用 TC-filtering 模型而非 vhost

setupTCFiltering(network_linux.go:1084)是 Kata 默认的网络互操作模型之一。它直接创建 TAP 并把 fd 给 VM(CLH 侧做 virtio-net),而不启用 vhost-net(disableVhostNet 时跳过 createVhostFds,network_linux.go:1102-1108)。

完整调用链总结

创建容器 → createNetwork → AddEndpoints → VethEndpoint.Attach
  → AddDevice(NetDev) → clh.addNet
  → setupTCFiltering                       (network_linux.go:1084)
      → createLink(&netlink.Tuntap{...})   (network_linux.go:729)
          → netHandle.LinkAdd(newLink)     (network_linux.go:775)
              → linkModify (netlink 库)
                  → unix.Open("/dev/net/tun")      ← 打开字符设备
                  → ioctl(TUNSETIFF, ifreq)        ← 实例化为具体 TAP 接口(关键)
                  → 返回 fds (每个队列一个 tap fd)
      → netPair.VMFds = fds                (network_linux.go:1100)
  → clh.netDevicesFiles[mac] = VMFds       (clh.go:2173)
  → vmAddNetPutRequest:
      oob = syscall.UnixRights(fds...)     (clh.go:233)  ← fd 传给 CLH

一句话总结:Kata 通过 netlink.Tuntap 配置(名字、IFF_TAP 类型、队列、IFF_NO_PI|IFF_VNET_HDR 标志),调用 LinkAdd;netlink 库据此 open("/dev/net/tun") 后对每个队列执行一次 ioctl(TUNSETIFF, ifreq),内核据此在当前 netns 中创建并绑定一个具体的 TAP 接口,返回的 fd 就是该 TAP 的句柄,随后经 SCM_RIGHTS 传给 CLH 供 virtio-net 使用。

posted @ 2026-10-19 12:23  rincloud  阅读(9)  评论(0)    收藏  举报