4. TUN/TAP 设备创建与销毁:ioctl 接口、命名规则与内核回调

本章深入 TUN/TAP 设备生命周期的核心环节——用户态通过 ioctl 系统调用与内核驱动交互,完成虚拟网络接口的创建、命名与销毁。我们将重点剖析 TUNSETIFF 命令的工作流程、设备命名规则,以及内核中 tun_net_init 与 tun_net_uninit 两个关键回调函数的实现机制。

核心要点: TUN/TAP 设备的创建并非通过 ifconfig 或 ip link,而是由用户态程序打开 /dev/net/tun 字符设备后,通过 ioctl(TUNSETIFF) 触发内核动态创建对应的虚拟网络接口。销毁则通过关闭文件描述符或显式 ioctl(TUNSETIFF 变体实现。

4.1 ioctl 接口:TUNSETIFF 命令详解

ioctl 是用户态与 TUN/TAP 驱动通信的主要通道。TUNSETIFF 命令负责将字符设备文件描述符与一个虚拟网络接口绑定,并在必要时创建新的接口。

4.1.1 接口定义与数据结构

用户态程序需包含 <linux/if_tun.h> 头文件,使用 struct ifreq 结构体传递参数:

#include <linux/if.h>
#include <linux/if_tun.h>

int ioctl(fd, TUNSETIFF, (void *)&ifr);

struct ifreq {
    char ifr_name[IFNAMSIZ]; /* 接口名称,如 "tun0" 或 "tap0" */
    int  ifr_flags;          /* 标志位:IFF_TUN | IFF_TAP | IFF_NO_PI 等 */
};
标志位 值 说明
IFF_TUN 0x0001 创建 TUN 设备(三层隧道,处理 IP 数据包)
IFF_TAP 0x0002 创建 TAP 设备(二层隧道,处理以太网帧)
IFF_NO_PI 0x1000 不包含额外的包信息头(默认包含 4 字节标志头)
IFF_ONE_QUEUE 0x2000 使用单队列模式(较老内核选项)
IFF_MULTI_QUEUE 0x0100 启用多队列,支持多核并行处理

4.1.2 创建流程

  1. 打开字符设备: fd = open("/dev/net/tun", O_RDWR);
  2. 填充 ifreq 结构: 设置 ifr_name(可留空由内核自动命名)和 ifr_flags。
  3. 调用 ioctl: ioctl(fd, TUNSETIFF, &ifr);
  4. 内核处理: 驱动检查名称是否冲突、分配 net_device 结构、注册网络接口、调用 tun_net_init。
  5. 返回: 成功后 ifr.ifr_name 被填充为实际接口名,用户态可通过该 fd 读写数据包。
编程提示: 若 ifr_name 为空字符串,内核会自动分配 "tunX" 或 "tapX"(X 为递增数字)。若指定名称(如 "mytun"),内核会尝试创建该名称的接口,若已存在则返回 EEXIST 错误。

4.2 设备命名规则

TUN/TAP 设备的命名遵循内核网络设备命名规范,同时受驱动内部逻辑约束:

  • 默认前缀: TUN 设备默认以 tun 开头,TAP 设备以 tap 开头。
  • 自动编号: 若用户未指定名称,内核从 0 开始递增分配,如 tun0、tun1……
  • 自定义名称: 用户可指定任意不超过 IFNAMSIZ(16 字节)的名称,但需避免与已有接口冲突。
  • 持久化与非持久化: 默认创建的设备随文件描述符关闭而销毁。通过 ioctl(TUNSETPERSIST) 可设置为持久化设备,即使 fd 关闭,接口仍保留在系统中。
避坑指南: 自定义名称不要使用 eth、lo、wlan 等系统保留前缀,否则可能导致网络管理工具(如 NetworkManager)行为异常。建议使用 tun- 或 tap- 作为自定义前缀。
TUNSETIFF 设备创建流程 用户态程序 open("/dev/net/tun") 填充 ifreq 结构 ioctl(TUNSETIFF) TUN/TAP 内核驱动 检查名称合法性 分配 net_device 结构 调用 tun_net_init() 注册网络接口 返回 ifr_name 说明:用户态通过 ioctl 触发内核创建虚拟网络接口,成功后可通过同一 fd 读写数据包

4.3 tun_net_init:网络设备初始化回调

当内核通过 register_netdevice 注册新的虚拟网络接口时,会调用驱动注册的 net_device_ops 中的 ndo_init 回调。对于 TUN/TAP 驱动,该回调指向 tun_net_init 函数。

4.3.1 函数原型与核心逻辑

static int tun_net_init(struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);
    struct tun_file *tfile;

    /* 1. 初始化 tun_struct 中的关键字段 */
    tun->flags = 0;
    tun->filter_attached = false;
    INIT_LIST_HEAD(&tun->disabled);
    
    /* 2. 设置设备类型(ARP 硬件类型) */
    if (tun->flags & IFF_TAP) {
        dev->type = ARPHRD_ETHER;      /* 以太网类型 */
        dev->hard_header_len = ETH_HLEN; /* 以太网头部长度 */
    } else {
        dev->type = ARPHRD_NONE;       /* 无链路层 */
        dev->hard_header_len = 0;
    }
    
    /* 3. 设置 MTU 和特性 */
    dev->mtu = 1500;
    dev->features |= NETIF_F_LLTX;     /* 驱动自身管理锁 */
    
    /* 4. 关联第一个 tfile(由 ioctl 传入的 fd 对应) */
    tfile = rtnl_dereference(tun->tfiles[0]);
    if (tfile)
        tfile->tun = tun;
    
    return 0;
}
初始化项 说明
dev->type 设置 ARP 硬件类型,TUN 为 ARPHRD_NONE,TAP 为 ARPHRD_ETHER
dev->hard_header_len TUN 为 0(无 L2 头),TAP 为 14 字节(以太网头)
dev->mtu 默认 1500,用户可通过 ifconfig 修改
dev->features 设置 NETIF_F_LLTX 表示驱动自行处理传输锁
tun->tfiles 初始化文件指针数组,关联用户态 fd
关键设计: tun_net_init 并不分配内存(内存已在 tun_set_iff 中通过 alloc_netdev_mqs 分配),它只负责初始化设备特定的运行时状态。这种分离设计使得设备创建流程清晰且可扩展。

