欢迎加入本站的kubernetes技术交流群,微信添加:加Blue_L。
在介绍之前,先来熟悉一下linux给我们提供的一些基础设备。回顾一下之前cni与pod网络中的测试用例。
[root@localhost ~]# ip link add name testbr type bridge
[root@localhost ~]# ip link add name veth1 type veth peer name br-veth1
[root@localhost ~]# ip link add name veth2 type veth peer name br-veth2
[root@localhost ~]# ip netns add test1
[root@localhost ~]# ip netns add test2
[root@localhost ~]# ip link set netns test1 dev veth1
[root@localhost ~]# ip link set netns test2 dev veth2
[root@localhost ~]# ip link set master testbr dev br-veth1
[root@localhost ~]# ip link set master testbr dev br-veth2
[root@localhost ~]# ip netns exec test1 ip addr add 172.16.0.2/24 dev veth1
[root@localhost ~]# ip netns exec test2 ip addr add 172.16.0.3/24 dev veth2
[root@localhost ~]# ip netns exec test1 ip link set veth1 up
[root@localhost ~]# ip netns exec test2 ip link set veth2 up
[root@localhost ~]# ip link set br-veth1 up
[root@localhost ~]# ip link set br-veth2 up
[root@localhost ~]# ip link set testbr up
[root@localhost ~]# ip netns exec test1 ip route add default dev veth1 src 172.16.0.2
[root@localhost ~]# ip netns exec test2 ip route add default dev veth2 src 172.16.0.3
[root@localhost ~]# ip netns exec test1 ping 172.16.0.3
PING 172.16.0.3 (172.16.0.3) 56(84) bytes of data.
64 bytes from 172.16.0.3: icmp_seq=1 ttl=64 time=0.081 ms
64 bytes from 172.16.0.3: icmp_seq=2 ttl=64 time=0.081 ms
接口设备
loopback
loopback是一个虚拟网络设备接口,在linux上这个接口名为lo,通常ip地址是127.0.0.1/8。这个地址主要用来做本机系统之间互相通信使用。在linux的路由实现中,对于向本地地址发的数据包都会从lo这个网卡走。这个lo网卡将上层传递过来的ip数据包通过设备层的发送方法直接将数据包放到设备的接收队列中,在调用网络层的接收数据方法,从而实现将这个接口出去的数据再传递会给上层网络层,应用层。
veth-pair
veth-pair是一对虚拟的网络设备接口,是一个2层设备。我们可以把它看作为用一条网线连接的两个网卡。在任一网卡上发的数据会在另一块网卡上接收到。但是veth-pair设备发数据时并不会将2层数据包通过某些硬件发送出去,因为他们都在同一个主机上,一端网卡发的数据包的处理是由内核直接将数据包放到了另一块网卡的数据包接收队列中。因此,无论哪块网卡发送数据,数据都会由另一块网卡接收到。虚拟网络接口对他们是对等的,所以哪一端放容器里都一样。
veth-pair通常用来连接不同的网络命名空间使用,比如kubernetes中,一端在主机的网络命名空间下,另一端在pod的网络命名空间下。从而实现了在不同节点上的pod的网络数据可以通过主机的网卡发送和接收到。
通常,pod内的网卡是需要配置ip地址,路由,mtu等配置信息的,而在主机上的这一端则不需要配置。当然,主机上的这一段也可以配置ip地址,不影响这个veth-pair的功能使用。
外面的接口随便配置ip,可正常配置ip地址,发送数据时根据目的地选择网卡发出,回包时也是从别的网卡回直接进去内核网络战由上层直接处理。本机通信直接走local路由,然后发给loopback接口。无论是在本地访问还是外面配置路由访问这个ip地址,数据都不经过这个ip对应的接口,可以参考下面的验证。
[root@localhost ~]# ip addr add 192.168.10.3/32 dev br-veth2
[root@localhost ~]# nc -kl 192.168.10.3 8888
在同一个主机发起访问
[root@localhost ~]# tcpdump -i lo host 192.168.10.3
在同一个主机中抓包,可以观察到数据包都是通过lo接口走的
[root@localhost ~]# tcpdump -i lo host 192.168.10.3
在另外一个主机上访问
root@debian:~# ip route add 192.168.10.3 via 192.168.56.6
root@debian:~# curl 192.168.10.3 8888
在原主机抓包,其实是走的主机的另外的网卡
tcpdump -nN -i enp0s8 port 8888
他们都没经过br-veth2。
数据包走本地路由时,也会经过netfilter的各个hook,其中包括OUTPUT,POSTROUTING和INPUT。可以通过下列iptables规则验证:
DNAT到了一个不存在的地址,外部无法访问
[root@localhost ~]# iptables -tnat -A PREROUTING -p tcp --dst 192.168.10.3 -j DNAT --to-destination 192.168.10.6
禁止本地和外部的访问
[root@localhost ~]# iptables -A INPUT -p tcp --dst 192.168.10.3 -j DROP
配置tcp mss大小,PREROUTING不生效,tcpdump -nnnvvvvN -i lo port 8888
[root@localhost ~]# iptables -tnat -A POSTROUTING -p tcp --syn --dst 192.168.10.3 -j TCPMSS --set-mss 50
bridge
同一个主机上的pod通信有两种方式,这块主要讲2层模式,3层的在calico会详细分析:
- 使用网桥
- 3层路由
网桥也是一个2层设备,起到桥接多个接口的作用。连接在同一个bridge上的设备之间通过2层协议进行数据交换,参考上面同局域网路由规则。网桥设备在收到arp请求后会进行广播。
linux下网桥设备的实现和loopback接口类似,连接到bridge接口发出的数据在发送时直接放到了bridge设备的接收队列里,bridge设备根据目标mac地址进行转发。
连接到网桥的接口之间默认是无法访问主机上的其他ip地址以及主机之外的ip地址的,默认只能够访问到连接到这个bridge上的接口。但是通过设置net.ipv4.conf.{all/testbr}.arp_ignore=0这个内核参数,可以让bridge设备代理返回主机上其他ip地址的arp请求,响应的mac地址为bridge本身的mac。其他接口发给bridge(其他网卡也一样)数据经过linux网络协议栈上升到ip层经过路由后最终确定发给本地地址则会在将数据上升给应用程序,如果目标地址不是本地地址的话根据主机的路由规则配置进行转发,这个过程也会经过netfilter相关处理。
经过上面配置后虽然接口可以访问到主机上的ip地址,但是仍然无法访问该主机之外的地址。可以采用下面路由加nat方式配置既可以访问本机ip又可以访问外部的ip。设置网卡的默认路由为bridge的ip地址,并配置iptables规则设置源ip为172.16.0.0/24网段的自动进行网络地址转换。
[root@localhost ~]# ip netns exec test1 ip route rep default dev veth1 via 172.16.0.1 src 172.16.0.2
[root@localhost ~]# iptables -t nat -A POSTROUTING -s 172.16.0.0/24 -j MASQUERADE
如果要主机能够访问bridge里的ip,在主机上加上下列路由,bridge根据mac地址判断数据是发给自己的,不能直接2层转发,而是看到ip地址和自己是同一个网段的,需要arp协议解析到目标地址的mac地址,然后发送给目标网卡。
[root@localhost ~]# ip route add 172.16.0.0/24 dev testbr
拉起本地lo,否则访问本地ip不通。
[root@localhost ~]# ip netns exec test1 ip link set lo up
配置开启bridge的hairpin模式,不开启的话,自己发出的包不会再发给自己,因为bridge从这个口接收到的包不会在这个口回来。某系场景可能有问题,比如访问的service的clusterIP,经过iptables进行dnat后又回到了自己就回不来了(网卡混杂模式也可以解决)。参考:https://en.wikipedia.org/wiki/Hairpinning。
[root@localhost ~]# brctl hairpin testbr br-veth1 on
[root@localhost ~]# brctl hairpin testbr br-veth2 on
递交上层(vxlan设备也有类似机制)。
路由
在linux中路由的功能是确保将数据包能够发送到正确的目的地,按照目的地的不同,大概可以分为这几种情况来考虑路由问题:
- 目标地址在同一个主机上
- 目标地址和主机在同一个局域网内
- 除上面两种情况的其他情况
linux在发送数据包时,需要查询路由表以便确认数据包从哪个网卡发出,发给谁。下面是一个示例路由表配置:
# 查看路由表
[root@localhost ~]# ip rule list
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
# 查看本地路由,高优先级
# 这些规则是linux根据网卡配置自动创建的,local为本地路由,目的地为192.168.56.6的话则通过enp0s8发出
[root@localhost ~]# ip route show table local | grep 192.168.56
broadcast 192.168.56.0 dev enp0s8 proto kernel scope link src 192.168.56.6
local 192.168.56.6 dev enp0s8 proto kernel scope host src 192.168.56.6
broadcast 192.168.56.255 dev enp0s8 proto kernel scope link src 192.168.56.6
# 查看主路由
[root@localhost ~]# ip route show
default via 10.0.2.2 dev enp0s3
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15
172.16.0.0/24 dev testbr scope link
192.168.56.0/24 dev enp0s8 proto kernel scope link src 192.168.56.6 metric 100
同主机
在linux查询路由表时,会先从local表查,如果我们访问192.168.56.6,linux就会选中上面所示路由规则,将数据包通过enp0s8发出。但是要注意的是linux在实现的时候并不是真的将数据包从enp0s8发出,而是发给了loopback接口。loopback接口就是我们主机上的lo网卡,一般ip地址为127.0.0.1。只要目标地址是本机地址,linux都会将数据包从该网卡发出。loopback接口发的数据都会放到loopback接口设备的接收队列中,从而实现数据包又传递到网络层。
因此,我们上面说的在主机一侧的veth设备配置了ip地址后不影响其功能,因为数据都是经过loopback接口发出去的,并不走这个veth设备。当然,如果没有那个local的路由规则,而是添加了一个unicast(unicast 10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15)类型的路由规则的话,那么数据包的发送就会经过这个veth设备了。可以通过下面实现验证:
[root@localhost ~]# ip addr add 10.10.10.10/32 dev br-veth2
[root@localhost ~]# nc -kl 192.168.10.3 8888
# 在同一个主机发起访问
[root@localhost ~]# nc 192.168.10.3 8888
# 在同一个主机中抓包,可以观察到数据包都是通过lo接口走的
[root@localhost ~]# tcpdump -i lo host 10.10.10.10
# 在另外一个主机上访问
root@debian:~# ip route add 192.168.10.3 via 192.168.56.6
root@debian:~# curl 192.168.10.3 8888
# 在原主机抓包,其实是走的主机的另外的网卡
tcpdump -nN -i enp0s8 port 8888
# 他们都没经过br-veth2
数据包走主机路由时,也会经过netfilter的各个hook,其中包括OUTPUT,POSTROUTING和INPUT。可以通过下列iptables规则验证:
# DNAT到了一个不存在的地址,外部无法访问
[root@localhost ~]# iptables -tnat -A PREROUTING -p tcp --dst 192.168.10.3 -j DNAT --to-destination 192.168.10.6
# 禁止了本地和外部的访问
[root@localhost ~]# iptables -A INPUT -p tcp --dst 192.168.10.3 -j DROP
# 配置tcp mss大小,PREROUTING不生效,tcpdump -nnnvvvvN -i lo port 8888
[root@localhost ~]# iptables -tnat -A POSTROUTING -p tcp --syn --dst 192.168.10.3 -j TCPMSS --set-mss 50
同局域网
[root@localhost ~]# ip -d route
unicast default via 10.0.2.2 dev enp0s3 proto boot scope global
unicast 10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15
unicast 172.16.0.0/24 dev testbr proto boot scope link
unicast 192.168.56.0/24 dev enp0s8 proto kernel scope link src 192.168.56.6 metric 100
通过这条路由规则我们可以看到,对于每个网卡,linux都会根据其配置为我们自动配置了一个局域网路由,比如我们的192.168.56.2/24,则配置了一个该网段的路由为通过enp0s8发出去,scope link是通过链路层的相关方法发送到对应的主机,比如arp。src 192.168.56.6帮助内核确定客户端本地使用哪个ip地址。若要给对方发包,需要知道对方的mac地址,如果不知道则会通过arp协议解析得到对方的mac地址。
其他
其他情况则走上面的default默认路由规则。10.0.2.2是一个路由器,它知道接下来如何转发这个包。当要发包给网关时,还是需要通过unicast 10.0.2.0/24这个路由来选择从哪个网口发出。先通过网卡做arp解析2.2的mac地址,将包发给它的mac。
netfilter
netfilter是linux对网络数据包进行过滤的框架,框架暴露了一些执行对网络数据包进行过滤操作的函数入口,如nf_hook()函数,linux会在不同网络层的不同位置调用入口函数执行包过滤操作。如下图参考所示:
按照数据流向来分类的话,可以将这些入口点分为:
- 路由前
数据到达链路层或网络层时对数据包进行过滤操作
- 到本地
在数据包提交给上层之前做过滤操作
- 转发
目的地不是本地的进行转发
- 出本地
进行转发的数据包或本地发出的数据包,已经设置好了目的地信息
- 路由后
对转发的数据包进行过滤操作
如果钩子函数允许数据包经过则返回1,如果返回其他值则表示数据包不允许通过,不允许数据包通过可能有多种情况。如设置了iptables的过滤规则,或者是进行了网络地址转换等。linux网络栈不同层不同位置会调用上面函数执行注册在这个点的钩子。那么这些钩子上的处理函数是如何挂上的呢。
netfilter框架本身除了提供钩子注册和执行的基本功能外,还提供了一些列的内核模块。这些内核模块会调用netfilter的注册方法,挂载一些处理函数到钩子上。这些处理函数可以处理是conntrack,nat等相关功能的。
那么对于用户空间一般要注册函数到钩子上是通过使用iptables,nftables等命令行工具实现的。这些工具一般通过套接字和这些内核模块进行通信,如规则的添加,删除等。iptables相关命令我们之前已经详细讲解过了。
nf_conntrack
链接追踪模块,支持tcp,udp,icmp等等协议。
nf_nat
用于进行网络地址转换相关操作。
nf_log
记录网络数据包详细信息到日志,如系统日志或到用户空间。
nf_queue
将数据包传递给用户态程序以决定是否允许通过。
nf_tables
存储netfilter相关表,类似于iptables内核中相关表模块。
ipvs
内核lvs实现。
ipset
定义集合管理结合内容,可以使用特定方式对结合内的元素进行检查存在与否。
nfnetlink
用户态和内核通过linux的netlink接口与netfilter系统交互(AFNETLINK,类比AFUNIX,AFINET或AFINET6)。可用于接收nflog日志,或nfqueue的数据包。
网络参数
kube-proxy或flannel或calico等对内核的参数会做一定调整,下面举一些比较常见的参数设置,具体详细信息可以参考相关文档或代码。一般情况下这些软件会自动配置好。这里做解释说明。
网卡的每个sysctl参数项都有这all/default/inteface三种形式,一般情况下all是针对所有网卡生效,不管对应的网卡配置的是什么,default是默认值,设置新网卡的参数值,针对具体网卡配置的值。
ip_forward
net.ipv4.ip_forward=1
veth-pair网卡,pod内发送的数据发到了peer的接收队列,然后进入网络层,如果访问的不是本地地址的话,那么就需要走转发,如果参数不开,访问不了非本地的其他地址。
arp_ignore
bridge测试使用
net.ipv4.conf.all.arp_ignore=0 回复任何本地ip地址的arp响应
net.ipv4.conf.all.arp_ignore=1 只回复配置在接收到网卡上的ip地址的响应,比如给testbr这个bridge设备配上ip可以解析
net.ipv4.conf.all.arp_ignore=2 只回复配置在接收到网卡上的ip地址的响应,并且要求目的ip和源ip是同一个子网,比如给testbr这个bridge设备配上ip可以解析,测试不同子网ip。
net.ipv4.conf.all.arp_ignore=3 不回复scope host的,只回复global和link地址的
4-7 保留
8 不回复任何本地地址解析的响应
arp_filter
bridge测试使用
net.ipv4.conf.all.arp_filter=0 linux会相应其他网卡上ip地址的arp解析
net.ipv4.conf.all.arp_filter=1 如果发包时,回复客户端ip地址时包会从这个网卡出,则会响应。
nf-call-iptables
net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables=1
配置bridge调用iptables规则。kubernetes中访问服务时通常不是直接访问具体pod,而是访问的clusterIP虚拟地址,此时会通过iptables对clusterIP对pod地址做DNAT,如果dnat之后是一个本地的pod则可能会出现无法建链接情况。在使用bridge的情况下(参考上面bridge访问主机ip和外部ip),则可能会出现本地的链接信息为:
clientIP:12345 -> clusterIP:23456实际上是clusterIP:12345 -> serverPodIP:23456
建链接时服务端返回的ack是发给clientIP:12345的,但是客户端没有找到要和serverPodIP:23456相关的链接就会失败。参考这里。
accept_local
如果pod经过dnat又回到了自己,则源ip是自己,但是报是网卡从外面收的,默认是丢弃所以要开启pod内的这个参数:
[root@localhost ~]# ip netns exec test1 sysctl -a | grep accept_local
proxy_arp
net.ipv4.conf.testbr.proxy_arp=0 不开启
net.ipv4.conf.testbr.proxy_arp=1 为接口配置启用arp代理,如果开启里的话,会回复本接口的mac地址作为arp响应。
rp_filter
net.ipv4.conf.testbr.rp_filter=0 不验证
net.ipv4.conf.testbr.rp_filter=1 严格模式,接收到的数据包的网卡,必须也是回数据包的网卡
net.ipv4.conf.testbr.rp_filter=2 松散模式,主机上能够找到回数据包的任何网卡都可以。
反向路径验证。比如针对flannel,访问其他节点pod经过flannel.1,对方flannel.1收到数据后看源ip需要从本机的eth0返回。