ljzsdut
GitHubToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage
Edit page

1 Calico网络通信原理揭秘 Pod流量如何进入宿主机网卡

Calico 网络通信原理揭秘

Calico 是一个纯三层的数据中心网络方案,而且无缝集成像 OpenStack 这种 Iaas 云架构,能够提供可控的 VM、容器、裸机之间的 IP 通信。为什么说它是纯三层呢?因为所有的数据包都是通过路由的形式找到对应的主机和容器的,然后通过BGP协议来将所有路由同步到所有的机器或数据中心,从而完成整个网络的互联。

简单来说,Calico在主机上创建了一堆的veth pair,其中一端在主机上,另一端在容器的网络命名空间里,然后在容器主机中分别设置几条路由,来完成网络的互联。

1.Calico网络模型揭秘

下面我们通过具体的例子来帮助大家理解Calico网络的通信原理。任意选择 k8s 集群中的一个节点作为实验节点,进入容器 A,查看容器 A 的 IP 地址:

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
3: eth0@if771: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1440 qdisc noqueue state UP
    link/ether 66:fb:34:db:c9:b4 brd ff:ff:ff:ff:ff:ff
    inet 172.17.8.2/32 scope global eth0
       valid_lft forever preferred_lft forever

这里容器获取的是 /32 位主机地址,表示将容器 A 作为一个单点的局域网,没有其他IP与其在同一个子网中。所以直接查询默认路由,将二层转换为三层。

瞄一眼容器 A 的默认路由:

$ ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link

现在问题来了,从路由表可以知道 169.254.1.1 是容器的默认网关,但却找不到任何一张网卡对应这个 IP 地址,169.254.1.1是一个虚拟的不存在的IP,这是个什么鬼?谁会响应这个IP的arp请求呢?

莫慌,先回忆一下,当一个数据包的目的地址不是本机时,就会查询路由表,从路由表中查到网关IP后,它首先会通过 ARP 获得网关的MAC地址,然后在发出的网络数据包中将目标MAC改为网关的MAC,而**网关的IP地址不会出现在任何网络包头中**。也就是说,没有人在乎这个IP地址究竟是什么,只要能找到网关IP对应的MAC地址,能响应ARP就行了。

想到这里,我们就可以继续往下进行了,可以通过 ip neigh 命令查看一下本地的 ARP 缓存:

# 容器内
$ ip neigh
169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee REACHABLE

这个MAC地址应该是Calico硬塞进去的,而且还能响应 ARP。但它究竟是怎么实现的呢?

我们先来回想一下正常情况,内核会对外发送ARP请求,询问整个二层网络中谁拥有 169.254.1.1 这个 IP 地址,拥有这个IP地址的设备会将自己的 MAC地址返回给对方。但现在的情况比较尴尬,容器和主机都没有这个IP地址,甚至连主机上的端口 calicba2f87f6bb,MAC地址也是一个无用的 ee:ee:ee:ee:ee:ee。按道理容器和主机网络根本就无法通信才对呀!所以Calico是怎么做到的呢?

这里我就不绕弯子了,实际上Calico利用了网卡的代理ARP功能。代理ARP是ARP协议的一个变种,当ARP请求目标跨网段时,网关设备收到此ARP请求,会用自己的MAC地址返回给请求者,这便是代理ARP(Proxy ARP)。

在开启了proxy_arp的情况下,如果接收到的arp请求中的ip地址不是本机网卡接口的地址,但是有该地址的路由(即目标IP在ARP代理主机上路由可达,或者说ARP代理主机可以访问目标IP。即使用ip route get时有返回条目),则会以自己的MAC地址进行回复;如果没有该地址的路由,不回复。

如果不开启proxy_arp,网卡只会响应自己具有的IP的arp请求。

举个例子:

img

上面这张图中,Router可以访问8.8.8.8(可能是通过自己的默认网关),当PC发送ARP请求服务器 8.8.8.8 的MAC地址,路由器(网关)收到这个arp请求时会进行判断,~~由于目标8.8.8.8不属于192.168.1.0/24网段(即跨网段)~~由于自己开启Porxy ARP功能且可以访问8.8.8.8这个IP,此时便返回自己的接口MAC地址给PC,后续电脑访问服务器时,目标MAC直接封装为MAC254。参考链接

