对于刚接触K8S的同学来说,K8S网络显得尤为复杂,例如Pod如何访问主机以及pod间如何进行通信等。本系列文章将站在一个初学者角度,逐层刨析Kubernetes网络实现原理,并利用基本的Linux命令加以实现。
网络虚拟化基石:network namespace
network namespace 在Linux内核2.6版本引入,作用是隔离Linux系统设备,使得它们有独自的协议栈信息,一个直观的例子就是:每个容器都可以有自己的虚拟网络设备,并且容器内进程可以放心的绑定在端口上而不担心冲突。
和其他namespace一样, network namespace也可以通过系统调用来创建,同时也可以助 ip
命令来完成各种操作。ip
命令来自于 iproute2
安装包。
ip命令管理的功能很多,操作network namespce的命令为:ip netns,可以使用ip netns help获取帮助。下面先介绍几条间的的network namespace管理命令。
创建一个名为ns1的network namespace可以使用以下命令:
# ip netns add ns1
当创建出一个network namespace空间后,Linux
内核将该namespace挂载至/var/run/netns路径下,此时可以使用ip netns exec 进入该namespae,执行网络查询或者配置工作。
# ip netns exec ns1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
可以看到,一个新的network namespace 创建出来之后,其网卡信息中只包含了一块本地回环设备loopback,除此之外,防火墙规则、路由规则等也是一片空白。
想在主机上查看存在的network namespace,可以使用以下命令:
# ip netns list
想删除network namespace 可以通过以下命令实现
# ip netns delete ns1
虚拟设备桥梁:veth pair
veth pair是一种Linux内核技术,可用于连接两个虚拟网络接口,这两个虚拟网络接口总是成对出现,其工作原理就是向veth pair的一端发送的数据,数据经过协议栈后从另外一端出来。
正因为有一个特性,它常常充当着一个桥梁,连接着各种虚拟设备,典型例子就是:使用veth pair连接两个network namespace。
下面命令演示了如何创建一个veth pair:
# ip link add veth0 type veth peer name veth1
46: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 22:18:89:26:fd:63 brd ff:ff:ff:ff:ff:ff
47: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether f6:ca:0d:bf:85:34 brd ff:ff:ff:ff:ff:ff
Pod和宿主机通信
接下来,我们使用network namespace和veth pair技术来模拟Pod和主机通信
使用过kubernets的同学应该知道,当我们创建一个Pod之后,CNI Plugin将会这个Pod下的所有containers分配一个network namespace。同时如果使用calico等CNI Plugin时,还能观察到Pod所在宿主机上会多出一些虚拟网卡,这些虚拟网卡一端连接Pod所在network namespace,一端连接在宿主机上。
所谓Pod,从网络的角度来看,就是共享一个ns的多个容器,这些容器在网络上与外界完全隔离,它们既访问不了外面,外面也访问不了它们。要向他们互相通信,就需要kubernetes cni来完成这项工作,下面我们就用Linux命令来完成cni所做的事。
首先,我们创建一个名为pod-1的network namespace
# ip netns add pod-1
我们用这个pod-1代表pod所在namespace,进入pod-1查看网络设备信息:除了loopback设备外一片空白。
# ip netns exec ns1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
现在,我们创建一对 veth pair
# ip link add eth0 type veth peer name cali-001
将veth pair中的一块虚拟设备添加进pod-1
# ip link set eth0 netns pod-1
并且为eth0配置ip address,ip地址可以随意分配。
# ip netns exec pod-1 ip addr add 10.1.10.10 dev eth0
启动两块虚拟网络设备
# ip netns exec pod-1 ip link set dev eth0 up
# ip link set dev cali-001 up
进入pod-1,尝试ping一下host,这里我的host ip为:192.168.11.126
# ip netns exec pod-1 ping -c 1 192.168.11.126
connect: Network is unreachable
为什么会出现网络不可达呢,上文不是说veth pair的一端发送数据,会从另外一端出来吗?是上面说错了吗?其实不然,我们回想一下Linux基础网络知识:
当Linux需要向外发送一个数据包时,总是执行以下步骤:
- 查找该数据包目的地的路由信息,如果是直连路由,则在邻居表插在该目的地的MAC地址。
- 如果非直连路由,则在邻居表查找下一跳的MAC地址。
- 如果找不到对应路由信息,就报告”network is unreachable“。
- 如果邻居表没有相应MAC信息,则向外发送ARP请求询问。
- 找到MAC地址后,数据帧源MAC地址为发送网卡MAC地址,目标MAC则为下一跳MAC地址。
而这里,我们ping host,则会发出ICMP报文,因为没有路由,所以返回了network is unreachable
我们查看以下pod-1的路由信息
# ip netns exec pod-1 route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
发现路由表为空,上文我们说过,当一个network namespce创建出来之后,其路由表为空。既然这样,我们添加路由信息
# ip netns exec pod-1 ip route add 169.254.1.1 dev eth0
# ip netns exec pod-1 ip route add default via 169.254.1.1 dev eth0
169.254.1.1是CNI插件calico的默认的网关,后面系列文章会讲到
再次查看路由信息
# ip netns exec pod-1 ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
此时,我们看到路由表中多了一条非直连路由,意思是默认流量走eth0网卡,下一跳为169.254.1.1
然后我们再次尝试我们ping host
# ip netns exec pod-1 ping -c 1 192.168.11.126
PING 192.168.11.126 (192.168.11.126) 56(84) bytes of data.
--- 192.168.11.126 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
还是不行,按照刚才所说的Linux网络知识第2点,我们查看一下邻居表
# ip netns exec pod-1 ip neigh
169.254.1.1 dev eth0 FAILED
这里可以看到,eth0为FAILED,意味着获取不到网关的MAC地址,即整个网络中没有一张网卡是这个地址,我们可以抓包证明
# ip netns exec pod-1 tcpdump -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
07:57:00.780329 ARP, Request who-has 169.254.1.1 tell 10.1.10.10, length 28
可以看到,我们在pod-1 ping host的时候,eth0发起ARP广播,请求169.254.1.1的MAC地址,但是没有任何响应。
再次回到Linux网络知识第4点,当前数据帧源地址为eth0的MAC地址,通过邻居表知道,下一跳是169.254.1.1。这时候如果能给169.254.1.1一个MAC地址,则数据帧就满足发送要求了,那把谁的地址给他呢?
答案是:veth pair的另外一端,即主机上的cali-001
那如何将cali-001的MAC地址给到169.254.1.1呢?
答案是:通过ARP欺骗,让cali-001作为代理ARP的角色,收到eth0的ARP请求时,代为应答,告诉eth0自己的MAC地址,而eth0收到响应,则认为是169.254.1.1的MAC地址,然后写入邻居表
可以通过以下命令设置网络设备开启代理APR
# echo 1 > /proc/sys/net/ipv4/conf/cali-001/proxy_arp
同时打开转发功能
# echo 1 > /proc/sys/net/ipv4/ip_forward
我们再次尝试ping host
# ip netns exec pod-1 ping -c 1 192.168.11.126
PING 192.168.11.126 (192.168.11.126) 56(84) bytes of data.
--- 192.168.11.126 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
仍然访问不通,如果这时候抓包,你会发现,发送的arp包仍然没有响应。同时邻居表中也没有写入cali-001的MAC地址,实验环境为centos7,不排除其他OS可以。
这时你需要执行以下命令,关闭反向校验
# echo 0 > /proc/sys/net/ipv4/conf/cali-001/rp_filter
# echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
对于某个网卡而言,实际生效的值为 相应网卡的值与 all 值两者中的最大值
这时候,你再次ping host,可以通吗?
结果是仍然不同,但幸运的是,这时候你如果查看pod-1的邻居表,会发现169.254.1.1有MAC地址了。
# ip netns exec pod-1 ip neigh
169.254.1.1 dev eth0 lladdr aa:bc:80:1d:6d:29 REACHABLE
# ip addr
52: cali-001@if53: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether aa:bc:80:1d:6d:29 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::a8bc:80ff:fe1d:6d29/64 scope link
valid_lft forever preferred_lft forever
可以看到169.254.1.1的MAC地址就等于cali-001的MAC地址,我们的代理arp生效了!!!
我们在主机上抓包cali-001看一下
# tcpdump -pne -i cali-001
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on cali-001, link-type EN10MB (Ethernet), capture size 262144 bytes
11:20:40.322404 a2:37:06:54:4f:f6 > aa:bc:80:1d:6d:29, ethertype IPv4 (0x0800), length 98: 10.1.10.10 > 192.168.11.126: ICMP echo request, id 41357, seq 1, length 64
终于不是arp请求了,而是icmp报文,但是这个报文只有请求,没有回复。
是什么原因呢?此时pod-1与host的对话应该是这样
pod-1:我给你发送了icmp报文,你为什么不回复我?
host:我收到了报文,但是我不知道怎么回复你,我这里没有看到10.1.10.10的路由,所以我交给了默认网关
默认网关:这条报文的目标地址我不知道,我把它丢了
pod-1: ......
还是路由问题,我们在主机上添加直连路由
# ip route add 10.1.10.10 dev cali-001 scope link
然后,我们再次ping host
# ip netns exec pod-1 ping -c 1 192.168.11.126
PING 192.168.11.126 (192.168.11.126) 56(84) bytes of data.
64 bytes from 192.168.11.126: icmp_seq=1 ttl=64 time=0.024 ms
--- 192.168.11.126 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.024/0.024/0.024/0.000 ms
终于通了!!!!
还没完,这时候我们执行以下命令,开启反向校验
# echo 1 > /proc/sys/net/ipv4/conf/cali-001/rp_filter
再次尝试ping host,发现仍然可以ping通,为什么会这样呢?
我们先来看内核参数rp_filter
的说明
值 | 含义 |
---|---|
0 | 关闭反向路由校验 |
1 | 开启严格的反向路径校验。对每个进来的数据包校验其反向路径是否是最佳路径接收报文的网卡和回数据的网卡是否是同一张网卡。如果反向路径不是最佳路径则直接丢弃该数据包。 |
2 | 开启松散的反向路径校验。对每个进来的数据包校验其源地址是否可达即反向路径是否能通通过任意网卡如果反向路径不通则直接丢弃 |
现在我们删除路由信息,并把rp_filter修改为1,看看发生了什么
# ip route del 10.1.10.10 dev cali-001
# echo 1 > /proc/sys/net/ipv4/conf/cali-001/rp_filter
要观察内核发生了什么,我们还需要打印syslog
# sysctl -w net.ipv4.conf.all.log_martians=1
该参数用于打印是否存在火星包(丢包)
一切准备就绪,还是在pod-1中ping host
# ip netns exec pod-1 ping -c 1 -I eth0 192.168.11.126
PING 192.168.11.126 (192.168.11.126) from 10.1.10.10 eth0: 56(84) bytes of data.
--- 192.168.11.126 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms
# dmesg
[85071.926604] IPv4: martian source 169.254.1.1 from 10.1.10.10, on dev cali-001
[85071.926609] ll header: 00000000: ff ff ff ff ff ff a2 37 06 54 4f f6 08 06 .......7.TO...
# netstat -s | grep IPReversePathFilter
IPReversePathFilter: 10
上面展示了三条命令,第一条是pod-1 ping host,第二个是查看syslog信息,第三个是查看反向过滤拦截数。
从syslog日志中可以看到,出现了火星包,这个日志大意是:
cali-001上收到了src=10.0.10.10,dst=192.254.1.1的包,但是按照本机的路由设置对10.0.10.10进行路由计算,得出的out dev不是cali-001,主机路由如下:
# ip route
default via 192.168.11.2 dev ens33 proto static metric 100
192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.126 metric 100
因为主机没有配置10.0.10.10路由,就会从默认路由,即ens33出去,所以校验失败,内核丢弃该包,所以pod-1内收不到Arp响应
当我们配置路由后,主机路由中就有了10.0.10.10的路由,并且这个路由对应的网卡就是cali-001,满足过滤条件,内核就不会丢弃该包,而是应答Arp请求。
总结
总结以下,上文我们创建了一对虚拟网卡cali-001/eth0,并将eth0分配给pod-1这个network namespace,然后在容器中发送了一个ICMP报文,数据流转如下:
- 在用户态中执行ping命令,通过socket调用给到pod-1协议栈
- pod-1协议栈准备发起ICMP报文,查找路由表,获知从eth0出去,下一跳是169.254.1.1
- pod-1协议栈查找Arp表,没有169.254.1.1的MAC地址,于是先发起Arp请求
- cali-001收到pod-1中eth0发来的Arp请求,因为配置了proxy_arp,于是响应自己的MAC地址
- pod-1协议栈收到响应,组装ICMP报文,发送给cali-001
- cali-001收到ICMP报文,交给自己的协议栈,即host协议栈
- host协议栈处理完ICMP报文,准备回复
- host协议栈先查自己的路由表,获知应该从cali-001出去
- host将响应报文从cali-001发出
- cali-001和eth0是一对veth pair,所以eth0收到响应报文
- eth0将报文交给pod-1的协议栈
后续
读完本文,你可能还有疑问,同宿主机上不同network namespace应该如何通信,以及跨主机不同network namespace应该如何通信,关于这些问题,在后续文章中将会展开介绍