Calico网络中的ProxyARP

如果K8s使用Calico作为网络方案的话,应该都会知道Calico是个纯3层的方案,也是就说,所有的数据包,都是通过路由的形式找到对应机器和容器的,然后通过BGP协议来将所有的路由同步到所有的机器或者数据中心,来完成整个网络的互联。
简单的来说,Calico针对一个容器,在主机上创建了一堆veth pair,其中一端在主机,一端在容器的网络空间里,然后在主机和容器中分别设置几条路由,来完成网络的互联,我们可以看一个例子:

主机上:

$ ip addr
...
771: cali45b9132fec1@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
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever
...

$ ip route 
...
10.218.240.252 dev cali45b9132fec1 scope link
...

容器里:

$ 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 10.218.240.252/32 scope global eth0
       valid_lft forever preferred_lft forever

$ ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0

按照上面的逻辑,可以理一下:

  • 当目的地址是10.218.240.252的数据包,也就是目的是容器的数据包,到达主机,主机根据10.218.240.252 dev cali45b9132fec1 scope link这条路由,将数据包丢给cali45b9132fec1这个veth
    ,然后容器中对应的eth0就可以收到数据包了。
  • 当容器中的数据包需要发出,就是走默认路由,也就是default via 169.254.1.1 dev eth0,将数据包丢给eth0,这时主机对应的cali45b9132fec1可以收到包,然后继续进行路由选择,转发到对应端口。

这么一看好像没什么问题,但是总觉得不对,为什么容器里的默认网关是169.254.1.1呢?二层是怎么处理的?

我们重新思考一下数据包的传输:
当一个数据包的目的地址不是本机,所以需要查询路由表,当查到路由表中的网关之后,需要获取网关的MAC地址,并将数据包的MAC地址修改成网关地址,然后发送到对应的网卡。

问题来了。在容器里的网关是169.254.1.1,那网关的MAC地址是什么?
正常情况下,内核会对外发送ARP请求,去询问整个二层网络中谁拥有169.254.1.1这个IP地址,拥有这个IP地址的设备会将自己的MAC返回。
但是现在的情况是,对于容器和主机,都没有169.254.1.1这个IP,甚至,在主机上的端口cali45b9132fec1,MAC地址也是一个无用的ee:ee:ee:ee:ee:ee。所以,如果仅仅是目前的状况,容器和主机网络根本就无法通信!
所以Calico是怎么做到的呢?在Calico的FAQ里,官方给了答案:

Why can’t I see the 169.254.1.1 address mentioned above on my host?

Calico tries hard to avoid interfering with any other configuration on the host. Rather than adding the gateway address to the host side of each workload interface, Calico sets the proxy_arp flag on the interface. This makes the host behave like a gateway, responding to ARPs for 169.254.1.1 without having to actually allocate the IP address to the interface.

Calico利用了网卡的proxy_arp功能,具体的,是将/proc/sys/net/ipv4/conf/DEV/proxy_arp置为1,当设置这个标志之后,主机就会看起来像一个网关,会响应所有的ARP请求,并将自己的MAC地址告诉客户端。
也就是说,当容器发送ARP请求时,主机会告诉容器,我拥有169.254.1.1这个IP,我的MAC地址是XXX,这样,容器就可以顺利的将数据包发出来了,于是网络就通了。

其实Calico不仅仅设置了这个标志,但是这个标志是最重要的,毕竟关系到网络是否能通的问题。看了看Cailco的代码,发现Calico还设置了其他几个标志位:

  • /proc/sys/net/ipv4/conf/DEV/rp_filter => 1:开启反向路径过滤,确认数据包来源,对于普通容器,IP基本无法伪装,但是如果是VM(Calico也支持VM),很容易伪装IP地址,所以为了安全打开这个选项。
  • /proc/sys/net/ipv4/conf/DEV/route_localnet => 1:允许路由到本地。
  • /proc/sys/net/ipv4/neigh/DEV/proxy_delay => 0:默认情况下,主机为了减少ARP风暴的可能,会延迟一段时间回复ARP包,这个选项关闭这个延迟。
  • /proc/sys/net/ipv4/conf/DEV/forwarding => 1:允许转发数据包(如果不允许转发的话,那数据包就出不去主机了)。

上面是IPv4的情况,如果是IPv6的网络,则会设置:

  • /proc/sys/net/ipv6/conf/DEV/proxy_ndp => 1:这个和proxy_arp是一样的。
  • /proc/sys/net/ipv4/conf/DEV/forwarding => 1:同IPv4。