Linux Virtual Networking Devices

Table of Contents

1. Linux虚拟网络设备

和磁盘设备类似,Linux 用户想要使用网络功能,不能通过直接操作硬件完成,而需要直接或间接的操作一个 Linux 为我们抽象出来的设备,既通用的 Linux 网络设备来完成。一个常见的情况是,系统里装有一个硬件网卡,Linux 会在系统里为其生成一个网络设备实例,如 eth0,用户需要对 eth0 发出命令以配置或使用它了。更多的硬件会带来更多的设备实例,虚拟的硬件也会带来更多的设备实例。随着网络技术,虚拟化技术的发展,更多的高级网络设备被加入了到了 Linux 中,如:TUN/TAP、VETH、Bridge、802.1.q VLAN device。

对于Linux内核网络设备管理模块来说,虚拟设备和物理设备没有区别,都是网络设备,都能配置IP,从网络设备来的数据,都会转发给协议栈,协议栈过来的数据,也会交由网络设备发送出去,至于是怎么发送出去的,发到哪里去,那是设备驱动的事情,跟Linux内核就没关系了,所以说 虚拟网络设备的一端也是协议栈,而另一端是什么取决于虚拟网络设备的驱动实现。

2. Tun/Tap(可实现用户态程序向内核协议栈注入数据)

TUN (network TUNnel) and TAP (network TAP) are virtual network kernel devices.

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

本节主要摘自:https://segmentfault.com/a/1190000009249039

2.1. Tun/Tap示意图

前面介绍过虚拟网络设备的一端也是协议栈,而另一端是什么取决于虚拟网络设备的驱动实现。

Tun/Tap的另一端是什么?参见下面示意图:

+----------------------------------------------------------------+
|                                                                |
|  +--------------------+      +--------------------+            |
|  | User Application A |      | User Application B |<-----+     |
|  +--------------------+      +--------------------+      |     |
|               | 1                    | 5                 |     |
|...............|......................|...................|.....|
|               ↓                      ↓                   |     |
|         +----------+           +----------+              |     |
|         | socket A |           | socket B |              |     |
|         +----------+           +----------+              |     |
|                 | 2               | 6                    |     |
|.................|.................|......................|.....|
|                 ↓                 ↓                      |     |
|             +------------------------+                 4 |     |
|             | Network Protocol Stack |                   |     |
|             +------------------------+                   |     |
|                | 7                 | 3                   |     |
|................|...................|.....................|.....|
|                ↓                   ↓                     |     |
|        +----------------+    +----------------+          |     |
|        |      eth0      |    |      tun0      |          |     |
|        +----------------+    +----------------+          |     |
|    10.32.0.11  |                   |   192.168.3.11      |     |
|                | 8                 +---------------------+     |
|                |                                               |
+----------------|-----------------------------------------------+
                 ↓
         Physical Network

这个图列举的是一个典型的Tun/Tap设备的应用场景,发到192.168.3.0/24网络的数据通过程序B这个隧道,利用10.32.0.11发到远端网络的10.33.0.1,再由10.33.0.1转发给相应的设备,从而实现VPN。

图中有两个应用程序A和B,都在用户层,而其它的socket、协议栈(Network Protocol Stack)和网络设备(eth0和tun0)部分都在内核层,其实socket是协议栈的一部分,这里分开来的目的是为了看的更直观。

tun0是一个Tun/Tap虚拟设备,从上图中可以看出它和物理设备eth0的差别,它们的一端虽然都连着协议栈,但另一端不一样,eth0的另一端是物理网络,这个物理网络可能就是一个交换机, 而tun0的另一端是一个用户层的程序, 协议栈发给tun0的数据包能被这个应用程序读取到,并且应用程序能直接向tun0写数据。*

这里假设eth0配置的IP是10.32.0.11,而tun0配置的IP是192.168.3.11。

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

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

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

2.2. Tun(IP层)和Tap(链路层)的区别

用户层程序通过Tun设备只能读写第三层数据包(常见的就是IP数据包);而通过Tap设备能读写第二层(即链路层)数据包。 类似于普通socket和raw socket的差别一样,处理数据包的格式不一样。

2.3. 实例:创建Tun设备

下面是一个实例程序,会创建一个Tun设备,它收到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;
}

2.3.1. 演示Tun设备实例程序

下面打开三个shell窗口来演示上面程序。在第一个shell窗口中编译和运行上面程序:

#将上面的程序保存成tun.c,然后编译
dev@debian:~$ gcc tun.c -o tun

