tun、tap

Tue Jan 8, 2019

2700 Words|Read in about 6 Min
Tags: linux   tun   tap  

对于网络设备来说,就像一个管道(pipe),有两端,从其中任意一端收到的数据将从另一端发出去。 如物理网卡eth0,他都两端分别为内核协议栈和外部物理网络,从外部收到都数据会转发给内核协议栈,从应用程序收到都数据会通过物理网络发送出去。

TUN、TAP

TUN和TAP设备是Linux内核虚拟网络设备,纯软件实现。 OS向连接到TUN/TAP设备的用户空间程序发送报文;用户空间程序可像物理口发送报文那像向TUN/TAP口发送报文,在这种情况下,TUN/TAP设备发送(或注入)报文到OS协议栈,就像报文从物理端口收到一样。

如何工作

TUN/TAP为简单的点对点或以太网设备,不是从物理介质接收数据包,而是从用户空间程序接收;不通过物理介质发送数据包,而是将它们发送到用户空间程序。假设您在tap0上配置IPX,那么每当内核向tap0发送一个IPX数据包时,它将传递给应用程序(例如 VTun)。应用程序加密、压缩数据包,并通过 TCP/UDP 发送到对端。对端的应用程序解压缩、解密接收的数据包,并将数据包写入TAP设备,然后内核处理数据包,就像该数据包来自真实的物理设备。

在Linux内核中添加了一个TUN/TAP虚拟网络设备的驱动程序和一个与之相关的字符设备/dev/net/tun,字符设备tun作为用户空间和内核空间交换数据的接口。

用户空间的应用程序可以通过这个设备文件来和内核中的驱动程序进行交互,其他操作方式和普通文件操作无异。当内核将数据包发送到虚拟网络设备时,数据包被保存在设备相关的一个队列中,直到用户空间程序通过打开的字符设备tun的描述符读取时,它才会被拷贝到用户空间的缓冲区中,其效果就相当于,数据包直接发送到了用户空间。通过系统调用write发送数据包时其原理与此类似。

tun/tap驱动程序中包含两部分:字符设备驱动和网卡驱动。利用网卡驱动部分接受来自tcp/ip协议栈的网络分包并发送或者反过来将接收到的网络分包传给协议栈处理。而字符设备驱动部门将网络分包在内核与用户态之间传送,模拟物理链路的数据接受和发送。tun/tap驱动很好的实现了两种驱动的结合。

下面来看看数据包的流程:

  • 应用程序A是一个普通的程序,通过socket A发送了一个数据包,假设这个数据包的目的IP地址是192.168.3.1
  • socket将这个数据包丢给协议栈
  • 协议栈根据数据包的目的IP地址,匹配本地路由规则,知道这个数据包应该由tun0出去,于是将数据包交给tun0
  • tun0收到数据包之后,发现另一端被进程B打开了,于是将数据包丢给了进程B
  • 进程B收到数据包之后,做一些跟业务相关的处理,然后构造一个新的数据包,将原来的数据包嵌入在新的数据包中,最后通过socket B将数据包转发出去,这时候新数据包的源地址变成了eth0的地址,而目的IP地址变成了一个其它的地址,比如是10.33.0.1.
  • socket B将数据包丢给协议栈
  • 协议栈根据本地路由,发现这个数据包应该要通过eth0发送出去,于是将数据包交给eth0
  • eth0通过物理网络将数据包发送出去

10.33.0.1收到数据包之后,会打开数据包,读取里面的原始数据包,并转发给本地的192.168.3.1,然后等收到192.168.3.1的应答后,再构造新的应答包,并将原始应答包封装在里面,再由原路径返回给应用程序B,应用程序B取出里面的原始应答包,最后返回给应用程序A

从上面的流程中可以看出,数据包选择走哪个网络设备完全由路由表控制,所以如果我们想让某些网络流量走应用程序B的转发流程,就需要配置路由表让这部分数据走tun0。

tun/tap设备有什么用?

从上面介绍过的流程可以看出来,tun/tap设备的用处是将协议栈中的部分数据包转发给用户空间的应用程序,给用户空间的程序一个处理数据包的机会。于是比较常用的数据压缩,加密等功能就可以在应用程序B里面做进去,tun/tap设备最常用的场景是VPN,包括tunnel以及应用层的IPSec等,比较有名的项目是VTun,有兴趣可以去了解一下。

tun和tap的区别

用户层程序通过tun设备只能读写IP数据包,而通过tap设备能读写链路层数据包,类似于普通socket和raw socket的差别一样,处理数据包的格式不一样。

示例程序

#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;
}
#--------------------------第一个shell窗口----------------------
#将上面的程序保存成tun.c,然后编译
[email protected]:~$ gcc tun.c -o tun

#启动tun程序,程序会创建一个新的tun设备,
#程序会阻塞在这里,等着数据包过来
[email protected]:~$ 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地址
[email protected]:~$ sudo ip addr add 192.168.3.11/24 dev tun1

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

#尝试ping一下192.168.3.0/24网段的IP,
#根据默认路由,该数据包会走tun1设备,
#由于我们的程序中收到数据包后,啥都没干,相当于把数据包丢弃了,
#所以这里的ping根本收不到返回包,
#但在前两个窗口中可以看到这里发出去的四个icmp echo请求包,
#说明数据包正确的发送到了应用程序里面,只是应用程序没有处理该包
[email protected]:~$ 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

See Also

Tue Jan 8, 2019

2700 Words|Read in about 6 Min
Tags: linux   tun   tap