通用TUN/TAP设备驱动

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通用TUN/TAP设备驱动相关的知识,希望对你有一定的参考价值。

参考技术A 原文 Universal TUN/TAP device driver

TUN/TAP为用户空间提供分组接收和传输。既可以看作一个Point-to-Point设备,也可以看做一个Ethernet设备。它从用户空间程序接收分组,而不是从物理媒介;将分组写入用户空间程序而不是通过物理媒介。

一个程序打开/dev/net/tun字符型文件,并向内核发出ioctl()来注册一个网络设备。根据选项,这个网络设备将显示为tunXX或者tapXX。当程序关闭文件描述符时,这个网络设备和所有对应的路由都将关闭。

用户空间程序read/write IP数据包(通过tun)还是以太网帧(通过tap),取决于选择的设备类型。正在使用哪一个取决于ioctl()设置的flag。

http://vtun.sourceforge.net/tun 下的软件包包含两个简单的示例,介绍如何使用tun和tap设备。两个示例的工作原理都像在两个网络接口之间的桥一样。

另外,最好的例子来源于VTun( http://vtun.sourceforge.net ) :))

创建设备节点:

设置权限:

驱动程序模块自动加载:

确保内核启用了“内核模块加载器” - 模块自动加载支持。 内核应该在第一次访问时加载它。

手动加载:

后一种方法,每次当你需要使用模块的时候,你都需要加载它。另一种方法将在你打开/dev/net/tun的时候自动加载。

设备的名称 char *dev 是具有格式的字符串(e.g. "tun%d"),也可以是任何有效的网络设备名称。注意,字符指针将会被真实网络设备名覆盖。

如果 IFF_NO_PI 标志没有被设置,每一帧格式如下:

从3.8版开始,Linux支持多队列tuntap,它可以使用多个文件描述符(队列)来并行发送或接收数据包。 设备分配与以前相同,如果用户想要创建多个队列,则必须使用IFF_MULTI_QUEUE标志多次调用具有相同设备名称的TUNSETIFF。

char *dev 是设备的名称, queues 是要创建的队列数, fds 用于存储和返回创建给调用者的文件描述符(队列)。 每个文件描述符都作为一个队列的接口,可以被用户空间访问。

引入了一个新的 ioctl(TUNSETQUEUE) 来启用或禁用队列。 当用 IFF_DETACH_QUEUE 标志调用它时,队列被禁用。 当用 IFF_ATTACH_QUEUE 标志调用它时,队列被启用。 通过 TUNSETIFF 创建队列后,队列默认启用。

fd是我们想要启用或禁用的文件描述符(队列),当enable为真时,我们启用它,否则我们禁用它

硬货 | 利用 Linux tap/tun 虚拟设备写一个 ICMP echo 程序

本文首发于我的公众号 CloudDeveloper(ID: cloud_dev),专注于干货分享,号内有大量书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫。

前面两篇文章已经介绍过 tap/tun 的原理和配置工具。这篇文章通过一个编程示例来深入了解 tap/tun 的程序结构。

01 准备工作

首先通过 modinfo tun 查看系统内核是否支持 tap/tun 设备驱动。

[[email protected] ~]# modinfo tun
filename:       /lib/modules/3.10.0-862.14.4.el7.x86_64/kernel/drivers/net/tun.ko.xz
alias:          devname:net/tun
alias:          char-major-10-200
license:        GPL
author:         (C) 1999-2004 Max Krasnyansky <[email protected]>
description:    Universal TUN/TAP device driver
retpoline:      Y
rhelversion:    7.5
srcversion:     50878D5D5A0138445B25AA8
depends:
intree:         Y
vermagic:       3.10.0-862.14.4.el7.x86_64 SMP mod_unload modversions
signer:         CentOS Linux kernel signing key
sig_key:        E4:A1:B6:8F:46:8A:CA:5C:22:84:50:53:18:FD:9D:AD:72:4B:13:03
sig_hashalgo:   sha256

在 linux 2.4 及之后的内核版本中,tun/tap 驱动是默认编译进内核中的。

如果你的系统不支持,请先选择手动编译内核或者升级内核。编译时开启下面的选项即可:

Device Drivers => Network device support => Universal TUN/TAP device driver support

tap/tun 也支持编译成模块,如果编译成模块,需要手动加载它:

[[email protected] ~]# modprobe tun
[[email protected] ~]# lsmod | grep tun
tun                    31665  0

关于以上的详细步骤,网上有很多教程,这里就不再赘述了。

https://blog.csdn.net/lishuhuakai/article/details/70305543

上面只是加载了 tap/tun 模块,要完成 tap/tun 的编码,还需要有设备文件,运行命令:

mknod /dev/net/tun c 10 200 # c表示为字符设备,10和200分别是主设备号和次设备号

这样在 /dev/net 下就创建了一个名为 tun 的文件。

02 编程示例

2.1 启动设备

使用 tap/tun 设备,需要先进行一些初始化工作,如下代码所示:

int tun_alloc(char *dev, int flags)
{
    assert(dev != NULL);

    struct ifreq ifr;
    int fd, err;

    char *clonedev = "/dev/net/tun";

    if ((fd = open(clonedev, O_RDWR)) < 0) {
        return fd;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = flags;
    
    if (*dev != '') {
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);
    }
    if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) {
        close(fd);
        return err;
    }

    // 一旦设备开启成功,系统会给设备分配一个名称,对于tun设备,一般为tunX,X为从0开始的编号;
    // 对于tap设备,一般为tapX
    strcpy(dev, ifr.ifr_name);

    return fd;
}