#启动tun程序,程序会创建一个新的tun设备,程序会阻塞在这里,等着数据包过来
dev@debian:~$ sudo ./tun
Open tun/tap device tun0 for reading...  #下面4行输出仅当第三个shell窗口中的ping运行后才会有
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

启动上面程序后,会创建一个Tun设备,我测试时新设备名为tun0,你测试时可能设备名为tun1或其它。默认,新创建的tun0没有IP,也没有启用。下面打开第二个shell窗口,给tun0分配IP并启用它,然后启动抓包程序,抓经过tun0的包:

#./tun启动之后,通过ip link命令就会发现系统多了一个tun设备,
#新的设备没有ip,我们先给tun0配上IP地址
dev@debian:~$ sudo ip addr add 192.168.3.11/24 dev tun0

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

#启动抓包程序,抓经过tun0的包
dev@debian:~$ sudo tcpdump -i tun0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes  #下面4行输出仅当第三个shell窗口中的ping运行后才会有
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窗口,通过ping命令创建数据包:

#尝试ping一下192.168.3.0/24网段的IP,
#根据默认路由,该数据包会走tun0设备,
#由于我们的程序中收到数据包后,啥都没干,相当于把数据包丢弃了,
#所以这里的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

3. Veth

The veth devices are virtual Ethernet devices.

Veth设备总是成对出现,它的一端连着协议栈,另一端彼此相连着(注:上一节介绍过,Tun/Tap设备一端连着协议栈,而另一端连着用户层的程序)。 如:

+----------------------------------------------------------------+
|                                                                |
|       +------------------------------------------------+       |
|       |             Network Protocol Stack             |       |
|       +------------------------------------------------+       |
|              ↑               ↑               ↑                 |
|..............|...............|...............|.................|
|              ↓               ↓               ↓                 |
|        +----------+    +-----------+   +-----------+           |
|        |   eth0   |    |   veth0   |   |   veth1   |           |
|        +----------+    +-----------+   +-----------+           |
|192.168.1.11  ↑               ↑               ↑                 |
|              |               +---------------+                 |
|              |         192.168.2.11     192.168.2.1            |
+--------------|-------------------------------------------------+
               ↓
         Physical Network

使用 ip 命令可以创建Veth对,如:

$ ip link add veth0 type veth peer name veth1

3.1. Veth应用:连接两个Namespace

Network Namespace 是 linux 内核提供的功能,主要用于资源的隔离。使用 Network Namespace,一个 Linux 系统就可以抽象出多个网络子系统,各子系统间都有自己的网络设备,协议栈等,彼此之间互不影响。 如果各个 Network Namespace 之间需要通信,可以使用 veth 对来做桥梁, 下文将介绍。

参考:https://www.cnblogs.com/bakari/p/10613710.html

3.1.1. 直接相连

直接相连是最简单的方式,如图 1 所示,一个 veth 对直接将两个 Network Namespace 连接在一起。

linux_veth1.png

Figure 1: 使用一个 veth 对将两个 Network Namespace 连接起来

3.1.2. 通过Bridge相连


Bridge 相当于一台交换机,可以中转两个 Network Namespace 的流量。

如图 2 所示,两个 veth 对分别将两个 Network Namespace 连到 Bridge 上可以实现 Network Namespace 之间的通信。

linux_veth2.gif

Figure 2: 两个 veth 对分别将两个 Network Namespace 连到 Bridge 上

4. Bridge

Bridge(桥)是一个虚拟网络设备,所以具有网络设备的特征,可以配置IP、MAC地址等; Bridge是一个虚拟交换机,和物理交换机(Switch)有类似的功能。

物理交换机工作在数据链路层,在操作过程当中会不断的收集资料去创建它维护的一个“Mac和端口映射表”(这是个学习过程),这个表相当简单,它说明了某个MAC地址是在哪个端口上被发现的,得到这个映射表后,当交换机收到一个TCP/IP数据包时,它便会看一下该数据包的目的MAC地址,核对一下自己的地址表以确认应该从哪个端口把数据包发出去。物理交换机工作方式可以参考:https://zh.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E4%BA%A4%E6%8F%9B%E5%99%A8#%E5%B7%A5%E4%BD%9C%E6%96%B9%E5%BC%8F

对于普通的网络设备来说,只有两端,从一端进来的数据会从另一端出去,例如物理网卡从外面网络中收到的数据会转发给内核协议栈,而从协议栈过来的数据会转发到外面的物理网络中。而Bridge不同,Bridge 有多个端口,数据可以从任何端口进来,进来之后从哪个口出去和物理交换机的原理差不多,要看“Mac和端口映射表”。

