Bootstrap

理解linux虚拟网络设备tun

tun是linux的另一种虚拟网络设备,与前面讲过的veth类似,只是另一端连的不一样,veth设备是一端连着内核协议栈,另一端连着另一个netns的协议栈;而tun设备是一端连着内核协议栈,另一端连接着一个用户程序,任何时候从协议栈发到tun网卡的数据都能从用户程序中读到,而从用户程序写入/dev/net/tun的数据都会被内核协议栈收到。

当我们通过程序打开/dev/net/tun设备时,就会发现我们的主机上多了一张网卡,打开多次会新增多张网卡,命名依次为tun0/tun1/tun2…,我们读写tun网卡的方式与正常读写一个文件一样,下面通过示例来了解一下。

首先编写一个c程序,文件名为tun.c,代码如下:

#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 main()
{
    int tun_fd, nread, err;
    char buffer[1500];
    struct ifreq ifr;
    if ((tun_fd = open("/dev/net/tun", O_RDWR)) < 0) {
        return tun_fd;
    }
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
    if ((err = ioctl(tun_fd, TUNSETIFF, (void *) &ifr)) < 0) {
        close(tun_fd);
        return err;
    }
    printf("open tun device: %s for reading...\n", ifr.ifr_name);
    if (tun_fd < 0) {
        perror("opening tun error");
        exit(1);
    }
    while (1) {
        nread = read(tun_fd, buffer, sizeof(buffer));
        if (nread < 0) {
            perror("reading tun error");
            close(tun_fd);
            exit(1);
        }
        printf("read %d bytes from %s\n", nread,ifr.ifr_name);
    }
    return 0;
}

然后编译并执行:

[root@worker2 ~]# gcc tun.c -o tun
[root@worker2 ~]# ./tun
open tun device: tun0 for reading...

然后从另一个终端打开并查看网卡:

[root@worker2 ~]# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 00:50:56:bb:16:db brd ff:ff:ff:ff:ff:ff
153: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 500
    link/none

发现已经多了一张tun0的网卡,我们给这个网卡配上地址,然后启动起来

[root@worker2 ~]# ip addr add 192.168.100.100/24 dev tun0
[root@worker2 ~]# ip link set tun0 up
[root@worker2 ~]# ip route
192.168.100.0/24 dev tun0 proto kernel scope link src 192.168.100.100

注意我们给tun0配上地址时,主机上会多一条路由

最后,ping一下192.168.100.101,正常应该没有回应,但观察打开的第一个终端,发现有收到数据包:

[root@worker2 ~]# ./tun
open tun device: tun0 for reading...
read 84 bytes from tun0
read 84 bytes from tun0
read 84 bytes from tun0
read 84 bytes from tun0
read 84 bytes from tun0

可以看到,我们的用户程序已经收到了ICMP包,注意这里PING的不是我们给tun0设置的地址,如果你去ping tun0的地址192.168.100.100,会发现有正常的ICMP包回应,而终端1却什么也没收到,为什么要PING一个不存在的地址才行呢?让我们来捋一捋:

我们在主机上ping tun0的地址,进入协议栈后,在ROUTING判决时,发现这是本机地址,于是从lo网卡发送出去了,所以tun0永远也收不到这个包,要想让tun0收到包,要ping一个和tun0同网段的地址,因为这样才会让协议栈让这个包从tun0发出去,另一端是我们的程序,我们的程序啥也没干,只是打印了一下结果,所以ping的那一端不会有回应。

;