0%

网络虚拟化

网络虚拟化更多关注的是数据中心网络、主机网络这样比较「细粒度」的网络,所谓细粒度,是相对来说的,是深入到某一台物理主机之上的网络结构来谈的。

如果把传统的网络看作「宏观网络」的话,那网络虚拟化关注的就是「微观网络」。网络虚拟化的目的,是要节省物理主机的网卡设备资源。从资源这个角度去理解,可能会比较好理解一点。

网络虚拟化

传统网络架构

在传统网络环境中,一台物理主机包含一个或多个网卡(NIC),要实现与其他物理主机之间的通信,需要通过自身的 NIC 连接到外部的网络设施,如交换机上,如下图所示。

img

这种架构下,为了对应用进行隔离,往往是将一个应用部署在一台物理设备上,这样会存在两个问题,

  • 是某些应用大部分情况可能处于空闲状态
  • 是当应用增多的时候,只能通过增加物理设备来解决扩展性问题。

不管怎么样,这种架构都会对物理资源造成极大的浪费。

虚拟化网络架构

为了解决这个问题,可以借助虚拟化技术对一台物理资源进行抽象,将一张物理网卡虚拟成多张虚拟网卡(vNIC),通过虚拟机来隔离不同的应用。

  • 针对问题 1),可以利用虚拟化层 Hypervisor 的调度技术,将资源从空闲的应用上调度到繁忙的应用上,达到资源的合理利用;
  • 针对问题 2),可以根据物理设备的资源使用情况进行横向扩容,除非设备资源已经用尽,否则没有必要新增设备。这种架构如下所示。

img

其中虚拟机与虚拟机之间的通信,由虚拟交换机完成,虚拟网卡和虚拟交换机之间的链路也是虚拟的链路,整个主机内部构成了一个虚拟的网络,如果虚拟机之间涉及到三层的网络包转发,则又由另外一个角色——虚拟路由器来完成。

一般,这一整套虚拟网络的模块都可以独立出去,由第三方来完成,如其中比较出名的一个解决方案就是 Open vSwitch(OVS)。

OVS 的优势在于它基于 SDN 的设计原则,方便虚拟机集群的控制与管理,另外就是它分布式的特性,可以「透明」地实现跨主机之间的虚拟机通信,如下是跨主机启用 OVS 通信的图示。

img

总结下来,网络虚拟化主要解决的是虚拟机构成的网络通信问题,完成的是各种网络设备的虚拟化,如网卡、交换设备、路由设备等。

在Linux内核中有一个网络设备管理层,处于网络设备驱动和协议栈之间,负责衔接它们之间的数据交互。驱动不需要了解协议栈的细节,协议栈也不需要了解设备驱动的细节。

对于一个网络设备来说,就像一个管道(pipe)一样,有两端,从其中任意一端收到的数据将从另一端发送出去。

比如一个物理网卡eth0,它的两端分别是内核协议栈(通过内核网络设备管理模块间接的通信)和外面的物理网络,从物理网络收到的数据,会转发给内核协议栈,而应用程序从协议栈发过来的数据将会通过物理网络发送出去。

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

NAT

网络设备的驱动程序并不直接与内核中的协议栈交互,而是通过内核的网络设备管理模块。这样做的好处是,驱动程序不需要了解协议栈的细节,协议栈也不需要针对特定驱动处理数据包。

对于内核网络设备管理模块来说,虚拟设备和物理设备没有区别,都是网络设备,都能配置IP,甚至从逻辑上来看,虚拟网络设备和物理网络设备并没有什么区别,它们都类似于管道,从任意一端接收到的数据将从另外一端发送出去。比如物理网卡的两端分别是协议栈于外面的物理网络,从外面物理网络接收到的数据包会转发给协议栈,相反,应用程序通过协议栈发送过来的数据包会通过物理网卡发送到外面的物理网络。但是对于具体将数据包发送到哪里,怎么发送,不同的网络设备有不同的驱动实现,与内核设备管理模块以及协议栈没什么关系。

总的来说,虚拟网络设备与物理网络设备没有什么区别,它们的一端连接着内核协议栈,而另一端的行为是什么取决于不同虚拟网络设备的驱动实现。

Tun/Tap

TUN/TAP虚拟网络设备一端连着协议栈,另外一端不是物理网络,而是另外一个处于用户空间的应用程序。也就是说,协议栈发给TUN/TAP的数据包能被这个应用程序读取到,当然应用程序能直接向TUN/TAP发送数据包。

这里简单介绍 Tun/Tap 设备的用法,关于其在内核中的实现,可以参考 我的另一篇文章

一个典型的TUN/TAP的例子如下图所示:

network-device-tun-tap.jpg

上图中我们配置了一个物理网卡,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

Veth

veth 虚拟网络设备一端连着协议栈,另外一端不是物理网络,而是另一个veth 设备,成对的veth设备中一个数据包发送出去后会直接到另一个veth设备上去。每个veth设备都可以被配置IP地址,并参与三层 IP 网络路由过程。

下面就是一个典型的veth设备对的例子:

network-device-veth.jpg

我们配置物理网卡eth0的IP为12.124.10.11, 而成对出现的veth设备分别为veth0和veth1,它们的IP分别是20.1.0.1020.1.0.11

1
2
3
4
5
# ip link add veth0 type veth peer name veth1
# ip addr add 20.1.0.10/24 dev veth0
# ip addr add 20.1.0.11/24 dev veth1
# ip link set veth0 up
# ip link set veth1 up

然后尝试从veth0设备ping另一个设备veth1:

1
2
3
4
5
6
7
# ping -c 2 20.1.0.11 -I veth0
PING 20.1.0.11 (20.1.0.11) from 20.1.0.11 veth0: 28(42) bytes of data.
64 bytes from 20.1.0.11: icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from 20.1.0.11: icmp_seq=2 ttl=64 time=0.052 ms

--- 20.1.0.11 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1500ms

Note: 在有些Ubuntu中有可能ping不通,原因是默认情况下内核网络配置导致veth设备对无法返回ARP返回包。解决办法是:

1
2
3
4
5
# echo 1 > /proc/sys/net/ipv4/conf/veth1/accept_local
# echo 1 > /proc/sys/net/ipv4/conf/veth0/accept_local
# echo 0 > /proc/sys/net/ipv4/conf/veth0/rp_filter
# echo 0 > /proc/sys/net/ipv4/conf/veth1/rp_filter
# echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter

可以尝试使用tcpdump看看在veth设备对上的请求包:

1
2
3
4
5
6
7
# tcpdump -n -i veth1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on veth1, link-type EN10MB (Ethernet), capture size 458122 bytes
20:24:12.220002 ARP, Request who-has 20.1.0.11 tell 20.1.0.10, length 28
20:24:12.220198 ARP, Request who-has 20.1.0.11 tell 20.1.0.10, length 28
20:24:12.221372 IP 20.1.0.10 > 20.1.0.11: ICMP echo request, id 18174, seq 1, length 64
20:24:13.222089 IP 20.1.0.10 > 20.1.0.11: ICMP echo request, id 18174, seq 2, length 64

