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 创建流程
- 打开字符设备:
fd = open("/dev/net/tun", O_RDWR); - 填充 ifreq 结构: 设置
ifr_name(可留空由内核自动命名)和ifr_flags。 - 调用 ioctl:
ioctl(fd, TUNSETIFF, &ifr); - 内核处理: 驱动检查名称是否冲突、分配 net_device 结构、注册网络接口、调用
tun_net_init。 - 返回: 成功后
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- 作为自定义前缀。
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 中处理完毕。
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 |
设备销毁回调,断开文件绑定、清理队列、唤醒等待进程 |
| 生命周期管理 | 创建→运行→销毁,通过文件描述符引用计数控制设备存活 |