4.4 tun_net_uninit:网络设备销毁回调

当虚拟网络接口被删除(如用户关闭 fd 或显式删除设备)时,内核调用 ndo_uninit 回调,即 tun_net_uninit。该函数负责清理资源,确保无残留引用。

4.4.1 函数实现与资源回收

static void tun_net_uninit(struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);
    struct tun_file *tfile;
    int i;

    /* 1. 断开所有关联的 tfile 与 tun 的绑定 */
    for (i = 0; i < tun->numqueues; i++) {
        tfile = rtnl_dereference(tun->tfiles[i]);
        if (tfile) {
            tfile->tun = NULL;
            /* 唤醒可能阻塞在 read/write 上的进程 */
            wake_up_interruptible(&tfile->wq.wait);
        }
    }
    
    /* 2. 清理过滤器列表 */
    tun_flow_cleanup(tun);
    
    /* 3. 释放多队列相关资源 */
    if (tun->flags & IFF_MULTI_QUEUE) {
        /* 释放每个队列的 skb 缓存 */
        for (i = 0; i < MAX_TAP_QUEUES; i++)
            __skb_queue_purge(&tun->tx_queue[i]);
    }
    
    /* 4. 标记设备为已关闭 */
    tun->flags |= IFF_DETACH_QUEUE;
}

4.4.2 销毁触发场景

  • 文件描述符关闭: 用户态程序调用 close(fd) 或进程退出,驱动检测到 tun_file 引用计数归零。
  • 显式删除: 通过 ioctl(TUNSETIFF) 传入 IFF_DETACH_QUEUE 标志,或使用 ip link delete tun0。
  • 模块卸载: rmmod tun 时,驱动遍历所有活跃设备并逐一销毁。
内核编程注意: tun_net_uninit 在 RTNL 锁保护下执行,不可在此函数中调用可能引起睡眠的复杂操作(如大块内存分配)。所有可能阻塞的操作应在 tun_chr_close 中处理完毕。
TUN/TAP 设备生命周期状态机 未创建 设备不存在 运行中 可读写数据包 销毁中 资源回收 TUNSETIFF close(fd) tun_net_uninit 完成 关键回调:tun_net_init() 在创建时调用,tun_net_uninit() 在销毁时调用 TUNSETPERSIST

4.5 完整创建与销毁示例代码

以下代码展示了用户态创建 TUN 设备、读写数据包并最终销毁的完整流程:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/if.h>
#include <linux/if_tun.h>

int tun_alloc(char *dev, int flags) {
    struct ifreq ifr;
    int fd, err;

    /* 打开 TUN/TAP 字符设备 */
    if ((fd = open("/dev/net/tun", O_RDWR)) < 0) {
        perror("open /dev/net/tun");
        return -1;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = flags;

    /* 若 dev 非空,则指定设备名;否则由内核自动分配 */
    if (*dev)
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);

    /* 调用 ioctl 创建设备 */
    if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
        perror("ioctl TUNSETIFF");
        close(fd);
        return -1;
    }

    /* 内核返回实际设备名 */
    strcpy(dev, ifr.ifr_name);
    return fd;
}

int main() {
    char tun_name[IFNAMSIZ] = "tun_demo";
    int tun_fd;
    char buf[2048];

    /* 创建 TUN 设备 */
    tun_fd = tun_alloc(tun_name, IFF_TUN | IFF_NO_PI);
    if (tun_fd < 0) {
        fprintf(stderr, "Failed to create TUN device\n");
        return 1;
    }
    printf("TUN device %s created, fd=%d\n", tun_name, tun_fd);

    /* 读取数据包(阻塞模式) */
    int nread = read(tun_fd, buf, sizeof(buf));
    if (nread > 0) {
        printf("Read %d bytes from TUN device\n", nread);
    }

    /* 关闭 fd 触发设备销毁 */
    close(tun_fd);
    printf("TUN device %s destroyed\n", tun_name);
    return 0;
}
编译与测试: 使用 gcc -o tun_demo tun_demo.c 编译,需以 root 权限运行。创建成功后可通过 ip addr show tun_demo 查看设备状态。关闭程序后设备自动消失(除非设置了持久化)。

4.6 本章小结

知识点 关键内容
TUNSETIFF ioctl 用户态创建虚拟网络接口的核心命令,通过 struct ifreq 传递名称和类型标志
设备命名规则 默认 tunX/tapX,支持自定义名称(不超过 16 字节),持久化设备可跨进程存活
tun_net_init 设备初始化回调,设置类型、MTU、特性,关联文件指针
tun_net_uninit 设备销毁回调,断开文件绑定、清理队列、唤醒等待进程
生命周期管理 创建→运行→销毁,通过文件描述符引用计数控制设备存活