TUN/TAP 虚拟网络设备一端连着协议栈,另外一端不是物理网络,而是另外一个处于用户空间的应用程序。也就是说,协议栈发给 TUN/TAP 的数据包能被这个应用程序读取到,当然应用程序能直接向 TUN/TAP 发送数据包。

一个典型的 TUN/TAP 的例子如下图所示:

上图中我们配置了一个物理网卡,IP 为 18.12.0.92,而 tun0 为一个 TUN/TAP 设备,IP 配置为 10.0.0.12。数据包的流向为:

  1. 应用程序 A 通过 socket A 发送了一个数据包,假设这个数据包的目的 IP 地址是 10.0.0.22

  2. socket A 将这个数据包丢给协议栈

  3. 协议栈根据本地路由规则和数据包的目的 IP,将数据包由给 tun0 设备发送出去

  4. tun0 收到数据包之后,将数据包转发给给了用户空间的应用程序 B

  5. 应用程序 B 收到数据包之后构造一个新的数据包,将原来的数据包嵌入在新的数据包(IPIP 包)中,最后通过 socket B 将数据包转发出去

    Note: 新数据包的源地址变成了 eth0 的地址,而目的 IP 地址则变成了另外一个地址 18.13.0.91.

  6. socket B 将数据包发给协议栈

  7. 协议栈根据本地路由规则和数据包的目的 IP,决定将这个数据包要通过 eth0 发送出去,于是将数据包转发给 eth0

  8. eth0 通过物理网络将数据包发送出去

我们看到发送给 10.0.0.22的网络数据包通过在用户空间的应用程序 B,利用 18.12.0.92发到远端网络的 18.13.0.91,网络包到达 18.13.0.91后,读取里面的原始数据包,读取里面的原始数据包,再转发给本地的 10.0.0.22。这就是VPN的基本原理。

使用 TUN/TAP 设备我们有机会将协议栈中的部分数据包转发给用户空间的应用程序,让应用程序处理数据包。常用的使用场景包括数据压缩,加密等功能。

Note: TUN 和 TAP 设备的区别在于,TUN 设备是一个虚拟的端到端 IP 层设备,也就是说用户空间的应用程序通过 TUN 设备只能读写 IP 网络数据包(三层),而 TAP 设备是一个虚拟的链路层设备,通过 TAP 设备能读写链路层数据包(二层)。在使用 ip命令创建设备的时候使用 --dev tun--dev tap来区分。

代码示例

这里写了一个程序,它收到 tun 设备的数据包之后,只打印出收到了多少字节的数据包,其它的什么都不做,如何编程请参考后面的参考链接。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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>
#include<stdlib.h>
#include<stdio.h>

int tun_alloc(int flags)
{

    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 ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) {
        close(fd);
        return err;
    }

    printf("Open tun/tap device: %s for reading...\n", ifr.ifr_name);

    return fd;
}

int main()
{
    int tun_fd, nread;
    char buffer[1500];

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

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

    while (1) {
        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\n", nread);
    }
    return 0;
}

虚拟设备演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#--------------------------第一个shell窗口----------------------
#将上面的程序保存成tun.c,然后编译
dev@debian:~$ gcc tun.c -o tun

#启动tun程序,程序会创建一个新的tun设备,
#程序会阻塞在这里,等着数据包过来
dev@debian:~$ sudo ./tun
Open tun/tap device tun1 for reading...
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device

#--------------------------第二个shell窗口----------------------
#启动抓包程序,抓经过tun1的包
# tcpdump -i tun1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun1, link-type RAW (Raw IP), capture size 262144 bytes
19:57:13.473101 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 1, length 64
19:57:14.480362 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 2, length 64
19:57:15.488246 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 3, length 64
19:57:16.496241 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 4, length 64

#--------------------------第三个shell窗口----------------------
#./tun启动之后,通过ip link命令就会发现系统多了一个tun设备,
#在我的测试环境中,多出来的设备名称叫tun1,在你的环境中可能叫tun0
#新的设备没有ip,我们先给tun1配上IP地址
dev@debian:~$ sudo ip addr add 192.168.3.11/24 dev tun1

