什么是tun/tap?
TUN/TAP虚拟网络设备为用户空间程序提供了网络数据包的发送和接收能力。他既可以当做点对点设备(TUN),也可以当做以太网设备(TAP)。
TUN/TAP虚拟网络设备的原理比较简单,他在Linux内核中添加了一个TUN/TAP虚拟网络设备的驱动程序和一个与之相关连的字符设备/dev/net/tun,字符设备tun作为用户空间和内核空间交换数据的接口。当内核将数据包发送到虚拟网络设备时,数据包被保存在设备相关的一个队列中,直到用户空间程序通过打开的字符设备tun的描述符读取时,它才会被拷贝到用户空间的缓冲区中,其效果就相当于,数据包直接发送到了用户空间。通过系统调用write发送数据包时其原理与此类似。
值得注意的是:一次read系统调用,有且只有一个数据包被传送到用户空间,并且当用户空间的缓冲区比较小时,数据包将被截断,剩余部分将永久地消失,write系统调用与read类似,每次只发送一个数据包。所以在编写此类程序的时候,请用足够大的缓冲区,直接调用系统调用read/write,避免采用C语言的带缓存的IO函数。
tun/tap的实现
tun/tap设备驱动初始化也是从init开始,主要进行操作如下:
1.注册netlink用以提供用户空间查看和设置网卡的接口。
2.注册miscdev设备,注册完之后将在系统中生成一个“/dev/net/tun”文件,同字符设备类似,
当应用程序使用open系统调用打开这个文件时,将生成file文件对象,而其file_operations将指向tun_fops。
3.注册网络事件监听回调tun_notifier_block。
tun/tap设备创建
1.用户态调用open函数打开字符设备。
2.然后调用ioctl通知内核创建tun/tap虚拟设备。
3.内核收到请求后会调用tun_chr_ioctl --> __tun_chr_ioctl。
4.__tun_chr_ioctl通过一系列参数检查会调用tun_set_iff进行处理。
5.tun_set_iff函数中申请net_device结构,并net_device私有数据内存大小为struct tun_struct结构体大小。
6.对申请的net_device进行初始化,根据虚拟接口类型对dev->netdev_ops进行赋值,tun赋值为tun_netdev_ops,tap赋值为tap_netdev_ops。
7.私有数据字段用tun变量保存,对tun各字段进行赋值,然后调用tun_attach将tun跟tfile进行关联。
8.将dev注册至net_device列表。
读取数据包流程
1.用户态通过read函数调用内核注册的tun_chr_read_iter函数。
2.tun_chr_read_iter函数根据用户空间传递的文件描述符查到到对应的tfile,tun等信息然后调用tun_do_read读取skb。
3.tun_do_read函数中先调用tun_ring_recv读取skb,如果当前没有skb读取,则调用schedule等待下次调度时再读取。
4.然后调用tun_put_user将读取到的skb进行一些处理(主要是vlan处理),然后再拷贝到用户空间,通过read返回。
数据包写入流程
1.用户态通过write函数调用内核注册的tun_chr_write_iter函数。
2.tun_chr_write_iter函数根据用户空间传递的文件描述符查到到对应的tfile,tun等信息然后调用tun_get_user处理。
3.tun_get_user函数首先进行一些非法检查,然后根据tun->flags对数据进行一些处理,然后申请skb,将数据封装进入skb,根据接口类型tun/tap对skb相关字段进行设置。
4.设置skb的网络层头部字段,然后调用netif_rx_ni,最终会进入netif_receive_skb进入协议栈。
数据结构关系图
tun/tap收发包流程
使用实例
用户态代码如下:
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int tun_create(char *dev, int flags)
{
struct ifreq ifr;
int fd, err;
assert(dev != NULL);
if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
return fd;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags |= flags;
if (*dev != '\0')
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
close(fd);
return err;
}
strcpy(dev, ifr.ifr_name);
return fd;
}
int main(int argc, char *argv[])
{
int i = 0;
int tun, ret;
char tun_name[IFNAMSIZ];
unsigned char buf[4096] = {0};
tun_name[0] = '\0';
tun = tun_create(tun_name, IFF_TUN | IFF_NO_PI);
if (tun < 0) {
perror("tun_create");
return 1;
}
printf("TUN name is %s\n", tun_name);
while (1) {
unsigned char ip[4];
ret = read(tun, buf, sizeof(buf));
if (ret < 0)
break;
printf("read %d bytes\n", ret);
memcpy(ip, &buf[12], 4);
memcpy(&buf[12], &buf[16], 4);
memcpy(&buf[16], ip, 4);
buf[20] = 0;
*((unsigned short*)&buf[22]) += 8;
ret = write(tun, buf, ret);
printf("write %d bytes\n", ret);
}
return 0;
}
上述代码简单地处理了ICMP的ECHO包,并回应以ECHO REPLY。
编译 make tuntap
执行./tuntap
再开启另一个终端执行
ifconfig tun0 0.0.0.0 up
route add 10.10.10.1 dev tun0
ping 10.10.10.1
查看结果如下:
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
64 bytes from 10.10.10.1: icmp_seq=1 ttl=64 time=0.056 ms
64 bytes from 10.10.10.1: icmp_seq=2 ttl=64 time=0.079 ms
64 bytes from 10.10.10.1: icmp_seq=3 ttl=64 time=0.050 ms
切换到执行tuntap终端查看如下:
read 84 bytes
write 84 bytes
read 84 bytes
write 84 bytes
read 84 bytes
write 84 bytes
符合代码逻辑
参考文档:
http://blog.chinaunix.net/uid-28541347-id-5765256.html
https://blog.csdn.net/hunanchenxingyu/article/details/28422867