可以看到在veth1上面只有ICMP echo的请求包,但是没有应答包。仔细想一下,veth1收到ICMP echo请求包后,转交给另一端的协议栈,但是协议栈检查当前的设备列表,发现本地有20.1.0.10,于是构造ICMP echo应答包,并转发给lo设备,lo设备收到数据包之后直接交给协议栈,紧接着给交给用户空间的ping进程。

我们可以尝试使用tcpdump抓取lo设备上的数据:

1
2
3
4
5
# tcpdump -n -i lo
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 458122 bytes
20:25:49.486019 IP IP 20.1.0.11 > 20.1.0.10: ICMP echo reply, id 24177, seq 1, length 64
20:25:50.4861228 IP IP 20.1.0.11 > 20.1.0.10: ICMP echo reply, id 24177, seq 2, length 64

由此可见,对于成对出现的veth设备对,从一个设备出去的数据包会直接发给另外一个设备。在实际的应用场景中,比如容器网络中,成对的veth设备对处于不同的网络命名空间中,数据包的转发在不同网络命名空间之间进行,后续在介绍容器网络的时候会详细说明。

Bridge

bridge一般叫网桥,它也是一种虚拟网络设备,所以具有虚拟网络设备的特征,可以配置IP、MAC地址等。与其他虚拟网络设备不同的是,bridge是一个虚拟交换机,和物理交换机有类似的功能。bridge一端连接着协议栈,另外一端有多个端口,数据在各个端口间转发是基于MAC地址。

bridge可以工作在二层(链路层),也可以工作在三层(IP网路层)。默认工作在二层。默认情况下,其工作在二层,可以在同一子网内的的不同主机间转发以太网报文;当给bridge分配了IP地址,也就开启了该bridge的三层工作模式。在Linux下,你可以用iproute2brctl命令对bridge进行管理。

创建bridge与创建其他虚拟网络设备类似,只需要制定type为bridge:

1
2
# ip link add name br0 type bridge
# ip link set br0 up

network-device-bridge-1.jpg

但是这样创建出来的bridge一端连接着协议栈,其他端口什么也没有连接,因此我们需要将其他设备连接到该bridge才能有实际的功能。

1
2
3
4
5
6
7
8
9
10
# ip link add veth0 type veth peer name veth1
# ip addr add 20.1.0.10/24 dev veth0
# ip addr add 20.1.0.11/24 dev veth1
# ip link set veth0 up
# ip link set veth1 up
# 将veth0连接到br0
# ip link set dev veth0 master br0
# 通过 bridge link 命令可以看到bridge上连接了哪些设备
# bridge link
6: veth0 state UP : <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 2

network-device-bridge-2.jpg

事实上,一旦br0和veth0连接后,它们之间将变成双向通道,但是内核协议栈和veth0之间变成了单通道,协议栈能发数据给veth0,但veth0从外面收到的数据不会转发给协议栈 ,同时br0的MAC地址变成了veth0的MAC地址。我们可以验证一下:

1
2
3
4
5
6
# ping -c 1 -I veth0 20.1.0.11
PING 20.1.0.11 (20.1.0.11) from 20.1.0.10 veth0: 56(84) bytes of data.
From 20.1.0.10 icmp_seq=1 Destination Host Unreachable

--- 20.1.0.11 ping statistics ---
1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms

如果我们使用tcpdump在br0上抓包就会发现:

1
2
3
4
# tcpdump -n -i br0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
21:45:48.225459 ARP, Reply 20.1.0.10 is-at a2:85:26:b3:72:6c, length 28

可以看到veth0收到应答包后没有给协议栈,而是直接转发给br0,这样协议栈得不到veth1的mac地址,从而ping不通。br0在veth0和协议栈之间数据包给拦截了。但是如果我们给br配置IP,会怎么样呢?

1
2
# ip addr del 20.1.0.10/24 dev veth0
# ip addr add 20.1.0.10/24 dev br0

这样,网络结构就变成了下面这样:

network-device-bridge-3.jpg

这时候再通过br0来ping一下veth1,会发现结果可以通:

1
2
3
4
5
6
7
# ping -c 1 -I br0 20.1.0.11
PING 20.1.0.11 (20.1.0.11) from 20.1.0.10 br0: 56(84) bytes of data.
64 bytes from 20.1.0.11: icmp_seq=1 ttl=64 time=0.121 ms

--- 20.1.0.11 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.121/0.121/0.121/0.000 ms

其实当去掉veth0的IP地址,而给br0配置了IP之后,协议栈在路由的时候不会将数据包发给veth0,为了表达更直观,我们协议栈和veth0之间的连接线去掉,这时候的veth0相当于一根网线。

在现实中,bridge常用的使用场景:

虚拟机

典型的虚拟机网络实现就是通过TUN/TAP将虚拟机内的网卡同宿主机的br0连接起来,这时br0和物理交换机的效果类似,虚拟机发出去的数据包先到达br0,然后由br0交给eth0发送出去,这样做数据包都不需要经过host机器的协议栈,运行效率非常高。

network-device-vm.jpg

容器

而对于容器网络来说,每个容器的网络设备单独的网络命名空间中,所以很好地不同容器的协议栈,我们在接下来的笔记中进一步讨论不同的容器实现。

network-device-docker.jpg

IPIP

上一篇笔记中,我们在介绍网络设备的时候了解了一种典型的通过TUN/TAP设备来实现VPN的原理,但是并没有实践TUN/TAP虚拟网络设备具体在linux中怎么发挥实际的功能。这篇笔记我们就来看看在云计算领域中一种非常典型的IPIP隧道如何TUN设备来实现。

IPIP隧道

上一篇笔记中我们也提到了,TUN网络设备能将三层(IP)网络包封装在另外一个三层网络包之中,看起来通过TUN设备发送出来的数据包会像会这样:

1
2
3
4
5
6
MAC: xx:xx:xx:xx:xx:xx
IP Header: <new destination IP>
IP Body:
IP: <original destination IP>
TCP: stuff
HTTP: stuff

这就是典型的IPIP隧道数据包的结构。Linux原生支持好几种不同的IPIP隧道类型,但都依赖于TUN网络设备,我们可以通过命令ip tunnel help来查看IPIP隧道的相关类型以及操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ip tunnel help
Usage: ip tunnel { add | change | del | show | prl | 6rd } [ NAME ]
[ mode { ipip | gre | sit | isatap | vti } ] [ remote ADDR ] [ local ADDR ]
[ [i|o]seq ] [ [i|o]key KEY ] [ [i|o]csum ]
[ prl-default ADDR ] [ prl-nodefault ADDR ] [ prl-delete ADDR ]
[ 6rd-prefix ADDR ] [ 6rd-relay_prefix ADDR ] [ 6rd-reset ]
[ ttl TTL ] [ tos TOS ] [ [no]pmtudisc ] [ dev PHYS_DEV ]