现在我们知道,Calico本质上还是利用了代理ARP撒了一个“善意的谎言”,下面我们来确认一下。

查看宿主机的网卡信息和路由信息:

$ ip addr
...
771: calicba2f87f6bb@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 14  #MAC地址是无用的ee:ee:ee:ee:ee:ee
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link    #没有配置IPv4地址(仅仅用作二层设备)
       valid_lft forever preferred_lft forever
...

$ ip route 
...
172.17.8.2 dev calicba2f87f6bb scope link  #二层条目,发送到容器172.17.8.2的报文直接发送的calicba2f87f6bb设备(该设备可以理解为是通往172.17.8.2的网关)
...

查看是否开启代理 ARP:

$ cat /proc/sys/net/ipv4/conf/calicba2f87f6bb/proxy_arp
1

如果还不放心,可以通过tcpdump抓包验证一下:

$ tcpdump -i calicba2f87f6bb -e -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on calicba2f87f6bb, link-type EN10MB (Ethernet), capture size 262144 bytes


14:27:13.565539 ee:ee:ee:ee:ee:ee > 0a:58:ac:1c:ce:12, ethertype IPv4 (0x0800), length 4191: 10.96.0.1.443 > 172.17.8.2.36180: Flags [P.], seq 403862039:403866164, ack 2023703985, win 990, options [nop,nop,TS val 331780572 ecr 603755526], length 4125
14:27:13.565613 0a:58:ac:1c:ce:12 > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 66: 172.17.8.2.36180 > 10.96.0.1.443: Flags [.], ack 4125, win 2465, options [nop,nop,TS val 603758497 ecr 331780572], length 0

总结:

  1. Calico通过一个巧妙的方法将workload的所有流量引导到一个特殊的网关169.254.1.1,从而引流到主机的calixxx网络设备上,最终将二三层流量全部转换成三层流量来转发。
  2. 在主机上通过开启代理ARP功能来实现ARP应答,使得ARP广播被抑制在主机上,抑制了广播风暴,也不会有ARP表膨胀的问题。

2. 模拟组网

既然我们已经掌握了Calico的组网原理,接下来就可以手动模拟验证了。架构如图所示:

img

先在 Host0 上执行以下命令:

# 创建veth-pair设备和netns,将eth0加入到网络名称空间ns0中
$ ip link add veth0 type veth peer name eth0
$ ip netns add ns0
$ ip link set eth0 netns ns0

# ns0中配置网卡的IP和路由
$ ip netns exec ns0 ip a add 10.20.1.2/24 dev eth0  #会自动生成二层路由:10.20.1.0/24 dev eth0 proto kernel scope link src 10.20.1.2,用于与同网段的其他IP进行二层路由通信
$ ip netns exec ns0 ip link set eth0 up
$ ip netns exec ns0 ip route add 169.254.1.1 dev eth0 scope link  # scope link表示在二层中路由报文,匹配到该类型的路由条目后需要再查询arp表或发送arp请求获取MAC地址
$ ip netns exec ns0 ip route add default via 169.254.1.1 dev eth0
# 以上2条指令可是使用ip netns exec ns0 ip route add default dev eth0 [scope link]代替,用于实现与其他网段通信的默认路由。可选,如果不需要访问其他网段的需求,可以不设置。报文封装过程:请求报文在封IP包时匹配到该条目后,发现是scope link条目,会再去查询arp表或发送arp请求获取目标MAC,获取到目标MAC地址后,便使用目标MAC进行链路层头部的封装。

# 宿主机内配置网卡的IP和路由
$ ip link set veth0 up
$ ip route add 10.20.1.2 dev veth0 scope link  #本主机上的容器
$ ip route add 10.20.1.3 via 192.168.1.16 dev ens192  #其他主机上的容器。next-hop为其他主机的IP

# 宿主机启用代理ARP
$ echo 1 > /proc/sys/net/ipv4/conf/veth0/proxy_arp

在 Host1 上执行以下命令:

$ ip link add veth0 type veth peer name eth0
$ ip netns add ns1
$ ip link set eth0 netns ns1