#默认情况下,tun1没有起来,用下面的命令将tun1启动起来
dev@debian:~$ sudo ip link set tun1 up

#尝试ping一下192.168.3.0/24网段的IP,
#根据默认路由,该数据包会走tun1设备,
#由于我们的程序中收到数据包后,啥都没干,相当于把数据包丢弃了,
#所以这里的ping根本收不到返回包,
#但在前两个窗口中可以看到这里发出去的四个icmp echo请求包,
#说明数据包正确的发送到了应用程序里面,只是应用程序没有处理该包
dev@debian:~$ ping -c 4 192.168.3.12
PING 192.168.3.12 (192.168.3.12) 56(84) bytes of data.

--- 192.168.3.12 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3023ms

什么是 tun/tap?

TUN/TAP 虚拟网络设备为用户空间程序提供了网络数据包的发送和接收能力。他既可以当做点对点设备(TUN),也可以当做以太网设备(TAP)。实际上,不仅 Linux 支持 TUN/TAP 虚拟网络设备,其他 UNIX 也是支持的,他们之间只有少许差别。

TUN/TAP 虚拟网络设备的原理比较简单,他在 Linux 内核中添加了一个 TUN/TAP 虚拟网络设备的驱动程序和一个与之相关连的字符设备/dev/net/tun,字符设备 tun 作为用户空间和内核空间交换数据的接口。当内核将数据包发送到虚拟网络设备时,数据包被保存在设备相关的一个队列中,直到用户空间程序通过打开的字符设备 tun 的描述符读取时,它才会被拷贝到用户空间的缓冲区中,其效果就相当于,数据包直接发送到了用户空间。通过系统调用 write 发送数据包时其原理与此类似。

值得注意的是:一次read系统调用,有且只有一个数据包被传送到用户空间,并且当用户空间的缓冲区比较小时,数据包将被截断,剩余部分将永久地消失,write系统调用与read类似,每次只发送一个数据包。所以在编写此类程序的时候,请用足够大的缓冲区,直接调用系统调用read/write,避免采用C语言的带缓存的IO函数。

TUN/TAP 是一类虚拟网卡的驱动。网卡驱动很好理解,就是 netdev+driver,最后将数据包通过这些驱动发送出去,netdev 可以参考内核或者 OVS 代码,基本使用的就是几个钩子函数。

