虚拟网络技术:TUN设备
本文首发于我的公众号码农之屋**(id: Spider1818)** ,专注于干货分享,包含但不限于Java编程、网络技术、Linux内核及实操、容器技术等。欢迎大家关注,二维码文末可以扫。
导读: 云化场景到处都是虚拟机和容器,它们背后的网络管理都离不开虚拟网络设备,了解虚拟网络设备将有助于我们更好地理解云化场景的网络架构。本篇文章将对Linux的TUN进行介绍。
一、虚拟网络设备和物理网络设备的区别
网络设备就像是一个管道,从其中任意一端收到的数据将从另一端发送出去。 比如物理网卡,它的两端分别是内核协议栈和物理网络,从物理网络收到的数据,会转发给内核协议栈,而应用程序从协议栈发过来的数据将会通过物理网络发送出去。
对于Linux内核网络设备管理模块来说,虚拟设备和物理设备没什么区别,都是网络设备,都能配置IP,从网络设备来的数据,都会转发给内核协议栈,而从协议栈过来的数据,也会交由网络设备发送出去(怎么发送出去的,发到哪里去,那是设备驱动的事情,跟Linux内核没关系)。所以说 虚拟网络设备的一端也是协议栈,而另一端是什么则取决于虚拟网络设备的驱动实现。
**二、TUN是什么?**它的另一端是什么?
TUN 是 Linux内核的 一种虚拟三层网络设备 ,纯软件实现, 通过此设备可以处理来自网络层的数据。

从上图可以看出TUN设备和物理设备eth0之间的差别,它们的一端虽然都连着协议栈,但另一端却不一样,eth0连接的是物理网络,而TUN设备的另一端是一个用户态程序,协议栈发给TUN设备的数据包能被这个应用程序读取到,并且应用程序能直接向TUN设备写数据。
这里列举一个典型的TUN设备的应用场景,host1的进程A跟host2的进程B通信,利用TUN驱动进程,实现隧道封装,从而实现以VPN方式进行通信。
这里假设host1的eth0 IP为10.10.10.1/24,TUN设备IP为172.168.1.1,而host2的eth0 IP为10.100.10.1/24,TUN设备IP为192.168.1.1。host1和host2网络是互通的。
下面来我们先来看看在host1上,数据包的发包流程:
1、host1的应用程序A通过socket发送了一个数据包,目的IP地址是172.168.1.100
2、socket将这个数据包丢给内核协议栈处理
3、协议栈根据数据包的目的IP地址,匹配本地路由规则,知道这个数据包应该由TUN虚拟设备出去,于是将数据包交给TUN设备
4、TUN设备收到数据包之后,发现另一端被TUN驱动进程打开了,于是将数据包丢给了TUN驱动进程
5、TUN驱动进程收到数据包之后,做一些跟业务相关的处理,然后构造一个新的数据包,将原来的数据包嵌入在新的数据包中,最后通过socke将数据包转发出去,这时候新数据包的源地址变成了eth0的地址,而目的IP地址变成了host2的eth0的IP
6、socket将数据包丢给内核协议栈
7、协议栈根据本地路由,发现这个数据包应该要通过eth0发送出去,于是将数据包交给eth0
8、eth0通过物理网络将数据包发送出去
接下来我们看下在host2上,数据包的收包流程:
1、eth0收到数据包之后,把数据包丢给了内核协议栈处理
2、协议栈进行拆包(仅拆除了外层封装,内层封装由TUN驱动进程处理),根据目的端口号把数据包发给了TUN驱动进程(两主机的TUN驱动进程都是监听同个端口号)
3、TUN驱动进程拿到数据包后进行解包处理,然后把解出来的包发给TUN设备
4、TUN设备收到包之后移交给内核协议栈处理
5、协议栈根据目的IP,发现是送给应用程序B的,于是将数据包交给程序B,最终程序B就收到了程序A给它发的数据包
从上面的流程中可以看出,数据包选择走哪个网络设备完全由路由表控制,所以如果我们想让某些网络流量走隧道封装的转发流程,就需要配置路由表让这部分数据走TUN设备。
三、TUN设备有什么用?
从上面介绍过的流程可以看出来,TUN设备的用处是将协议栈中的部分数据包转发给用户空间的应用程序,给用户空间的程序一个处理数据包的机会。于是比较常用的数据压缩,加密等功能就可以在TUN设备驱动程序实现, TUN设备最常用的场景是VPN,包括tunnel以及应用层的IPSec。
四、示例程序
这里写了一个程序,它收到TUN设备的数据包之后,只打印出收到了多少字节的数据包,其它的什么都不做。
#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;
}
我的公众号**「码农之屋」(id: Spider1818)** ,分享的内容包括但不限于 Linux、网络、云计算虚拟化、容器Docker、OpenStack、Kubernetes、SDN、OVS、DPDK、Go、Python、C/C++编程技术等内容,欢迎大家关注。