Bridge 和现实世界中的二层交换机有一个区别:数据被直接发到 Bridge 上,而不是从一个端口接受。这种情况可以看做 Bridge 自己有一个 MAC 可以主动发送报文,或者说 Bridge 自带了一个隐藏端口和寄主 Linux 系统自动连接,Linux 上的程序可以直接从这个端口向 Bridge 上的其他端口发数据。所以当一个 Bridge 拥有一个网络设备时,如 bridge0 加入了 eth0 时,实际上 bridge0 拥有两个有效 MAC 地址,一个是 bridge0 的,一个是 eth0 的,他们之间可以通讯。由此带来一个有意思的事情是,Bridge 可以设置 IP 地址。通常来说 IP 地址是三层协议的内容,不应该出现在二层设备 Bridge 上。但是 Linux 里 Bridge 是通用网络设备抽象的一种,只要是网络设备就能够设定 IP 地址。当一个 bridge0 拥有 IP 后,Linux 便可以通过路由表或者 IP 表规则在三层定位 bridge0,此时相当于 Linux 拥有了另外一个隐藏的虚拟网卡和 Bridge 的隐藏端口相连,这个网卡就是名为 bridge0 的通用网络设备,IP 可以看成是这个网卡的。当有符合此 IP 的数据到达 bridge0 时,内核协议栈认为收到了一包目标为本机的数据,此时应用程序可以通过 Socket 接收到它。一个更好的对比例子是现实世界中的带路由的交换机设备,它也拥有一个隐藏的 MAC 地址,供设备中的三层协议处理程序和管理程序使用。设备里的三层协议处理程序,对应名为 bridge0 的通用网络设备的三层协议处理程序,即寄主 Linux 系统内核协议栈程序。设备里的管理程序,对应 bridge0 寄主 Linux 系统里的应用程序。

注意:当一个设备被 attach 到 Bridge 上时,那个设备的 IP 会变的无效,Linux 不再使用那个 IP 在三层接受数据。举例如下:如果 eth0 本来的 IP 是 192.168.1.2,此时如果收到一个目标地址是 192.168.1.2 的数据,Linux 的应用程序能通过 Socket 操作接受到它。而当 eth0 被 attach 到一个 bridge0 时,尽管 eth0 的 IP 还在,但应用程序是无法接受到上述数据的。此时应该把 IP 192.168.1.2 赋予 bridge0。

4.1. 创建Bridge

使用 ip 命令可以创建Bridge,如:

$ sudo ip link add name br0 type bridge
$ sudo ip link set br0 up

当刚创建一个Bridge时,它是一个独立的网络设备,只有一个端口连着协议栈,其它的端口啥都没连,这样的Bridge没有任何实际功能,如:

+----------------------------------------------------------------+
|                                                                |
|       +------------------------------------------------+       |
|       |             Network Protocol Stack             |       |
|       +------------------------------------------------+       |
|              ↑                                ↑                |
|..............|................................|................|
|              ↓                                ↓                |
|        +----------+                     +------------+         |
|        |   eth0   |                     |     br0    |         |
|        +----------+                     +------------+         |
|              ↑                                                 |
|              |                                                 |
|              |                                                 |
+--------------|-------------------------------------------------+
               ↓
         Physical Network

可以通过 ip 命令配置Bridge,这里不详细介绍。

4.2. Bridge使用场景

下面介绍Bridge的两个使用场景。

4.2.1. 连接虚拟机

虚拟机通过Tun/Tap或者其它类似的虚拟网络设备,将虚拟机内的网卡同br0连接起来,这样就达到和真实交换机一样的效果,虚拟机发出去的数据包先到达br0,然后由br0交给eth0发送出去,数据包都不需要经过host机器的协议栈,效率高。如:

