0%

网络虚拟化

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

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

传统网络架构

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

img

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

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

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

虚拟化网络架构

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

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

img

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

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

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

img

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

NAT

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

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

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

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来区分。

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

VxLAN

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模块的源代码实现。

参考资料