0%

【网络虚拟化】TUN/TAP

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

参考资料