+------------------------------------------------------+--------------------------------+---------------------------------+
|                     Host                             |         VirtualMachine1        |         VirtualMachine2         |
|                                                      |                                |                                 |
|  +------------------------------------------------+  |  +-------------------------+   |  +-------------------------+    |
|  |             Network Protocol Stack             |  |  |  Network Protocol Stack |   |  |  Network Protocol Stack |    |
|  +------------------------------------------------+  |  +-------------------------+   |  +-------------------------+    |
|                     ↑                                |              ↑                 |               ↑                 |
|.....................|................................|..............|.................|...............|.................|
|                     ↓                                |              ↓                 |               ↓                 |
|                +---------------+                     |      +---------------+         |     +---------------+           |
|                | 192.168.3.101 |                     |      | 192.168.3.102 |         |     | 192.168.3.103 |           |
|   +------+     +---------------+     +-------+       |      +---------------+         |     +---------------+           |
|   | eth0 |<--->|   br0         |<--->|tun/tap|       |          | eth0  |             |           | eth0  |             |
|   +------+     +---------------+     +-------+       |          +-------+             |           +-------+             |
|       ↑             ↑                    ↑           |              ↑                 |               ↑                 |
|       |             |                    +--------------------------+                 |               |                 |
|       |             ↓                                |                                |               |                 |
|       |         +-------+                            |                                |               |                 |
|       |         |tun/tap|                            |                                |               |                 |
|       |         +-------+                            |                                |               |                 |
|       |             ↑                                |                                |               |                 |
|       |             +-----------------------------------------------------------------|---------------+                 |
|       |                                              |                                |                                 |
|       |                                              |                                |                                 |
|       |                                              |                                |                                 |
+-------|----------------------------------------------+--------------------------------+---------------------------------+
        ↓
  Physical Network  (192.168.3.0/24)

4.2.2. 连接Docker

由于容器运行在自己单独的Network Namespace里面,所以都有自己单独的协议栈,情况和上面的虚拟机差不多,但它采用了另一种方式来和外界通信(参见节 3.1.2 ):

+------------------------------------------------------+--------------------------------+---------------------------------+
|                     Host                             |         Container 1            |         Container 2             |
|                                                      |                                |                                 |
|  +------------------------------------------------+  |  +-------------------------+   |  +-------------------------+    |
|  |             Network Protocol Stack             |  |  |  Network Protocol Stack |   |  |  Network Protocol Stack |    |
|  +------------------------------------------------+  |  +-------------------------+   |  +-------------------------+    |
|       ↑               ↑                              |              ↑                 |               ↑                 |
|.......|...............|..............................|..............|.................|...............|.................|
|       ↓               ↓                              |              ↓                 |               ↓                 |
|   +-------------+  +------------+                    |       +-------------+          |     +-------------+             |
|   |192.168.3.101|  |192.168.9.1 |                    |       | 192.168.9.2 |          |     | 192.168.9.3 |             |
|   +-------------+  +------------+     +-------+      |       +-------------+          |     +-------------+             |
|   | eth0        |  |   br0      |<--->| veth0 |      |          | eth0  |             |         | eth0  |               |
|   +-------------+  +------------+     +-------+      |          +-------+             |         +-------+               |
|       ↑               ↑                   ↑          |              ↑                 |               ↑                 |
|       |               |                   +-------------------------+                 |               |                 |
|       |               ↓                              |                                |               |                 |
|       |            +-------+                         |                                |               |                 |
|       |            | veth1 |                         |                                |               |                 |
|       |            +-------+                         |                                |               |                 |
|       |               ↑                              |                                |               |                 |
|       |               +---------------------------------------------------------------|---------------+                 |
|       |                                              |                                |                                 |
|       |                                              |                                |                                 |
|       |                                              |                                |                                 |
+-------|----------------------------------------------+--------------------------------+---------------------------------+
        ↓
   Physical Network  (192.168.3.0/24)

容器中配置网关为192.168.9.1,发出去的数据包先到达br0,然后交给host机器的协议栈,由于目的IP是外网IP,且host机器开启了IP forward功能,于是数据包会通过eth0发送出去,由于192.168.9.1是内网IP,所以一般发出去之前会先做NAT转换(NAT转换和IP forward功能都需要自己配置)。由于要经过host机器的协议栈,并且还要做NAT转换,所以性能没有上面虚拟机那种方案好,优点是容器处于内网中,安全性相对要高点(由于数据包统一由IP层从eth0转发出去,所以不存在Mac地址的问题,在无线网络环境下也工作良好)。

5. 参考

本文基本上是由下面几篇文章“重新组织”而来的,推荐阅读原文:
Linux上的基础网络设备详解:https://www.ibm.com/developerworks/cn/linux/1310_xiawc_networkdevice/
Linux虚拟网络设备之tun/tap:https://segmentfault.com/a/1190000009249039
Linux虚拟网络设备之veth:https://segmentfault.com/a/1190000009251098
Linux虚拟网络设备之bridge(桥):https://segmentfault.com/a/1190000009491002
Linux网络虚拟化:network namespace简介:https://cizixs.com/2017/02/10/network-virtualization-network-namespace/

Author: cig01

Created: <2018-10-20 Sat>

Last updated: <2019-05-24 Fri>

Creator: Emacs 27.1 (Org mode 9.4)