虚拟网卡就是没有物理设备的网卡,那么他的驱动就是需要开发人员自己编写。一般虚拟网卡用于实现物理网卡不愿意做的事情,例如 tunnel 封装(用于 vpn,openvpn( http://openvpn.sourceforge.net)和Vtun( http://vtun.sourceforge.net)),多个物理网卡的聚合等。一般使用虚拟网卡的方式与使用物理网卡一样,在协议栈中通过回调函数call到虚拟网卡的API,经过虚拟网卡处理之后的数据包再由协议栈发送出去。

tun/tap 的使用

linux2.4 内核之后代码默认编译 tun、tap 驱动,使用的时候只需要将模块加载即可(modprobe tun,mknod /dev/net/tun c 10 200)。运行 tun、tap 设备之后,会在内核空间添加一个杂项设备(miscdevice,类比字符设备、块设备等)/dev/net/tun,实质上是主设备号 10 的字符设备。从功能上看,tun 设备驱动主要应该包括两个部分,一是虚拟网卡驱动,其实就是虚拟网卡中对 skb 进行封装解封装等操作;二是字符设备驱动,用于内核空间与用户空间的交互。

源代码在/drivers/net/tun.c 中,与其他 netdev 类似,tun 这个 netdev 也提供 open、close、read、write 等 API。在分析 TUN/TAP 驱动实现前,我们先看下如何使用。使用 tun/tap 设备的示例程序(摘自 openvpn 开源项目 http://openvpn.sourceforge.net,tun.c 文件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int open_tun (const char *dev, char *actual, int size)
{
    struct ifreq ifr;
    int fd;
    char *device = "/dev/net/tun";
    if ((fd = open (device, O_RDWR)) < 0) //创建描述符
        msg (M_ERR, "Cannot open TUN/TAP dev %s", device);
    memset (&ifr, 0, sizeof (ifr));
    ifr.ifr_flags = IFF_NO_PI;
    if (!strncmp (dev, "tun", 3)) {
        ifr.ifr_flags |= IFF_TUN;
    }
    else if (!strncmp (dev, "tap", 3)) {
        ifr.ifr_flags |= IFF_TAP;
    }
    else {
        msg (M_FATAL, "I don't recognize device %s as a TUN or TAP device",dev);
    }
    if (strlen (dev) > 3) /* unit number specified? */
        strncpy (ifr.ifr_name, dev, IFNAMSIZ);
    if (ioctl (fd, TUNSETIFF, (void *) &ifr) < 0) //打开虚拟网卡
        msg (M_ERR, "Cannot ioctl TUNSETIFF %s", dev);
    set_nonblock (fd);
    msg (M_INFO, "TUN/TAP device %s opened", ifr.ifr_name);
    strncpynt (actual, ifr.ifr_name, size);
    return fd;
}

调用上述函数后,就可以在 shell 命令行下使用 ifconfig 命令配置虚拟网卡了:

1
2
3
ifconfig devname 10.0.0.1 up

route add -net 10.0.0.2 netmask 255.255.255.255 dev devname

配置好虚拟网卡地址后,就可以通过生成的字符设备描述符,在程序中使用 read 和 write 函数就可以读取或者发送给虚拟的网卡数据了。

tun/tap 的实现

tun/tap 设备驱动的开始也是 init 函数,其中主要调用了 misc_register 注册了一个 miscdev 设备。

1
2
3
4
5
6
static int __init tun_init(void)
{
     /*……*/
     ret = misc_register(&tun_miscdev);
     /*……*/
}

而 tun_miscdev 得定义如下:

1
2
3
4
5
6
static struct miscdevice tun_miscdev = {
         .minor = TUN_MINOR,
         .name = "tun",
         .nodename = "net/tun",
         .fops = &tun_fops,
}

注册完这个设备之后将在系统中生成一个“/dev/net/tun”文件,同字符设备类似,当应用程序使用 open 系统调用打开这个文件时,将生成 file 文件对象,而其 file_operations 将指向 tun_fops。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static const struct file_operations tun_fops = {
    .owner    = THIS_MODULE,
    .llseek = no_llseek,
    .read = do_sync_read,
    .aio_read = tun_chr_aio_read,
    .write = do_sync_write,
    .aio_write = tun_chr_aio_write,
    .poll    = tun_chr_poll,
    .unlocked_ioctl    = tun_chr_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = tun_chr_compat_ioctl,
#endif
    .open    = tun_chr_open,
    .release = tun_chr_close,
    .fasync = tun_chr_fasync
};

下面我们以应用层使用的步骤来分析内核的对应实现。应用层首先调用 open 打开“/dev/net/tun”,这将最终调用 tun_fops 的 open 函数,即 tun_chr_open。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int tun_chr_open(struct inode *inode, struct file * file)
{
    struct tun_file *tfile;

    DBG1(KERN_INFO, "tunX: tun_chr_open\n");

    /*分配并初始化struct tun_file结构*/
    tfile = (struct tun_file *)sk_alloc(&init_net, AF_UNSPEC, GFP_KERNEL,
                     &tun_proto);
    if (!tfile)
        return -ENOMEM;
    rcu_assign_pointer(tfile->tun, NULL);
    tfile->net = get_net(current->nsproxy->net_ns);
    tfile->flags = 0;

    rcu_assign_pointer(tfile->socket.wq, &tfile->wq);
    init_waitqueue_head(&tfile->wq.wait);

    tfile->socket.file = file;
    /*设置struct tun_file的socket成员ops*/
    tfile->socket.ops = &tun_socket_ops;

    sock_init_data(&tfile->socket, &tfile->sk);
    sk_change_net(&tfile->sk, tfile->net);

    tfile->sk.sk_write_space = tun_sock_write_space;
    tfile->sk.sk_sndbuf = INT_MAX;
    /*将struct tun_file作为file的私有字段,而file就是每次应用调用open打开/dev/net/tun生成的*/
    file->private_data = tfile;
    set_bit(SOCK_EXTERNALLY_ALLOCATED, &tfile->socket.flags);
    INIT_LIST_HEAD(&tfile->next);

    sock_set_flag(&tfile->sk, SOCK_ZEROCOPY);

    return 0;
}

经过这个函数后,整个数据结构的关系就如下图所示。注意这里的 struct file 结构就是每次应用调用 open 打开/dev/net/tun 生成的。

img
img

应用程序执行完 open 操作后,一般会执行 ioctl (fd, TUNSETIFF, (void *) &ifr) 来真正创建 tap/tun 设备。这将最终调用 tun_ops 中的 tun_chr_ioctl 函数。

l tun_chr_ioctl

tun_chr_ioctl 中会调用__tun_chr_ioctl。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
static long __tun_chr_ioctl(struct file *file, unsigned int cmd,
             unsigned long arg, int ifreq_len)
{
    struct tun_file *tfile = file->private_data;
    struct tun_struct *tun;
    void __user* argp = (void __user*)arg;
    struct ifreq ifr;
    kuid_t owner;
    kgid_t group;
    int sndbuf;
    int vnet_hdr_sz;
    int ret;

    if (cmd == TUNSETIFF || cmd == TUNSETQUEUE || _IOC_TYPE(cmd) == 0x89) {
        if (copy_from_user(&ifr, argp, ifreq_len))
            return -EFAULT;
    } else {
        memset(&ifr, 0, sizeof(ifr));
    }
    if (cmd == TUNGETFEATURES) {
        /* Currently this just means: "what IFF flags are valid?".
         * This is needed because we never checked for invalid flags on
         * TUNSETIFF. */
        return put_user(IFF_TUN | IFF_TAP | IFF_NO_PI | IFF_ONE_QUEUE |
                IFF_VNET_HDR | IFF_MULTI_QUEUE,
                (unsigned int __user*)argp);
    } else if (cmd == TUNSETQUEUE)
        return tun_set_queue(file, &ifr);

    ret = 0;
    rtnl_lock();
/*获取tun_struct结构,首次调用TUNSETIFF时为NULL*/
    tun = __tun_get(tfile);
    if (cmd == TUNSETIFF && !tun) {
        ifr.ifr_name[IFNAMSIZ-1] = '\0';

        ret = tun_set_iff(tfile->net, file, &ifr);

        if (ret)
            goto unlock;

        if (copy_to_user(argp, &ifr, ifreq_len))
            ret = -EFAULT;
        goto unlock;
    }

    ret = -EBADFD;
    if (!tun)
        goto unlock;

    ret = 0;
    switch (cmd) {
    case TUNGETIFF:
    /*……*/
   }
   unlock:
   rtnl_unlock();
   if (tun)
      tun_put(tun);
   return ret;
}

可以看出如果 cmd 是 TUNSETIFF,则会调用 tun_set_iff 函数。

l tun_set_iff

点击(此处)折叠或打开

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
{
    struct tun_struct *tun;
    struct tun_file *tfile = file->private_data;
    struct net_device *dev;
    int err;

    if (tfile->detached)
        return -EINVAL;

    dev = __dev_get_by_name(net, ifr->ifr_name);
    if (dev) { /*首次调用dev为NULL*/
         /*略*/
    }
    else {
        char *name;
        unsigned long flags = 0;
        int queues = ifr->ifr_flags & IFF_MULTI_QUEUE ?
             MAX_TAP_QUEUES : 1;

        if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
            return -EPERM;

        /* Set dev type */
        if (ifr->ifr_flags & IFF_TUN) { /*tun设备*/
            /* TUN device */
            flags |= TUN_TUN_DEV;
            name = "tun%d";
        } else if (ifr->ifr_flags & IFF_TAP) { /*tap设备*/
            /* TAP device */
            flags |= TUN_TAP_DEV;
            name = "tap%d";
        } else
            return -EINVAL;

        if (*ifr->ifr_name)
            name = ifr->ifr_name;
        /*分配net_device结构,并将struct tun_struct作为其private结构*/
        dev = alloc_netdev_mqs(sizeof(struct tun_struct), name,
                 tun_setup, queues, queues);

        if (!dev)
            return -ENOMEM;

        dev_net_set(dev, net);
        dev->rtnl_link_ops = &tun_link_ops;

        tun = netdev_priv(dev);
        tun->dev = dev;
        tun->flags = flags; /*标识设备的类型,tun或tap*/
        tun->txflt.count = 0;
        tun->vnet_hdr_sz = sizeof(struct virtio_net_hdr);

        tun->filter_attached = false;
        tun->sndbuf = tfile->socket.sk->sk_sndbuf;

        spin_lock_init(&tun->lock);
        /*根据设备类型是tap或tun初始化net_device结构,关键是其dev->netdev_ops 成员*/
        tun_net_init(dev);

        err = tun_flow_init(tun);
        if (err < 0)
            goto err_free_dev;

        dev->hw_features = NETIF_F_SG | NETIF_F_FRAGLIST |
            TUN_USER_FEATURES;
        dev->features = dev->hw_features;
        dev->vlan_features = dev->features;

        INIT_LIST_HEAD(&tun->disabled);
        /*将tun(tun_struct)和file的private,即tun_file关联*/
        err = tun_attach(tun, file);
        if (err < 0)
            goto err_free_flow;
        /*注册net_device,完成网络设备驱动程序的注册*/
        err = register_netdevice(tun->dev);
        if (err < 0)
            goto err_detach;

        if (device_create_file(&tun->dev->dev, &dev_attr_tun_flags) ||
         device_create_file(&tun->dev->dev, &dev_attr_owner) ||
         device_create_file(&tun->dev->dev, &dev_attr_group))
            pr_err("Failed to create tun sysfs files\n");
    }
    /*……*/
    if (netif_running(tun->dev)) /*运行网卡qdisc 队列*/
        netif_tx_wake_all_queues(tun->dev);

    strcpy(ifr->ifr_name, tun->dev->name);
    return 0;

err_detach:
    tun_detach_all(dev);
err_free_flow:
    tun_flow_uninit(tun);
    security_tun_dev_free_security(tun->security);
err_free_dev:
    free_netdev(dev);
    return err;
}

经过这个函数之后 tun/tap 的相关数据结构组织就如下图所示了。

img
img

数据通道实现

下面我们看 tun/tap 设备是如何进行数据转发的,我们从两个方向分析,首先是用户态到内核态,然后是内核态到用户态。整个过程如下图所示。

img
img

用户态到内核态

用户态调用 write 向 tun/tap 设备中写入数据,最终调用 kernel 中对应 file 结构中的 tun_fops 的 write 函数。整个调用路径如下。

img
img

注意 tun/tap 设备的 net_device 并没有向 bridge 那样注册 rx_handle 接受函数,所以经过 netif_receive_skb 后就进入了上层协议栈,对于系统来说就像从物理网卡 eth0 接受上来的包一样。如果用户态想在接受,就要创建 socket 了。

内核态到用户态

从 tun/tap 设备发出的数据包,就需要调用 net_device 的 ndo_start_xmit 函数了。整个流程如下图(橙色的线)。这里要说明一点,有人可能会疑惑如果所有进程都打开”/dev/net/tun”读取数据不会混淆吗?答案是不会的,因为每个进程 open 后内核都有自己的 file 文件对象,同时 TUNSETIFF 后也会有不同的 net_device 设备对象。

img
img

参考资料