Where: NAME := STRING
ADDR := { IP_ADDRESS | any }
TOS := { STRING | 00..ff | inherit | inherit/STRING | inherit/00..ff }
TTL := { 1..255 | inherit }
KEY := { DOTTED_QUAD | NUMBER }

其中mode代表不同的IPIP隧道类型,Linux原生共支持5种IPIP隧道:

  1. ipip: 普通的IPIP隧道,就是在报文的基础上再封装成一个IPv4报文
  2. gre: 通用路由封装(Generic Routing Encapsulation),定义了在任意一种网络层协议上封装其他任意一种网络层协议的机制,所以对于IPv4和IPv6都适用
  3. sit: sit模式主要用于IPv4报文封装IPv6报文,即IPv6 over IPv4
  4. isatap: 站内自动隧道寻址协议(Intra-Site Automatic Tunnel Addressing Protocol),类似于sit也是用于IPv6的隧道封装
  5. vti: 即虚拟隧道接口(Virtual Tunnel Interface),是一种IPsec隧道技术

还有一些有用的参数: - ttl N 设置进入隧道数据包的TTL为N(N是一个1—255之间的数字,0是一个特殊的值,表示这个数据包的TTL值是继承(inherit)的),ttl参数的缺省值是为inherit - tos T/dsfield T 设置进入通道数据包的TOS域,缺省是inherit - [no]pmtudisc 在这个隧道上禁止或者打开路径最大传输单元发现(Path MTU Discovery),默认打开的

Note: nopmtudisc选项和固定的ttl是不兼容的,如果使用了固定的ttl参数,系统会打开路径最大传输单元发现( Path MTU Discovery)功能

one-to-one

我们首先以最基本的one-to-one的IPIP隧道模式为例来介绍如何在linux中搭建IPIP隧道来实现两个不同子网之间的通信。

开始之前需要注意的是,并不是所有的linux发行版都会默认加载ipip.ko模块,可以通过lsmod | grep ipip查看内核是否加载该模块;若没有则用modprobe ipip先加载;如果一切正常则应该显示:

1
2
3
4
5
6
# lsmod | grep ipip
# modprobe ipip
# lsmod | grep ipip
ipip 20480 0
tunnel4 16384 1 ipip
ip_tunnel 24576 1 ipip

现在就可开始搭建IPIP隧道了,我们的网络拓扑如下图所示:

network-ipip-1.jpg

其中有处于在同一个网段172.16.0.0/16的两台主机A和B,因此可以直接联通。我们需要做的是分别在两台主机上创建两个不同的子网:

Note: 实际上,这两台主机A和B不必要处于同一个子网,只要处于同一个三层网络之中,也就是说能通过三层网络路由得到就可以完成IPIP隧道的搭建。

1
2
A: 10.42.1.0/24
B: 10.42.2.0/24

为了简化,我们先在A节点上创建bridge网络设备mybr0,并且设置IP地址为10.42.1.0/24子网的网关地址,然后启用mybr0

1
2
3
# ip link add name mybr0 type bridge
# ip addr add 10.42.1.1/24 dev mybr0
# ip link set dev mybr0 up

类似地,然后在B节点上分别执行类似的操作:

B:

1
2
3
# ip link add name mybr0 type bridge
# ip addr add 10.42.2.1/24 dev mybr0
# ip link set dev mybr0 up

接下来,我们分别在A和B两台节点上

  1. 创建对应的TUN网络设备
  2. 设置对应的local和remote地址为node节点的可路由地址
  3. 设置对应的网关地址分别为我们即将创建的TUN网络设备
  4. 启用TUN网络设备来创建IPIP隧道

Note: 步骤3是为了节省简化我们创建子网的步骤,直接设置网关地址就可以不用创建额外的网络设备。

A:

1
2
3
4
# modprobe ipip
# ip tunnel add tunl0 mode ipip remote 172.16.232.194 local 172.16.232.172
# ip addr add 10.42.1.1/24 dev tunl0
# ip link set tunl0 up

上面的命令我们创建了新的隧道设备tunl0并且设置了隧道的remotelocal的IP地址,这是IPIP数据包的外层地址;对于内层地址,我们分别设置两个子网地址,这样,IPIP数据包会看起来如下如所示:

network-ipip-2.jpg

B:

1
2
3
4
# modprobe ipip
# ip tunnel add tunl0 mode ipip remote 172.16.232.172 local 172.16.232.194
# ip addr add 10.42.2.1/24 dev tunl0
# ip link set tunl0 up

为了保证我们通过创建的IPIP隧道来访问两个不同主机上的子网,我们需要手动添加如下静态路由:

A:

1
# ip route add 10.42.2.0/24 dev tunl0

B:

1
# ip route add 10.42.1.0/24 dev tunl0

现在主机AB的路由表如下所示:

A:

1
2
3
4
5
# ip route show
default via 172.16.200.51 dev ens3
10.42.1.0/24 dev tunl0 proto kernel scope link src 10.42.1.1
10.42.2.0/24 dev tunl0 scope link
172.16.0.0/16 dev ens3 proto kernel scope link src 172.16.232.172

B:

1
2
3
4
5
# ip route show
default via 172.16.200.51 dev ens3
10.42.1.0/24 dev tunl0 scope link
10.42.2.0/24 dev tunl0 proto kernel scope link src 10.42.2.1
172.16.0.0/16 dev ens3 proto kernel scope link src 172.16.232.194

到此我们就可以开始验证IPIP隧道是否正常工作:

A:

1
2
3
4
5
6
7
8
# ping 10.42.2.1 -c 2
PING 10.42.2.1 (10.42.2.1) 56(84) bytes of data.
64 bytes from 10.42.2.1: icmp_seq=1 ttl=64 time=0.269 ms
64 bytes from 10.42.2.1: icmp_seq=2 ttl=64 time=0.303 ms

--- 10.42.2.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1013ms
rtt min/avg/max/mdev = 0.269/0.286/0.303/0.017 ms

B:

1
2
3
4
5
6
7
8
# ping 10.42.1.1 -c 2
PING 10.42.1.1 (10.42.1.1) 56(84) bytes of data.
64 bytes from 10.42.1.1: icmp_seq=1 ttl=64 time=0.214 ms
64 bytes from 10.42.1.1: icmp_seq=2 ttl=64 time=3.27 ms

--- 10.42.1.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1021ms
rtt min/avg/max/mdev = 0.214/1.745/3.277/1.532 ms

是的,可以ping通,我们通过tcpdump在TUN设备抓取数据:

1
2
3
4
5
6
7
# tcpdump -n -i tunl0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tunl0, link-type RAW (Raw IP), capture size 262144 bytes
01:32:05.486835 IP 10.42.1.1 > 10.42.2.1: ICMP echo request, id 3460, seq 1, length 64
01:32:05.486868 IP 10.42.2.1 > 10.42.1.1: ICMP echo reply, id 3460, seq 1, length 64
01:32:06.509617 IP 10.42.1.1 > 10.42.2.1: ICMP echo request, id 3460, seq 2, length 64
01:32:06.509668 IP 10.42.2.1 > 10.42.1.1: ICMP echo reply, id 3460, seq 2, length 64