$ ip netns exec ns1 ip a add 10.20.1.3/24 dev eth0
$ ip netns exec ns1 ip link set eth0 up
$ ip netns exec ns1 ip route add 169.254.1.1 dev eth0 scope link
$ ip netns exec ns1 ip route add default via 169.254.1.1 dev eth0

$ ip link set veth0 up
$ ip route add 10.20.1.3 dev veth0 scope link
$ ip route add 10.20.1.2 via 192.168.1.32 dev ens192

$ echo 1 > /proc/sys/net/ipv4/conf/veth0/proxy_arp

网络连通性测试:

# Host0:10.20.1.2 ping  10.20.1.3
$ ip netns exec ns0 ping 10.20.1.3
PING 10.20.1.3 (10.20.1.3) 56(84) bytes of data.
64 bytes from 10.20.1.3: icmp_seq=1 ttl=62 time=0.303 ms
64 bytes from 10.20.1.3: icmp_seq=2 ttl=62 time=0.334 ms

实验成功!

具体的转发过程如下:

  1. ns0网络空间的所有数据包都转发到一个虚拟的IP地址169.254.1.1,发送ARP请求。
  2. Host0的veth端收到ARP请求时通过开启网卡的代理ARP功能直接把自己的MAC地址返回给ns0。
  3. ns0发送目的地址为ns1的IP数据包。
  4. Host0接收到的数据包,判断目标MAC为自己,但目标IP为10.20.1.3,非本机IP,判断为三层路由转发,查询本地路由 10.20.1.3 via 192.168.1.16 dev ens192 发送给对端Host1,如果配置了BGP,这里就会看到proto协议为 BIRD
  5. 当Host1收到10.20.1.3的数据包时,判断目标MAC为自己,但目标IP为10.20.1.3,非本机IP,匹配本地的路由表 10.20.1.3 dev veth0 scope link,将数据包转发到对应的 veth0 端,从而到达 ns1。
  6. 回程类似

通过这个实验,我们可以很清晰地掌握Calico网络的数据转发流程,首先需要给所有的 ns 配置一条特殊的路由,并利用 veth 的代理 ARP 功能让 ns 出来的所有转发都变成三层路由转发,然后再利用主机的路由进行转发。这种方式不仅实现了同主机的二三层转发,也能实现跨主机的转发。

FAQ

1、为什么宿主机上的所有calixxx网卡的mac地址是ee:ee:ee:ee:ee:ee,却不影响同节点Pod间通信?

官方解释

因为宿主机上为每个Pod的IP以32位掩码的形式,写入一条路由条目。从而可以通过三层路由找到目标网卡。

以PodA请求PodB为例。

PodA的3层报文要封装2层数据帧头部(源MAC为PodA内eth0网卡的MAC,目标MAC为caliB网卡的地址ee:ee:ee:ee:ee:ee,arp请求目标MAC由caliA网卡的arp_proxy代答),发送的数据帧到达caliA网卡,协议栈的链路层发现MAC地址为自己的MAC,表明是发往自己的数据帧。然后剥掉数据帧头部,进入协议栈的IP层,通过查询路由表,发现目标IP不是自己,而是发送至caliB网卡转发出去,所以通过ip_forward将报文在三层送至caliB网卡进行封装数据帧头部(源MAC为caliB网卡的地址ee:ee:ee:ee:ee:ee,目标MAC为PodB内eth0网卡的MAC,arp请求会被PodB内的eth0网卡应答),然后到达PodB中。

整个过程中发现,宿主机内部封装数据帧时,并没有使用ee:ee:ee:ee:ee:ee的MAC地址作为目标MAC来发送至下一跳;而ee:ee:ee:ee:ee:ee作为目标MAC是在Pod内部封装数据帧时使用,而Pod内ee:ee:ee:ee:ee:ee这个MAC地址是唯一的。所以所有calixxx网卡的mac地址是ee:ee:ee:ee:ee:ee不会影响通信。

个人理解???无论是2层头部还是3层头部,封包工作由内核完成,不是由网卡完成。网卡完成电信号的传输。

在 Linux 内核实现中,链路层协议靠网卡驱动来实现,内核协议栈来实现网络层和传输层。内核对更上层的应用层提供 socket 接口来供用户进程访问。