基于veth的网络虚拟化

Time: 二月 2, 2016
Category: docker & lxc, kubernetes, namespaces, tcp/ip internals

关于Network Namespace的原理不再详解,请直接移步:Namespaces in operation, part 7: Network namespaces 

但是需要注意的,这个文章里network namespace操作所使用的是最新内核&操作系统提供的非常便利的ip netns工具,不过这些工具在低版本的操作系统上都是不提供的。如果真的需要使用network namespace,最好通过netlink编程的方式来实现,直接基于操作系统调用来完成所有设备的虚拟化工作
我们知道在clone进程的时候使用CLONE_NEWNET参数可以创建一个新的独立的network namespace,但是光有这个还是远远不够的,所有网络设备都没有初始化、没启动,这个时候的容器就是一个完全的离线的容器,不在任何网络里,也访问不了任何网络。 

为了让容器独立能够与外网接通,我们需要创建并初始化一些设备,让容器内的网络和外网互通,veth是一种比较简单的方案

veth网络的原理图如下,当创建容器时,为每个容器创建一个veth-pair设备,veth0作为容器内的eth0网卡,veth1桥接到br0,通过br0来解决同宿主机跨容器间的网络访问问题。由于br0网桥使用的是虚拟ip,虚拟ip是不被外网识别,不被路由的,因此我们需要一些iptables规则将br0发出的数据包伪装成本机发送出的数据包,从而打通容器内到外网的网络

在基于veth的容器虚拟网络模型里:

  1. 容器是一个虚拟的网络节点,拥有独立的网络设施:lo设备、eth0网卡、ip地址、iptables防火墙规则等等
  2. 容器能够访问网络
  3. 除非通过端口映射的方式来暴露容器特定的端口外,外网一律无法访问容器。这个是当前这个方案的局限性

1. veth-pair设备

veth设备全称为Virtual Enternet device,veth主要的目的是为了跨Network Namespace之间提供一种类似于Linux进程间通信的技术,所以veth总是成对出现的,例如veth0@veth1等等,其中veth0在一个Network Namespace内,而另一个veth1在另外一个Network Namespace,其中往veth设备上任意一端上RX到的数据,都会在另一端上以TX的方式发送出去,veth工作在L2数据链路层,veth-pair设备在转发数据包过程中并不串改数据包内容

显然,仅有veth-pair设备,容器是无法访问网络的。因为容器发出的数据包,实质上直接进入了veth1设备的协议栈里。如果容器需要访问网络,需要使用bridge等技术,将veth1接收到的数据包通过某种方式转发出去

2 Linux bridge网桥

网桥是用于连接两个不同网段的常见手段,不同网段通过网桥连接后就如同在一个网段一样,它的工作原理很简单,就是在L2数据链路层进行数据包转发

网桥和路由很相似,都可以用来分发网络数据包,当时他们本质上还是有很大不同的:路由工作在L3网络层,使用路由协议,但网桥是工作在L2数据链路层的,它不使用路由协议,而是通过学习和缓存在链路上传输的数据包中的源地址以及物理层的输入端口:

  1. 收到新数据包时,记录源MAC地址和输入端口
  2. 根据数据包中的目的MAC地址查找本地缓存,如果能找到对应的MAC地址记录
  3. 若发现记录不在本地网段,直接丢弃数据包
  4. 若发现记录存在对应的端口,则将数据包直接从该端口转发出去
  5. 如果本地缓存中找不到任何记录,则在本网段内进行广播

基本原理图如下:

因此,通过Linux网桥来实现打通容器网络是一个非常有效的方法,通过bridge,我们能做到:

  1. 连接同宿主机内所有容器的虚拟网络
  2. 打通容器内网与外网,通过bridge将数据转发到真实的物理网卡eth0

如上图所示,我们有两个容器A和B,A容器在独立的Network ns虚拟内,B容器在独立的Network ns虚拟网络内,默认情况下,如果没有bridge设备只有veth的话,容器只能够与宿主机器进行通信,容器之间无法通信,容器与外网也无法通信。

但是通过bridge,我们就能同时解决这两个问题,ns1中的veth设备连接ns1与Host,而ns2中的veth设备用来连接ns2与Host,将ns1中的veth-pair设备对端桥接到bridge0上,同时将ns2中的veth-pair设备对端桥接到bridge0上,由bridge0负责两个ns之间以及ns与host之间的通信

3. 网络设备的初始化

3.1 虚拟网卡IP

创建一个容器时,根据bridge所在的网段,为虚拟网卡分配一个独立的IP地址即可

3.2 虚拟网络内的路由策略

默认情况下,Linux内核在给网络接口设定IP地址时,会创建一个特定路由策略,该路由策略会明确确定从本机发往同网段的数据包通过当前网络接口发送出去。虚拟设备指定IP地址后的容器内的第一条路由规则如下:

# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
172.27.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

但是这条规则只能路由bridge0网段内的数据包,也就是说,只能完成bridge0下同网段内容器之间的数据通信,对于其他目的地址的数据包是不能够被路由的,因为内核根本不知道通过哪个网卡发送出去。所以我们还需要增加一条默认路由规则,该默认路由声明:当路由表中找不到任何路由规则可以路由当前数据包时,数据包将依据默认的路由策略转发出去

# route add -net 0.0.0.0 gw 172.27.0.1
# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
172.27.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0
0.0.0.0         172.27.0.1      0.0.0.0         UG    0      0        0 eth0

3.3 iptables防火墙规则

veth0设备和bridge0网桥是打通容器虚拟网络与外网必要的基础设施,但是这些网络设备地址都是虚拟,这些虚拟地址发出的数据包在真实的网络上是不可寻址和不可被路由的:

换句话说,你的数据包能够发送出去,因为数据包中的目的地址是真实存在的,可被路由和寻址的,然后当目的主机发送响应数据包时,由于响应数据包中的目的地址指向了一个虚拟网络,真实网络上的网络设备会以Destination Host Unreachable为由直接丢弃

因此,我们需要将容器内虚拟网络发出的数据包伪装成本机的网络数据包发送出去,当接收到响应的数据包时,再将数据包转换回来。这就是NAT的数据伪装技术,NAT的基本工作原理是,当私有网主机和公共网主机通信的IP包经过NAT网关时,将IP包中的源IP或目的IP在私有IP和NAT的公共IP之间进行转换


Leave a Comment