到此为止,我们的实验是成功的。但是需要注意的是,如果我们使用的是gre模式,有可能需要设置防火墙才能让两个子网互通,这种情况在搭建IPv6隧道较为常见。

one-to-many

上一节中我们通过指定TUN设备的local地址和remote地址创建了一个one-to-one的IPIP隧道,实际上,在创建IPIP隧道的时候完全可以不指定remote地址,只要在TUN设备上增加对应的路由,IPIP隧道就知道如何封装新的IP数据包并发送到路由指定的目标地址。

还是举个栗子来说明,假设我们现在有处于同一个三层网络的3个节点:

1
2
3
A: 172.16.165.33
B: 172.16.165.244
C: 172.16.168.113

同时在这三个节点上分别attach三个不同的子网:

1
2
3
A: 10.42.1.0/24
B: 10.42.2.0/24
C: 10.42.3.0/24

与上一小节不同的是,我们没有直接将子网的网关地址设置为TUN设备的IP地址,而是创建额外的bridge网络设备以模拟实际常用的容器网络模型。我们在A节点上创建bridge网络设备mybr0,并且设置IP地址为10.42.1.0/24子网的网关地址,然后启用mybr0

1
2
3
# ip link add name mybr0 type bridge
# ip addr add 10.42.1.1/24 dev mybr0
# ip link set dev mybr0 up

类似地,然后在B和C节点上分别执行类似的操作:

B:

1
2
3
# ip link add name mybr0 type bridge
# ip addr add 10.42.2.1/24 dev mybr0
# ip link set dev mybr0 up

C:

1
2
3
# ip link add name mybr0 type bridge
# ip addr add 10.42.3.1/24 dev mybr0
# ip link set dev mybr0 up

我们的最终目标是在三个节点之间分别俩俩搭建IPIP隧道来保证这三个不同的子网直接能够互相通信,因此下一步是创建TUN网络设备并且设置路由信息。分别在A和B两台节点上:

  1. 创建对应的TUN网络设备并启用
  2. 设置TUN网络设备的IP地址
  3. 设置到不同子网的路由,指明下一跳的地址

对应的网关地址分别为我们即将创建的TUN网络设备 4. 启用TUN网络设备来创建IPIP隧道

Note: TUN网络设备的IP地址是对应节点的子网地址,但是子网掩码是32位的,例如A节点上子网地址是10.42.1.0/24,A节点上的TUN网络设备的IP地址是10.42.1.0/32。这样做的原因是有时候同一个子网(例如10.42.1.0/24)的地址会分配相同的MAC地址,因此不能通过二层的链路层直接通信,而如果保证TUN网络设备的IP地址和任何地址都不在同一个子网,也就不存在二层的链路层直接通信了。关于这点请参考calico的实现原理,每个容器会有相同的MAC地址,后面我们有机会在深入探究。

Note: 还有一点需要注意,给TUN网络设备设置路由的时候指定了onlink, 这样做的目的是保证下一跳是直接attach到该TUN网络设备的,这样保证即使节点之间不在同一个子网中也可以搭建IPIP隧道。

A:

1
2
3
4
5
6
# modprobe ipip
# ip tunnel add tunl0 mode ipip
# ip link set tunl0 up
# ip addr add 10.42.1.0/32 dev tunl0
# ip route add 10.42.2.0/24 via 172.16.165.244 dev tunl0 onlink
# ip route add 10.42.3.0/24 via 172.16.168.113 dev tunl0 onlink

B:

1
2
3
4
5
6
# modprobe ipip
# ip tunnel add tunl0 mode ipip
# ip link set tunl0 up
# ip addr add 10.42.2.0/32 dev tunl0
# ip route add 10.42.1.0/24 via 172.16.165.33 dev tunl0 onlink
# ip route add 10.42.3.0/24 via 172.16.168.113 dev tunl0 onlink

C:

1
2
3
4
5
6
modprobe ipip
ip tunnel add tunl0 mode ipip
ip link set tunl0 up
ip addr add 10.42.3.0/32 dev tunl0
ip route add 10.42.1.0/24 via 172.16.165.33 dev tunl0 onlink
ip route add 10.42.2.0/24 via 172.16.165.244 dev tunl0 onlink

到此我们就可以开始验证我们搭建的IPIP隧道是否正常工作:

A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# try to ping IP in 10.42.2.0/24 on Node B
# ping 10.42.2.1 -c 2
PING 10.42.2.1 (10.42.2.1) 56(84) bytes of data.
64 bytes from 10.42.2.1: icmp_seq=1 ttl=64 time=0.338 ms
64 bytes from 10.42.2.1: icmp_seq=2 ttl=64 time=0.302 ms

--- 10.42.2.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1028ms
rtt min/avg/max/mdev = 0.302/0.320/0.338/0.018 ms
...
# try to ping IP in 10.42.3.0/24 on Node C
# ping 10.42.3.1 -c 2
PING 10.42.3.1 (10.42.3.1) 56(84) bytes of data.
64 bytes from 10.42.3.1: icmp_seq=1 ttl=64 time=0.315 ms
64 bytes from 10.42.3.1: icmp_seq=2 ttl=64 time=0.381 ms

--- 10.42.3.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1029ms
rtt min/avg/max/mdev = 0.315/0.348/0.381/0.033 ms

看起来一切正常,如果反过来从B或者C节点分别ping其他子网,也是可以通的。这就说明我们确实可以创建一对多的IPIP隧道,这中one-to-many的模式在一些典型的多节点网络中创建overlay通信模型中非常有用。

under the hood

我们再通过tcpdump在分别在B和C的TUN设备抓取数据:

B:

1
2
3
4
5
6
7
# tcpdump -n -i tunl0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tunl0, link-type RAW (Raw IP), capture size 262144 bytes
22:38:28.268089 IP 10.42.1.0 > 10.42.2.1: ICMP echo request, id 6026, seq 1, length 64
22:38:28.268125 IP 10.42.2.1 > 10.42.1.0: ICMP echo reply, id 6026, seq 1, length 64
22:38:29.285595 IP 10.42.1.0 > 10.42.2.1: ICMP echo request, id 6026, seq 2, length 64
22:38:29.285629 IP 10.42.2.1 > 10.42.1.0: ICMP echo reply, id 6026, seq 2, length 64

C:

1
2
3
4
5
6
7
# tcpdump -n -i tunl0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tunl0, link-type RAW (Raw IP), capture size 262144 bytes
22:36:18.236446 IP 10.42.1.0 > 10.42.3.1: ICMP echo request, id 5894, seq 1, length 64
22:36:18.236499 IP 10.42.3.1 > 10.42.1.0: ICMP echo reply, id 5894, seq 1, length 64
22:36:19.265946 IP 10.42.1.0 > 10.42.3.1: ICMP echo request, id 5894, seq 2, length 64
22:36:19.265997 IP 10.42.3.1 > 10.42.1.0: ICMP echo reply, id 5894, seq 2, length 64

