在之前的两篇文章中分别介绍了pod与主机连接并且上外网的原理及service的clusterIP和nodeport的实现原理,对于组织pod的网络这件事来说,还有最后一环需要打通,就是分布在不同集群节点的pod之间如何相互通信,本章我们来解决这最后一环的问题
在这里我们继续用linux network namespace(ns)代表pod
我们将用下面三种方式实现跨主机的pod通信:
我准备了两台节点:
先在两台节点中分别创建一个pod,并与节点能相互通信,创建pod并与节点通信的相关原理在第一章已经介绍过,这里不再一一解释,直接上命令:
ip netns add pod-a
ip link add eth0 type veth peer name veth-pod-a
ip link set eth0 netns pod-a
ip netns exec pod-a ip addr add 192.168.10.10/24 dev eth0
ip netns exec pod-a ip link set eth0 up
ip netns exec pod-a ip route add default via 169.254.10.24 dev eth0 onlink
ip link set veth-pod-a up
echo 1 > /proc/sys/net/ipv4/conf/veth-pod-a/proxy_arp
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -I FORWARD -s 192.168.0.0/16 -d 192.168.0.0/16 -j ACCEPT
ip route add 192.168.10.10 dev veth-pod-a scope link
ip netns add pod-b
ip link add eth0 type veth peer name veth-pod-b
ip link set eth0 netns pod-b
ip netns exec pod-b ip addr add 192.168.11.10/24 dev eth0
ip netns exec pod-b ip link set eth0 up
ip netns exec pod-b ip route add default via 169.254.10.24 dev eth0 onlink
ip link set veth-pod-b up
echo 1 > /proc/sys/net/ipv4/conf/veth-pod-b/proxy_arp
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -I FORWARD -s 192.168.0.0/16 -d 192.168.0.0/16 -j ACCEPT
ip route add 192.168.11.10 dev veth-pod-b scope link
如无意外,host1应该能ping通pod-a,host2也能ping通pod-b了,环境准备完成,下面我们介绍主机路由模式,这是flannel的host-gw模式的原理。
其实每一台linux主机本身就是一台路由器,可以用ip route命令配置主机上的路由表,要让pod-a和pod-b相互通信,只需要在两台主机上加一条路由即可:
ip route add 192.168.11.0/24 via 10.57.4.21 dev eth0 onlink
##这个eth0是host1连接host2的网卡,要根据你的测试节点的情况调整
ip route add 192.168.10.0/24 via 10.57.4.20 dev eth0 onlink
##这个eth0是host2连接host1的网卡,要根据你的测试节点的情况调整
注意上面我们加的路由是针对24位网络地址相同的子网段的,一般来说k8s集群的每个节点会独占一个24位的网络地址的子网段,所以每增加一个集群节点,其它节点加一条路由就可以了,但如果不是这样设计,像之前提过的pod要固定IP,又想要能在整个集群的任意节点运行,这个主机路由条目就会比较多,因为每条路由都是针对单个pod的
此时在pod-a中去ping pod-b应该是通了的,假设在pod-b的8080端口运行着一个http服务,在pod-a中请求这个服务,在主机路由的模式下,host1发往host2的数据包是长这样的:
注意图中的源IP和目标IP是容器的,但MAC地址却是主机的,我们在第一章中提到的linux网络知识的发送包的第5点说起过,数据包发送过程中,除非经过NAT,否则IP不会变化,始终标明通信双方,但MAC地址是每一段都会变化,数据包从pod-a到pod-b一共会经历三段:
这是跨节点容器通信方式中最简单高效的方式,没有封包拆包带来的额外消耗,但这种方式的使用场景有一些限制:
云平台的虚拟机为什么要做源/目地址检查呢?因为要防止IP spoofing
因为以上限制,host-gw通常在idc机房且节点数不多都在同一子网的情况下使用,或者与别的模式混合使用,比如flannel的DirectRouting开启时,相同网段的用host-gw,跨网段用vxlan;
有没有节点跨网段也能使用的模式呢?接下来介绍的ip tunnel(就是常说的ipip模式)就是了。
ipip模式并不是像主机路由那样,修改数据包的mac地址,而是在原ip包的前面再加一层ip包,然后链路层是以外层ip包的目标地址封装以太网帧头,而原来的那层ip包更像是被当成了外层包的数据,完成这个封包过程的是linux 虚拟网络设备tunnel网卡,它的工作原理是用节点路由表中匹配原ip包的路由信息中的下一跳地址为外层IP包的目标地址,以本节点的IP地址为源地址,再加一层IP包头,所以使用ip tunnel的模式下,我们需要做两件事情:
我们接着上面的环境继续操作:
host1:
ip route del 192.168.11.0/24 via 10.57.4.21 dev eth0 onlink
host2:
ip route del 192.168.10.0/24 via 10.57.4.20 dev eth0 onlink
host1:
ip tunnel add mustang.tun0 mode ipip local 10.57.4.20 ttl 64
ip link set mustang.tun0 mtu 1480 ##因为多一层IP头,占了20个字节,所以MTU也要相应地调整
ip link set mustang.tun0 up
ip route add 192.168.11.0/24 via 10.57.4.21 dev mustang.tun0 onlink
ip addr add 192.168.10.1/32 dev mustang.tun0 ## 这个地址是给主机请求跨节点的pod时使用的
host2:
ip tunnel add mustang.tun0 mode ipip local 10.57.4.21 ttl 64
ip link set mustang.tun0 mtu 1480
ip link set mustang.tun0 up
ip route add 192.168.10.0/24 via 10.57.4.20 dev mustang.tun0 onlink
ip addr add 192.168.11.1/32 dev mustang.tun0
这时候两个pod应该已经可以相互ping通了,还是假设pod-a请求pod-b的http服务,此时host1发往host2的数据包是长这样的:
因为主机协议栈工作时是由下往上识别每一层包,所以ipip包对于主机协议栈而言,与正常主机间通信的包并没有什么不同,帧头中的源/目标mac是主机的,ip包头中源/目标ip也是节点的,这让节点所处的物理网络也感觉这是正常的节点流量,所以 这个模式相对于主机路由来说对环境的适应性更广,起码跨网段的节点也是可以通的,但是在云平台上使用这种模式还是要注意下,留意图二中外层IP包中的传输层协议号是不一样的(是IPPROTO_IPIP),正常的IP包头,这应该是TCP/UDP/ICMP,这样有可能也会被云平台的安全组策略拦截掉,在linux内核源码中可以看到:
//include/uapi/linux/in.h
enum {
...
IPPROTO_ICMP = 1, /* Internet Control Message Protocol */
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6, /* Transmission Control Protocol */
#define IPPROTO_TCP IPPROTO_TCP
IPPROTO_UDP = 17, /* User Datagram Protocol */
#define IPPROTO_UDP IPPROTO_UDP
...
一般而言我们在云平台安全组设置规则时,传输层协议都只有三个可选项,就是:TCP、UDP、ICMP(没有IPIP),所以最好是在云平台上把安全组内的主机间的所有协议都放开,会不会真的被拦截掉要看具体云平台,华为云是会限制的;
笔者曾经试过在华为云上使用ipip模式,总会出现pod-a ping不通ping-b,卡着的时候,在pod-b上ping pod-a,然后两边就同时通了,这是典型的有状态防火墙的现象; 之后我们把集群节点都加入一个安全组,在安全组的规则配置中,把组内所有节点的所有端口所有协议都放开后,问题消失,说明默认对IPIP协议是没有放开的
在host1中执行:
ip netns exec pod-a ping -c 5 192.168.11.10
在host2的eth0用tcpdump打印一下流量,就能看到有两层ip头:
tcpdump -n -i eth0|grep 192.168.11.10
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
18:03:35.048106 IP 10.57.4.20 > 10.57.4.21: IP 192.168.10.10 > 192.168.11.10: ICMP echo request, id 3205, seq 1, length 64 (ipip-proto-4)
18:03:35.049483 IP 10.57.4.21 > 10.57.4.20: IP 192.168.11.10 > 192.168.10.10: ICMP echo reply, id 3205, seq 1, length 64 (ipip-proto-4)
18:03:36.049147 IP 10.57.4.20 > 10.57.4.21: IP 192.168.10.10 > 192.168.11.10: ICMP echo request, id 3205, seq 2, length 64 (ipip-proto-4)
18:03:36.049245 IP 10.57.4.21 > 10.57.4.20: IP 192.168.11.10 > 192.168.10.10: ICMP echo reply, id 3205, seq 2, length 64 (ipip-proto-4)
calico的ipip模式就是这种,ip tunnel解决了主机路由不能在跨网段中使用的问题,在idc机房部署k8s集群的场景下,会拿host-gw和ipip两种模式混合使用,节点在相同网段则用host-gw,不同网段则用ipip,思路和flannel的directrouting差不多,只不过ipip比vxlan性能要好一些;
ip tunnel仍然有一些小小的限制,像上面说的云平台安全组对协议限制的问题,下面再介绍一种终极解决方案,只要节点网络是通的,容器就能通,完全没有限制,这就是vxlan模式;
主机路由是按普通路由器的工作原理,每一跳修改MAC地址;ipip模式是给需要转发的数据包前面加一层IP包;而vxlan模式则是把pod的数据帧(注意这里是帧,就是包含二层帧头)封装在主机的UDP包的payload中,数据包封装的工作由linux虚拟网络设备vxlan完成,vxlan设备可以用下面的命令创建:
ip link add vxlan0 type vxlan id 100 dstport 4789 local 10.57.4.20 dev eth0
##设备名为vxlan0
##vxlan id 为 100
##dstport指示使用哪个udp端口
##eth0指示封装好vxlan包后通过哪个主机网卡发送
vxlan设备在封包时是根据目标MAC地址来决定外层包的目标IP,所以需要主机提供目标MAC地址与所属节点IP的映射关系,这些映射关系存在主机的fdb表(forwarding database)中,fdb记录可以用下面的命令查看:
bridge fdb show|grep vxlan0
8a:e7:df:c0:84:07 dev vxlan0 dst 10.57.4.21 self permanent
上面的记录的意思是说去往MAC地址为8a:e7:df:c0:84:07
的pod在节点IP为10.57.4.21
的节点上,fdb的信息可以手工维护,也可以让vxlan设备自动学习;
bridge fdb append 8a:e7:df:c0:84:07 dev vxlan0 dst 10.57.4.21 self permanent
ip link add vxlan0 type vxlan id 100 dstport 4789 group 239.1.1.1 dev eth0 learning
所有集群的节点都加入这个多播组,这样就能自动学习fdb记录了,当然这需要底层网络支持多播;
bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.57.4.21 self permanent
我们接着上面的环境继续往下做,先把mustang.tun0删除,在两个节点上执行:
ip link del mustang.tun0
然后
host1:
ip link add vxlan0 type vxlan id 100 dstport 4789 local 10.57.4.20 dev eth0 learning ## 这个eth0要根据你自己测试节点的网卡调整
ip addr add 192.168.10.1/32 dev vxlan0
ip link set vxlan0 up
ip route add 192.168.11.0/24 via 192.168.11.1 dev vxlan0 onlink
bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.57.4.21 self permanent
host2:
ip link add vxlan0 type vxlan id 100 dstport 4789 local 10.57.4.21 dev eth0 learning ## 这个eth0要根据你自己测试节点的网卡调整
ip addr add 192.168.11.1/32 dev vxlan0
ip link set vxlan0 up
ip route add 192.168.10.0/24 via 192.168.10.1 dev vxlan0 onlink
bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.57.4.20 self permanent
这时候两台主机的pod应该可以相互ping通了
ip netns exec pod-b ping -c 5 192.168.10.10
PING 192.168.10.10 (192.168.10.10) 56(84) bytes of data.
64 bytes from 192.168.10.10: icmp_seq=1 ttl=62 time=0.375 ms
64 bytes from 192.168.10.10: icmp_seq=2 ttl=62 time=0.497 ms
64 bytes from 192.168.10.10: icmp_seq=3 ttl=62 time=0.502 ms
64 bytes from 192.168.10.10: icmp_seq=4 ttl=62 time=0.386 ms
64 bytes from 192.168.10.10: icmp_seq=5 ttl=62 time=0.390 ms
--- 192.168.10.10 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4000ms
rtt min/avg/max/mdev = 0.375/0.430/0.502/0.056 ms
此时pod-a请求pod-b的http服务,数据包从host-1发往host-2时是长这样的:
可以看到,vxlan包是把整个pod-a发往pod-b最原始的帧都装进了一个udp数据包的payload中,整个流程简述如下:
192.168.11.0/24 via 192.168.11.1 dev vxlan0 onlink ## 这是我们在上面的host1执行的命令中的第四条命令添加的
bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.57.4.21 self permanent
所以,这里找到的目标只有一个,就是10.57.4.21,然后vxlan就借助host1的eth0发起了这个广播,只不过eth0发起的不是广播,而是有明确目标IP的udp数据包,如果上面我们是配置了多个全0的fdb记录,这里eth0就会发起多播。
像这种vxlan学习fdb的方式难免会在主机网络间产生广播风暴,所以flannel的vxlan模式下,是关闭了vxlan设备的learning机制,然后用控制器维护fdb记录和邻居表记录的
可以看到这个过程中两次都需要用到全0的fdb记录,我们也可以在host1上查看vxlan0学习到的fdb记录和邻居表信息:
bridge fdb|grep vxlan0
00:00:00:00:00:00 dev vxlan0 dst 10.57.4.21 self permanent ## 这是我们手工添加的
6e:39:38:33:7c:24 dev vxlan0 dst 10.57.4.21 self ## 这是vxlan0自动学习的,6e:39:38:33:7c:24 正是host2中vxlan0的地址
邻居表记录:
ip n
192.168.11.1 dev vxlan0 lladdr 6e:39:38:33:7c:24 STALE
在pod-b中ping pod-a的时候,在host1打开网卡监听,拦截的数据如下:
tcpdump -n -i eth0 src 10.57.4.21
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:21:01.050849 IP 10.57.4.21.55255 > 10.57.4.20.otv: OTV, flags [I] (0x08), overlay 0, instance 1
IP 192.168.11.10 > 192.168.10.10: ICMP echo request, id 26972, seq 15, length 64
10:21:02.051894 IP 10.57.4.21.55255 > 10.57.4.20.otv: OTV, flags [I] (0x08), overlay 0, instance 1
IP 192.168.11.10 > 192.168.10.10: ICMP echo request, id 26972, seq 16, length 64
......
可以看到也是两层包头,外层包头显示这是otv(overlay transport virtualization)包,对于otv,用一句话解释:
OTV is a "MAC in IP" technique to extend Layer 2 domains over any transport
从上面的过程可以看出来,vxlan模式依赖udp协议和默认的4789端口,所以在云平台的ECS上使用vxlan模式,还是需要在安全组上把udp 4789端口放开
什么终极解决方案,弄了半天也是要设置安全组的哈哈哈!!
不是的,不管是ipip还是vxlan模式下,主机协议栈把外层包头摘掉后,会把原始数据包重新扔回协议栈,重走一遍netfilter的几个点,所以针对podIP的防火墙策略依旧会生效的;
看回上一篇文章的图一,ipvs工作在netfilter扩展点中的LOCAL_IN点(也就是INPUT点),之前的内容中提过,流量在经过IPIsLocal时,会判断目标IP是否为本机地址,如果是则会走INPUT点,否则走FORWATD;为了让ipvs能操作流量,必须先让流量先到达INPUT点,于是就把所有clusterIP都挂在kube-ipvs0上,所有访问clusterIP的流量到达IPIsLocal点时,主机协议栈都会认为这是去往本机的流量,转到INPUT点去;
-A KUBE-FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
首先要了解,现在的防火墙技术都是基于连接状态的基础之上的,就是常说的有状态的防火墙;
拿上面的pod-a和pod-b来举例,假设我们不允许pod-a访问pod-b,于是在host1上创建一条这样的iptables规则:
iptables -A FORWARD -t filter -s 192.168.10.10 -d 192.168.11.10 -j DROP
好了,这时候pod-a中去ping pod-b已经不通了,但是,pod-b中去ping pod-a也不通了,因为pod-a回pod-b的包也命中了上面那条策略;
当我们说:不允许pod-a访问pod-b,只是说不允许pod-a主动访问pod-b,但是允许pod-a被动访问pod-b
这个听着有点绕,类似你跟你的二逼朋友说:平时没事别主动给老子打电话,但老子打你电话你要接!
好了,问题来了,怎么标识这是主动和流量还是被动的流量呢?这个问题linux内核协议栈已经帮我们解决好了,linux内核协议栈会悄悄维护连接的状态:
于是我们只要优先放过所有的连接状态为ESTABLISHED的包就可以了,问题中的命令的作用正是这个:
-A KUBE-FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-m conntrack
是说使用连接追踪模块标识的数据包状态,--ctstate
是connection track state(连接追踪状态)的简称,状态值有:NEW/ESTABLISHED/INVALID/RELATED等,各种状态的解释自行google;
上面这条规则的优先级一般都是最高的,如果放在其它限制规则的后面就没有意义了,不单是容器平台的防火墙策略,大多数云平台网络中ACL、安全组的策略也是这种玩法;
下一章我们来介绍pod的流量控制。
(上面的三张图,其中一张图中的有个细节是错误的,你看出来了吗?)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。