首先打开字符设备文件 /dev/net/tun,然后用 ioctl 注册设备的工作模式,是 tap 还是 tun。这个模式由结构体 struct ifreq 的属性 ifr_flags 来定义,它有以下表示:

  • IFF_TUN: 表示创建一个 tun 设备。
  • IFF_TAP: 表示创建一个 tap 设备。
  • IFF_NO_PI: 表示不包含包头信息,默认的,每个数据包传到用户空间时,都会包含一个附加的包头来保存包信息,这个表示不加包头。
  • IFF_ONE_QUEUE:表示采用单一队列模式。

还是有一个属性是 ifr_name,表示设备的名字,它可以由用户自己指定,也可以由系统自动分配,比如 tapXtunX,X 从 0 开始编号。

ioctl 完了之后,文件描述符 fd 就和设备建立起了关联,之后就可以根据 fd 进行 read 和 write 操作了。

2.2 写一个 ICMP 的调用函数

为了测试上面的程序,我们写一个简单的 ICMP echo 程序。我们会使用 tun 设备,然后给 tunX 接口发送一个 ping 包,程序简单响应这个包,完成 ICMP 的 request 和 reply 的功能。

如下代码所示:

int main()
{
    int tun_fd, nread;
    char buffer[4096];
    char tun_name[IFNAMSIZ];

    tun_name[0] = '';

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *        IFF_NO_PI - Do not provide packet information
     */
    tun_fd = tun_alloc(tun_name, IFF_TUN | IFF_NO_PI);

    if (tun_fd < 0) {
        perror("Allocating interface");
        exit(1);
    }

    printf("Open tun/tap device: %s for reading...
", tun_name);
    
    while (1) {
        unsigned char ip[4];
        // 收包
        nread = read(tun_fd, buffer, sizeof(buffer));
        if (nread < 0) {
            perror("Reading from interface");
            close(tun_fd);
            exit(1);
        }
        
        printf("Read %d bytes from tun/tap device
", nread);
        
        // 简单对收到的包调换一下顺序
        memcpy(ip, &buffer[12], 4);
        memcpy(&buffer[12], &buffer[16], 4);
        memcpy(&buffer[16], ip, 4);

        buffer[20] = 0;
        *((unsigned short *)&buffer[22]) += 8;
        
        // 发包
        nread = write(tun_fd, buffer, nread);

        printf("Write %d bytes to tun/tap device, that's %s
", nread, buffer);
    }
    return 0;
}

下面测试一下。

2.3 给 tap/tun 设备配置 IP 地址

编译:

[[email protected] coding]# gcc -o taptun taptun.c
[[email protected] coding]# ./taptun
Open tun/tap device: tun0 for reading...

开另一个终端,查看生成了 tun0 接口:

[[email protected] coding]# ip a
6: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN qlen 500
    link/none

tun0 接口配置 IP 并启用,比如 10.1.1.2/24

[[email protected] ~]# ip a a 10.1.1.2/24 dev tun0
[[email protected] ~]# ip l s tun0 up

再开一个终端,用 tcpdumptun0 的包。

[[email protected] ~]# tcpdump -nnt -i tun0

然后在第二个终端 ping 一下 10.1.1.0/24 网段的 IP,比如 10.1.1.3,看到:

[[email protected] ~]# ping -c 4 10.1.1.3
PING 10.1.1.3 (10.1.1.3) 56(84) bytes of data.
64 bytes from 10.1.1.3: icmp_seq=1 ttl=64 time=0.133 ms
64 bytes from 10.1.1.3: icmp_seq=2 ttl=64 time=0.188 ms
64 bytes from 10.1.1.3: icmp_seq=3 ttl=64 time=0.092 ms
64 bytes from 10.1.1.3: icmp_seq=4 ttl=64 time=0.110 ms

--- 10.1.1.3 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3290ms
rtt min/avg/max/mdev = 0.092/0.130/0.188/0.038 ms

由于 tun0 接口建好之后,会生成一条到本网段 10.1.1.0/24 的默认路由,根据默认路由,数据包会走 tun0 口,所以能 ping 通,可以用 route -n 查看。

再看 tcpdump 抓包终端,成功显示 ICMP 的 request 包和 reply 包。

[[email protected] ~]# tcpdump -nnt -i tun0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
IP 10.1.1.2 > 10.1.1.3: ICMP echo request, id 3250, seq 1, length 64
IP 10.1.1.3 > 10.1.1.2: ICMP echo reply, id 3250, seq 1, length 64
IP 10.1.1.2 > 10.1.1.3: ICMP echo request, id 3250, seq 2, length 64
IP 10.1.1.3 > 10.1.1.2: ICMP echo reply, id 3250, seq 2, length 64

再看程序 taptun.c 的输出:

[[email protected] coding]# ./taptun
Open tun/tap device: tun0 for reading...
Read 48 bytes from tun/tap device
Write 48 bytes to tun/tap device
Read 48 bytes from tun/tap device
Write 48 bytes to tun/tap device

ok,以上便验证了程序的正确性。

03 总结

通过这个小例子,让我们知道了基于 tap/tun 编程的流程,对 tap/tun 又加深了一层理解。

使用 tap/tun 设备需要包含头文件 #include <linux/if_tun.h>,以下是完整代码。

/******************************************************************************
 *  File Name: taptun.c
 *  Author: 公众号: CloudDeveloper
 *  Created Time: 2019年02月23日 星期六 21时28分24秒
 *****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <linux/if_tun.h>

int tun_alloc(char *dev, int flags)
{
    assert(dev != NULL);

    struct ifreq ifr;
    int fd, err;

    char *clonedev = "/dev/net/tun";

    if ((fd = open(clonedev, O_RDWR)) < 0) {
        return fd;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = flags;
    
    if (*dev != '') {
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);
    }
    if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) {
        close(fd);
        return err;
    }

    // 一旦设备开启成功,系统会给设备分配一个名称,对于tun设备,一般为tunX,X为从0开始的编号;
    // 对于tap设备,一般为tapX
    strcpy(dev, ifr.ifr_name);

    return fd;
}

int main()
{
    int tun_fd, nread;
    char buffer[4096];
    char tun_name[IFNAMSIZ];

    tun_name[0] = '';

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *        IFF_NO_PI - Do not provide packet information
     */
    tun_fd = tun_alloc(tun_name, IFF_TUN | IFF_NO_PI);

    if (tun_fd < 0) {
        perror("Allocating interface");
        exit(1);
    }

    printf("Open tun/tap device: %s for reading...
", tun_name);
    
    while (1) {
        unsigned char ip[4];
        // 收包
        nread = read(tun_fd, buffer, sizeof(buffer));
        if (nread < 0) {
            perror("Reading from interface");
            close(tun_fd);
            exit(1);
        }
        
        printf("Read %d bytes from tun/tap device
", nread);
        
        // 简单对收到的包调换一下顺序
        memcpy(ip, &buffer[12], 4);
        memcpy(&buffer[12], &buffer[16], 4);
        memcpy(&buffer[16], ip, 4);

        buffer[20] = 0;
        *((unsigned short *)&buffer[22]) += 8;
        
        // 发包
        nread = write(tun_fd, buffer, nread);

        printf("Write %d bytes to tun/tap device, that's %s
", nread, buffer);
    }
    return 0;
}

我的公众号 CloudDeveloper(ID: cloud_dev),号内有大量书籍和视频资源,后台回复「1024」即可领取,分享的内容包括但不限于云计算虚拟化、容器、OpenStack、K8S、雾计算、网络、工具、SDN、OVS、DPDK、Linux、Go、Python、C/C++编程技术等内容,欢迎大家关注。

技术图片

以上是关于通用TUN/TAP设备驱动的主要内容,如果未能解决你的问题,请参考以下文章

TUN/TAP设备浅析

CentOS下使用TUN/TAP虚拟网卡的基本教程

硬货 | 利用 Linux tap/tun 虚拟设备写一个 ICMP echo 程序

[Linux用户空间编程-4]:Linux虚拟网络设备TUN/TAP的工作原理与代码示例

ubuntu14.04安装tun/tap网络设备

Kubernetes网络自学系列 | 给用户态一个机会:tun/tap设备