其实,从创建one-to-many的IPIP隧道的过程中我们就能大致猜到Linux的ipip模块基于路由信息获取IPIP包的内部ip然后再用外部IP封装成新的IP包。至于怎么解封IPIP数据包的呢,我们来看看ipip模块收数据包的过程:

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
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
const struct net_protocol *ipprot;
int raw, ret;

resubmit:
raw = raw_local_deliver(skb, protocol);

ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
if (!ipprot->no_policy) {
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
kfree_skb(skb);
return;
}
nf_reset_ct(skb);
}
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
skb);
if (ret < 0) {
protocol = -ret;
goto resubmit;
}
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
} else {
if (!raw) {
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
__IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH,
ICMP_PROT_UNREACH, 0);
}
kfree_skb(skb);
} else {
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
}

From https://github.com/torvalds/linux/blob/master/net/ipv4/ip_input.c#L187-L224

可以看到,ipip模块会根据数据包的协议类型去解封,然后将解封后的skb数据包再做一次解封。以上只是一些非常浅显的分析,如果大家感兴趣,推荐去多看看ipip模块的源代码实现。

IPVLAN

IPVLAN 和 MACVLAN 类似,都是从一个主机接口虚拟出多个虚拟网络接口。一个重要的区别就是所有的虚拟接口都有相同的 mac 地址,而拥有不同的 ip 地址。因为所有的虚拟接口要共享 mac 地址,所以有些需要注意的地方:

  • DHCP 协议分配 ip 的时候一般会用 mac 地址作为机器的标识。这个情况下,客户端动态获取 ip 的时候需要配置唯一的 ClientID 字段,并且 DHCP server 也要正确配置使用该字段作为机器标识,而不是使用 mac 地址

IPVLAN支持两种模式:

  • L2 模式:此时跟macvlan bridge 模式工作原理很相似,父接口作为交换机来转发子接口的数据。同一个网络的子接口可以通过父接口来转发数据,而如果想发送到其他网络,报文则会通过父接口的路由转发出去。
  • L3 模式:此时ipvlan 有点像路由器的功能,它在各个虚拟网络和主机网络之间进行不同网络报文的路由转发工作。只要父接口相同,即使虚拟机/容器不在同一个网络,也可以互相 ping 通对方,因为 ipvlan 会在中间做报文的转发工作。注意 L3 模式下的虚拟接口 不会接收到多播或者广播的报文(这个模式下,所有的网络都会发送给父接口,所有的 ARP 过程或者其他多播报文都是在底层的父接口完成的)。另外外部网络默认情况下是不知道 ipvlan 虚拟出来的网络的,如果不在外部路由器上配置好对应的路由规则,ipvlan 的网络是不能被外部直接访问的。

创建ipvlan的简单方法为

1
ip link add link <master-dev> <slave-dev> type ipvlan mode { l2 | L3 }

cni配置格式为

1
2
3
4
5
6
7
8
9
{
"name": "mynet",
"type": "ipvlan",
"master": "eth0",
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24"
}
}

需要注意的是

  • ipvlan插件下,容器不能跟Host网络通信
  • 主机接口(也就是master interface)不能同时作为ipvlan和macvlan的master接口

MACVLAN

MACVLAN可以从一个主机接口虚拟出多个macvtap,且每个macvtap设备都拥有不同的mac地址(对应不同的linux字符设备)。MACVLAN支持四种模式

  • bridge模式:数据可以在同一master设备的子设备之间转发
  • vepa模式:VEPA 模式是对 802.1Qbg 标准中的 VEPA 机制的软件实现,MACVTAP 设备简单的将数据转发到master设备中,完成数据汇聚功能,通常需要外部交换机支持 Hairpin 模式才能正常工作
  • private模式:Private 模式和 VEPA 模式类似,区别是子 MACVTAP 之间相互隔离
  • passthrough模式:内核的 MACVLAN 数据处理逻辑被跳过,硬件决定数据如何处理,从而释放了 Host CPU 资源

创建macvlan的简单方法为

1
ip link add link <master-dev> name macvtap0 type macvtap

cni配置格式为

1
2
3
4
5
6
7
8
{
"name": "mynet",
"type": "macvlan",
"master": "eth0",
"ipam": {
"type": "dhcp"
}
}

需要注意的是

  • macvlan需要大量 mac 地址,每个虚拟接口都有自己的 mac 地址
  • 无法和 802.11(wireless) 网络一起工作
  • 主机接口(也就是master interface)不能同时作为ipvlan和macvlan的master接口

VLAN

VxLAN

VXLAN 全称是 Virtual eXtensible Local Area Network,虚拟可扩展的局域网。它是一种 overlay 技术,通过三层的网络来搭建虚拟的二层网络。rfc7348 (参考资料1) 上的介绍是这样的:

A framework for overlaying virtualized layer 2 networks over lay 3 networks.

每一个技术出来都有它要解决的问题,VXLAN 也不例外,那么我们先看看 VXLAN 到底要解决哪些问题。

  • 虚拟化(虚拟机和容器)的兴起使得一个数据中心会有成千上万的机器需要通信,而传统的 VLAN 技术只能支持 4096 个网络上限,已经满足不了不断扩展的数据中心规模
  • 越来越多的数据中心(尤其是公有云服务)需要提供多租户的功能,不同用户之间需要独立地分配 ip 和 MAC 地址,如何保证这个功能的扩展性和正确性也是一个待解决的问题
  • 云计算业务对业务灵活性要求很高,虚拟机可能会大规模迁移,并保证网络一直可用,也就是大二层的概念。解决这个问题同时保证二层的广播域不会过分扩大,也是云计算网络的要求

传统二层+三层的网络在应对这些要求时变得力不从心,虽然很多改进型的技术比如堆叠、SVF、TRILL 等能够增加二层的范围,努力改进经典网络,但是要做到对网络改动小同时保证灵活性高却非常困难。

为了解决这些问题,有很多方案被提出来,vxlan 就是其中之一。vxlan 是 VMware、Cisco 等一众大型企业共同推出的,目前标准文档在 RFC7348

VXLAN 模型

vxlan 这类隧道网络的一个特点是对原有的网络架构影响小,原来的网络不需要做任何改动,在原来网络基础上架设一层新的网络。

vxlan 自然会引入一些新的概念,这部分就讲讲它们。下面这张图 是 vxlan 的工作模型,它创建在原来的 IP 网络(三层)上,只要是三层可达(能够通过 IP 互相通信)的网络就能部署 vxlan。在每个端点上都有一个 vtep 负责 vxlan 协议报文的封包和解包,也就是在虚拟报文上封装 vtep 通信的报文头部。物理网络上可以创建多个 vxlan 网络,这些 vxlan 网络可以认为是一个隧道,不同节点的虚拟机能够通过隧道直连。每个 vxlan 网络由唯一的 VNI 标识,不同的 vxlan 可以不相互影响。

img

  • VTEP(VXLAN Tunnel Endpoints):vxlan 网络的边缘设备,用来进行 vxlan 报文的处理(封包和解包)。vtep 可以是网络设备(比如交换机),也可以是一台机器(比如虚拟化集群中的宿主机)
  • VNI(VXLAN Network Identifier):VNI 是每个 vxlan 的标识,是个 24 位整数,一共有 2^24 = 16,777,216(一千多万),一般每个 VNI 对应一个租户,也就是说使用 vxlan 搭建的公有云可以理论上可以支撑千万级别的租户
  • Tunnel:隧道是一个逻辑上的概念,在 vxlan 模型中并没有具体的物理实体想对应。隧道可以看做是一种虚拟通道,vxlan 通信双方(图中的虚拟机)认为自己是在直接通信,并不知道底层网络的存在。从整体来说,每个 vxlan 网络像是为通信的虚拟机搭建了一个单独的通信通道,也就是隧道

现在来说,这些概念还是非常晦涩难理解的,我们会在下面具体讲解 vxlan 网络的报文和通信流程,希望文章结束之后再回来看这些概念能明白它们的意思。

VXLAN 报文解析

前面说过,vxlan 在三层网络上构建一个虚拟的二层网络出来,这一点能够在 vxlan 的报文上很明显地体现出来。

下图是 vxlan 协议的报文,白色的部分是虚拟机发送报文(二层帧,包含了 MAC 头部、IP 头部和传输层头部的报文),前面加了 vxlan 头部用来专门保存 vxlan 相关的内容,在前面是标准的 UDP 协议头部(UDP 头部、IP 头部和 MAC 头部)用来在底层网路上传输报文。

img

从这个报文中可以看到三个部分:

  1. 最外层的 UDP 协议报文用来在底层网络上传输,也就是 vtep之间互相通信的基础
  2. 中间是 VXLAN 头部,vtep 接受到报文之后,去除前面的 UDP 协议部分,根据这部分来处理 vxlan 的逻辑,主要是根据 VNI 发送到最终的虚拟机
  3. 最里面是原始的报文,也就是虚拟机看到的报文内容

报文各个部分的意义如下:

  • VXLAN header:vxlan 协议相关的部分,一共 8 个字节
    • VXLAN flags:标志位
    • Reserved:保留位
    • VNID:24 位的 VNI 字段,这也是 vxlan 能支持千万租户的地方
    • Reserved:保留字段
  • UDP 头部,8 个字节
    • UDP 应用通信双方是 vtep 应用,其中目的端口就是接收方 vtep 使用的端口,IANA 分配的端口是 4789
  • IP 头部:20 字节
    • 主机之间通信的地址,可能是主机的网卡 IP 地址,也可能是多播 IP 地址
  • MAC 头部:14 字节
    • 主机之间通信的 MAC 地址,源 MAC 地址为主机 MAC 地址,目的 MAC 地址为下一跳设备的 MAC 地址

可以看出 vxlan 协议比原始报文多 50 字节的内容,这会降低网络链路传输有效数据的比例。vxlan 头部最重要的是 VNID 字段,其他的保留字段主要是为了未来的扩展,目前留给不同的厂商用这些字段添加自己的功能。

vxlan 网络通信过程

通过上节的内容,我们大致了解 vxlan 报文的发送过程。虚拟机的报文通过 vtep 添加上 vxlan 以及外部的报文层,然后发送出去,对方 vtep 收到之后拆除 vxlan 头部然后根据 VNI 把原始报文发送到目的虚拟机。

上面的过程是双方已经知道所有通信信息的过程,但是在第一次通信之前还有很多问题有解决:

  • 哪些 vtep 需要加到一个相同的 VNI 组?
  • 发送方虚拟机怎么知道对方的 MAC 地址?
  • vtep 怎么知道目的虚拟机在哪一台宿主机上?

这三个问题可以归结为同一个问题:vxlan 网络怎么感知彼此的存在并选择正确的路径传输报文?

而且第一个问题也是不用回答的,因为 vtep 形成的组是虚构的概念,只有某些 vtep 能够正确地传递报文,它们就是在同一个组内。也就是说,我们只要回答后面两个问题就行。

要回答这两个问题,我们还是回到 vxlan 协议报文上,看看一个完整的 vxlan 报文需要哪些信息。

  • 内层报文:通信的虚拟机双方要么直接使用 IP 地址,要么通过 DNS 等方式已经获取了对方的 IP 地址,因此网络层地址已经知道。同一个网络的虚拟机需要通信,还需要知道对方虚拟机的 MAC 地址,vxlan 需要一个机制来实现传统网络 ARP 的功能
  • vxlan 头部:只需要知道 VNI,这一般是直接配置在 vtep 上的,要么是提前规划写死的,要么是根据内部报文自动生成的,也不需要担心
  • UDP 头部:最重要的是源地址和目的地址的端口,源地址端口是系统生成并管理的,目的端口也是写死的,比如 IANA 规定的 4789 端口,这部分也不需要担心
  • IP 头部:IP 头部关心的是 vtep 双方的 IP 地址,源地址可以很简单确定,目的地址是虚拟机所在地址宿主机 vtep 的 IP 地址,这个也需要由某种方式来确定
  • MAC 头部:如果 vtep 的 IP 地址确定了,MAC 地址可以通过经典的 ARP 方式来获取,毕竟 vtep 网络在同一个三层,经典网络架构那一套就能直接用了

总结一下,一个 vxlan 报文需要确定两个地址信息:目的虚拟机的 MAC 地址和目的 vtep 的 IP 地址,如果 VNI 也是动态感知的,那么 vtep 就需要一个三元组:

内部 MAC <—> VNI <—> VTEP IP

根据实现的不同,一般分为两种方式:多播和控制中心。多播的概念是同个 vxlan 网络的 vtep 加入到同一个多播网络,如果需要知道以上信息,就在组内发送多播来查询;控制中心的概念是在某个集中式的地方保存了所有虚拟机的上述信息,自动化告知 vtep 它需要的信息。

针对这两种方式,我们下面就分别分析。

多播

多播的概念和工作原理不是这里的重点,所以就不介绍了。简单来说,每个多播组对应一个多播 IP 地址,往这个多播 IP 地址发送的报文会发给多播组的所有主机。

为什么要使用多播?因为 vxlan 的底层网络是三层的,广播地址无法穿越三层网络,要给 vxlan 网络所有 vtep 发送报文只能通过多播。

下图是在多播模式下,vxlan 的报文工作流程,位于左下方的 机器 A 要通过 vxlan 网络发送报文给右下方的机器 B。

img

vtep 建立的时候会通过配置加入到多播组(具体做法取决于实现),图中的多播组 IP 地址是 239.1.1.1

  1. 机器 A 只知道对方的 IP 地址,不知道 MAC 地址,因此会发送 ARP 报文进行查询,内部的 ARP 报文很普通,目标地址为全 1 的广播地址
  2. vtep 收到 ARP 报文,发现虚拟机目的 MAC 为广播地址,封装上 vxlan 协议头部之后(外层 IP 为多播组 IP,MAC 地址为多播组的 MAC 地址),发送给多播组 239.1.1.1,支持多播的底层网络设备(交换机和路由器)会把报文发送给组内所有的成员
  3. vtep 接收到 vxlan 封装的 ARP 请求,去掉 vxlan 头部,并通过报文学习到发送方 <虚拟机 MAC - VNI - Vtep IP> 三元组保存起来,把原来的 ARP 报文广播给主机
  4. 主机接收到 ARP 请求报文,如果 ARP 报文请求的是自己的 MAC 地址,就返回 ARP 应答
  5. vtep-2 此时已经知道发送放的虚拟机和 vtep 信息,把 ARP 应答添加上 vxlan 头部(外部 IP 地址为 vtep-1 的 IP 地址,VNI 是原来报文的 VNI)之后通过单播发送出去
  6. vtep-1 接收到报文,并学习到报文中的三元组,记录下来。然后 vtep 进行解包,知道内部的 IP 和 MAC 地址,并转发给目的虚拟机
  7. 虚拟机拿到 ARP 应答报文,就知道了到目的虚拟机的 MAC 地址

在这个过程中,只有一次多播,因为 vtep 有自动学习的能力,后续的报文都是通过单播直接发送的。可以看到,多播报文非常浪费,每次的多播其实只有一个报文是有效的,如果某个多播组的 vtep 数量很多,这个浪费是非常大的。但是多播组也有它的实现起来比较简单,不需要中心化的控制,只有底层网络支持多播,只有配置好多播组就能自动发现了。

单播报文的发送过程就是上述应答报文的逻辑,应该也非常容易理解了。还有一种通信方式,那就是不同 VNI 网络之间的通信,这个需要用到 vxlan 网关(可以是物理网络设备,也可以是软件),它接收到一个 vxlan 网络报文之后解压,根据特定的逻辑添加上另外一个 vxlan 头部转发出去。

因为并不是所有的网络设备都支持多播,再加上多播方式带来的报文浪费,在实际生产中这种方式很少用到。

分布式控制中心

从多播的流程可以看出来,其实 vtep 发送报文最关键的就是知道对方虚拟机的 MAC 地址和虚拟机所在主机的 vtep IP 地址。如果能够事先知道这两个信息,直接告诉 vtep,那么就不需要多播了。

在虚拟机和容器的场景中,当虚拟机或者容器启动还没有进行网络通讯时,我们就可以知道它的 IP 和 MAC(可能是用某种方式获取,也有可能是事先控制这两个地址),分布式控制中心保存了这些信息。除此之外,控制中心还保存了每个 vxlan 网络有哪些 vtep,这些 vtep 的地址是多少。有了这些信息,vtep 就能发送报文时直接查询并添加头部,不需要多播去满网络地问了。

一般情况下,在每个 vtep 所在的节点都会有一个 agent,它会和控制中心通信,获取 vtep 需要的信息以某种方式告诉 vtep。具体的做法取决于具体的实现,每种实现可能会更新不同的信息给 vtep,比如 HER(Head End Replication)只是把多播组替换成多个单播报文,也就是把多播组所有的 VTEP IP 地址告诉 vtep,这样查询的时候不是发送多播,而是给组内每个 vtep 发送一个单播报文;有些实现只是告诉 vtep 目的虚拟机的 MAC 地址信息;有些实现告诉 MAC 地址对应的 vtep IP 地址。

此外,什么时候告诉 vtep 这些信息也是有区别的。一般有两种方式:常见的是一旦知道了虚拟机的三元组信息就告诉 vtep(即使某个 vtep 用不到这个信息,因为它管理的虚拟机不会和这个地址通信),一般这时候第一次通信还没有发生;另外一种方式是在第一次通信时,当 vtep 需要这些信息的时候以某种方式通知 agent,然后 agent 这时候才告诉 vtep 信息。

分布式控制的 vxlan 是一种典型的 SDN 架构,也是目前使用最广泛的方式。因为它的实现多样,而且每种实现都有些许差距,这里不便来具体的例子来说明,只要明白了上面的原理,不管是什么样的实现,都能很快上手。

vxlan 网络带来新的问题

vxlan 协议给虚拟网络带来了灵活性和扩展性,让云计算网络能够像计算、存储资源那样按需扩展,并灵活分布。和计算机领域所有技术一样,这也是一种 tradeoff,相对于经典网络来说,vxlan 主要的问题是它的复杂性和额外的开销。

额外的报文和计算

这一点可容易看出来,每个 vxlan 报文都有额外的 50 字节开销,如果加上 vlan 字段,开销要到 54 字节。这对于小报文的传输是非常昂贵的操作,试想如果某个报文应用数据才几个字节,原来的网络头部加上 vxlan 报文头部都能有 100 字节的控制信息。

额外的报文也带来了额外的计算量,每个 vxlan 报文的封包和解包操作都是必须的,如果用软件来实现这些步骤,额外的计算量也是不可以忽略的影响。

复杂度

vxlan 另外一个缺点是复杂度,虽然经典网络在应对云计算时捉紧见拙,但是经典网络模型已经发展了很久,所有的部署、监控、运维都比较成熟。如果使用 vxlan 网络,那么所有的这些都要重新学习,时间和人力成本必然会大大提高。

MACVLAN

macvlan 本身是 linxu kernel 模块,其功能是允许在同一个物理网卡上配置多个 MAC 地址,即多个 interface,每个 interface 可以配置自己的 IP。macvlan 本质上是一种网卡虚拟化技术(最大优点是性能极好)

可以在linux命令行执行lsmod | grep macvlan 查看当前内核是否加载了该driver;如果没有查看到,可以通过modprobe macvlan来载入,然后重新查看。内核代码路径 /drivers/net/macvlan.c

Macvlan 允许你在主机的一个网络接口上配置多个虚拟的网络接口,这些网络 interface 有自己独立的 MAC 地址,也可以配置上 IP 地址进行通信。Macvlan 下的虚拟机或者容器网络和主机在同一个网段中,共享同一个广播域。Macvlan 和 Bridge 比较相似,但因为它省去了 Bridge 的存在,所以配置和调试起来比较简单,而且效率也相对高。除此之外,Macvlan 自身也完美支持 VLAN

工作模式(Bridge VS MACVlan)

Bridge Mode

  • Bridge 是二层设备,仅用来处理二层的通讯。
  • Bridge 使用 MAC 地址表来决定怎么转发帧(Frame)。
  • Bridge 会从 host 之间的通讯数据包中学习 MAC 地址。
  • 可以是硬件设备,也可以是纯软件实现(例如:Linux Bridge)。

提示:

Bridge 有可能会遇到二层环路,如有需要,可以开启 STP 来防止出现环路。

MACVlan Mode

  • 可让使用者在同一张实体网卡上设定多个 MAC 地址。
  • 上述设定的 MAC 地址的网卡称为子接口(sub interface);而实体网卡则称为父接口(parent interface)。
  • parent interface 可以是一个物理接口(eth0),可以是一个 802.1q 的子接口(eth0.10),也可以是 bonding 接口。
  • 可在 parent/sub interface 上设定的不只是 MAC 地址,IP 地址同样也可以被设定。
  • sub interface 无法直接与 parent interface 通讯 (带有 sub interface 的 VM 或容器无法与 host 直接通讯)。
  • 若 VM 或容器需要与 host 通讯,那就必须额外建立一个 sub interface 给 host 用。
  • sub interface 通常以 mac0@eth0 的形式来命名以方便区別。

MACVlan模式

  • Bridge:属于同一个parent接口的macvlan接口之间挂到同一个bridge上,可以二层互通(macvlan接口都无法与parent 接口互通)。
  • VPEA(Virtual Ethernet Port Aggregator):所有接口的流量都需要到外部switch才能够到达其他接口。
  • Private:接口只接受发送给自己MAC地址的报文。
  • Passthru: 父接口和相应的MacVLAN接口捆绑在一起,这种模式每个父接口只能和一个 Macvlan 虚拟网卡接口进行捆绑,并且 Macvlan 虚拟网卡接口继承父接口的 MAC 地址。

前三种模式示意图:

image

实验

Bridge Mode:创建了两个macvlan接口,分别放到两个netns中;然后验证这两个macvlan口之间客户互通。

设置网卡混杂模式(PROMISC网卡混杂标志):

1
2
3
[root@localhost ~]#ifconfig ens224 promisc
[root@localhost ~]# ifconfig ens224
ens224: flags=4419<UP,BROADCAST,RUNNING,**PROMISC**,MULTICAST> mtu 1500

创建两个macvlan接口,其parent接口都是ens224

1
2
[root@localhost ~]# ip link add link ens224 name macv1 type macvlan mode bridge
[root@localhost ~]# ip link add link ens224 name macv2 type macvlan mode bridge

查看接口状态:

image

创建namespace
[root@localhost ~]# ip netns add net1
[root@localhost ~]# ip netns add net2

将macvlan接口插入到namespace
[root@localhost ~]# ip link set macv1 netns net1
[root@localhost ~]# ip link set macv2 netns net2

设置网卡IP,设置网卡UP状态
ip netns exec net1 ip addr add 52.1.1.151/24 dev macv1
ip netns exec net2 ip addr add 52.1.1.152/24 dev macv2

设置网卡IP,设置网卡UP状态
[root@localhost ~]#ip netns exec net1 ip addr add 52.1.1.151/24 dev macv1
[root@localhost ~]#ip netns exec net2 ip addr add 52.1.1.152/24 dev macv2
[root@localhost ~]#ip netns exec net1 ip link set macv1 up
[root@localhost ~]#ip netns exec net2 ip link set macv2 up

查看网卡状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost ~]# ip netns exec net1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: macv1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether 4e:a1:51:a9:3f:ef brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 52.1.1.151/24 scope global eth0
valid_lft forever preferred_lft forever
[root@localhost ~]# ip netns exec net2 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: macv2@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether fe:b5:85:c1:77:c1 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 52.1.1.152/24 scope global macv2
valid_lft forever preferred_lft forever

ping测试结果:
net1和net2无法ping通宿主机,ping自己也不通.2个容器互相ping没有问题

更改容器内网卡名字

ip netns exec net1 ip link set mac1 name eth0

IPVLAN

IPVlan 和 macvlan 类似,都是从一个主机接口虚拟出多个虚拟网络接口。一个重要的区别就是所有的虚拟接口都有相同的 macv 地址,而拥有不同的 ip 地址。因为所有的虚拟接口要共享 mac 地址,所有有些需要注意的地方:

  • DHCP 协议分配 ip 的时候一般会用 mac 地址作为机器的标识。这个情况下,客户端动态获取 ip 的时候需要配置唯一的 ClientID 字段,并且 DHCP server 也要正确配置使用该字段作为机器标识,而不是使用 mac 地址

Ipvlan 是 linux kernel 比较新的特性,linux kernel 3.19 开始支持 ipvlan,但是比较稳定推荐的版本是 >=4.2(因为 docker 对之前版本的支持有 bug),具体代码见内核目录:/drivers/net/ipvlan/

ipvlan模式

L2:

ipvlan L2 模式和 macvlan bridge 模式工作原理很相似,父接口作为交换机来转发子接口的数据。同一个网络的子接口可以通过父接口来转发数据,而如果想发送到其他网络,报文则会通过父接口的路由转发出去。

L3:

ipvlan 有点像路由器的功能,它在各个虚拟网络和主机网络之间进行不同网络报文的路由转发工作。只要父接口相同,即使虚拟机/容器不在同一个网络,也可以互相 ping 通对方,因为 ipvlan 会在中间做报文的转发工作。

模式架构图:

image

实验

创建IPVlan L3模式
[root@localhost ~]#ip link add link ens224 ipvlan1 type ipvlan mode l3
[root@localhost ~]#ip link add link ens224 ipvlan2 type ipvlan mode l3

注意看ipvlan1 和ipvlan2 的MAC地址跟ens224的一样
[root@localhost ~]# ip link
1: lo: 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: ens192: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:05:18:ac brd ff:ff:ff:ff:ff:ff
3: ens224: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:05:18:b6 brd ff:ff:ff:ff:ff:ff
4: ipvlan1@enp0s3: mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 00:0c:29:05:18:b6 brd ff:ff:ff:ff:ff:ff
5: ipvlan2@enp0s3: mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 00:0c:29:05:18:b6 brd ff:ff:ff:ff:ff:ff

创建ns绑定接口

[root@localhost ~]#ip net add net-1
[root@localhost ~]#ip net add net-2
[root@localhost ~]#ip link set ipvlan1 netns net-1
[root@localhost ~]#ip link set ipvlan2 netns net-2

配置IP
[root@localhost ~]#ip net exec net-1 ip addr add 10.0.2.18/24 dev ipvlan1
[root@localhost ~]#ip net exec net-2 ip addr add 10.0.3.19/24 dev ipvlan2
[root@localhost ~]#ip net exec net-1 ip link set ipvlan1 up
[root@localhost ~]#ip net exec net-2 ip link set ipvlan2 up

增加路由
[root@localhost ~]#ip net exec net-1 route add default dev ipvlan1
[root@localhost ~]#ip net exec net-2 route add default dev ipvlan2

ping测试
2个ns可以正常互相ping通,无法ping通宿主机IP

抓取ARP报文,结果无法在L3模式中抓到ARP,说明二层广播和组播都不处理,工作在L3.(这就是和L2模式的区别)
[root@localhost ~]#ip net exec net-1 tcpdump -ni ipvlan1 -p arp

创建L2模式,其余操作跟L3一样
ip link add link enp0s3 ipvlan1 type ipvlan mode l2
ip link add link enp0s3 ipvlan2 type ipvlan mode l2

区别在于L2可以在2个ns中抓取到ARP报文

总结:
ipvlan L3模式中外部网络默认情况下是不知道 ipvlan 虚拟出来的网络的,如果不在外部路由器上配置好对应的路由规则,ipvlan 的网络是不能被外部直接访问的。

参考资料