记一个 Harvester SNAT 案例

作者简介
姚灿武,SUSE Rancher 研发工程师,拥有 6 年云计算领域经验,热衷开源技术,在云原生相关技术领域拥有丰富的开发和实践经验。

Harvester 通过 Multus 扩展了标准的 Kubernetes CNI 网络,可以让虚拟机拥有基于 Bridge Vlan 技术分配的虚拟网卡。本文源于一次问题排查实践,以解决复杂网络情况下产生的通信问题。

本文使用的 Harvester 版本为 v1.0.0

问题描述

Harvester 利用 Kubernetes service 为虚拟机中的服务提供负载均衡。在这个方案中,负载均衡后端地址是<虚拟机的 IP 地址:端口>,被记录在与 Kubernetes service 对应的 endpointslice 中。示意图表示如下:

下面是本文所使用的例子对应的 service 与 endpointslice。

harvester-host:/home/rancher # kubectl get svc
NAME                        TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)        AGE
default-nginx-lb-db9bdca5   LoadBalancer   10.43.113.238                  80:32586/TCP   4h32m

harvester-host:/home/rancher # kubectl get endpointslices
NAME                       ADDRESSTYPE   PORTS   ENDPOINTS       AGE
default-nginx-lb-db9bdca5  IPv4          80      172.16.178.178   4h33m

但是,我们发现,当 VM 使用 Harvester VLAN 网络,并且发起请求的客户端(如 curl)与 VM 同在一个 Harvester 主机时,通过负载均衡(本例中是访问 service clusterIP)的请求失败了。结果如下:

harvester-host:/home/rancher # curl 10.43.113.238
curl: (7) Failed to connect to 10.53.202.161 port 80: Connection timed out

整个网络拓扑表示如下:

分析过程

明确流量路径

一般来说,对于网络问题,我们首先需要明确流量的转发路径。在 Kubernetes 中,当请求 service clusterIP 时,Kube-proxy 会将请求目的地址转为后端服务的地址。在我们的案例中,后端地址是 172.16.178.178:80。因为目的地址172.16.178.178 是在 VLAN 178 里,请求和响应都需要经过外部网关。因此,我们可以在网络拓扑中标记上流量转发路径。

抓包

不通过负载均衡,在各个 VLAN 网络中直接访问后端服务是通的,我们可以首先排除是外部交换机以及网关引发的问题。为了定位到是在哪个转发环节发生的问题,我们需要对 Harvester 主机流量路径上的各个网络接口进行抓包。我们将抓包结果整理后简化表示如下:

从抓包结果可以看出,VM 网卡正常接收到了 TCP SYN 网络包,并且响应发送了 SYN/ACK 报文。但是,当 SYN/ACK 报文被网桥从 veth2db2ad9c 转发到 eth1后,目的端口发生了改变。因为该目的端口与 SYN 报文的源端口不匹配,SYN/ACK 报文被丢弃导致 TCP 三次握手失败。由 conntrack 表中的记录可以看出,修改后的目的端口是负载均衡之前原方向请求的源端口。

harvester-host:/home/rancher # conntrack -L | grep 172.16.178.178
tcp      6 56 SYN_RECV src=172.16.0.57 dst=10.43.113.238 sport=38944 dport=80 src=172.16.178.178 dst=172.16.0.57 sport=80 dport=10598 mark=0 use=1
conntrack v1.4.5 (conntrack-tools): 262 flow entries have been shown.

因此,我们合理猜测在网桥转发 SYN/ACK 报文的过程中发生了一次网络地址转换(NAT)。

根据 Kubernetes 官方文档描述,Kubernetes 默认开启了net.bridge.bridge-nf-call-iptables设置。

if the plugin connects containers to a Linux bridge, the plugin must set the net/bridge/bridge-nf-call-iptables sysctl to 1 to ensure that the iptables proxy functions correctly.

该设置可以控制当报文经过网桥时,原来作用于三层网络的 iptables 规则是否在此二层转发过程中生效。在 Kubernetes 中,默认生效。也就是说,Kube-proxy 所设置的 iptables 规则包括一些 NAT 规则都会在网桥转发过程中起作用。

当我们设置 net.bridge.bridge-nf-call-iptables为 0 时,我们发现请求成功了。所以,毫无疑问,kube-proxy 的 iptables 规则导致了这个问题。但是,到目前为止,我们仍然无法定位具体是哪一条规则,或者说我们还没有定位 netfilter 中哪个环节导致了该问题。我们需要深入研究分析 netfilter 才能找到问题背后的根本原因。

深入分析 netfilter NAT

在网络分析工具 pwru 的帮助下,通过观察目的端口的变化以及打印出内核函数调用栈,我们可以确定 NAT 发生在 pre-routing 链上。

0xffff9e90d1c55200        [<empty>]      skb_ensure_writable   16542012456363 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:10598(tcp)
0xffff9e90d1c55200        [<empty>] inet_proto_csum_replace4   16542012502740 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:38944(tcp)
# stack of the function skb_ensure_writable
0xffff9e90c12a8d00    [ksoftirqd/5]      skb_ensure_writable   16558140061379 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:10598(tcp)
skb_ensure_writable
l4proto_manip_pkt [nf_nat]
nf_nat_ipv4_manip_pkt [nf_nat]
nf_nat_manip_pkt [nf_nat]
nf_nat_ipv4_pre_routing [nf_nat]
nf_hook_slow
br_nf_pre_routing [br_netfilter]
br_handle_frame [bridge]
__netif_receive_skb_core
__netif_receive_skb_one_core
process_backlog
__napi_poll
net_rx_action
__softirqentry_text_start
run_ksoftirqd
smpboot_thread_fn
kthread
ret_from_fork

# stack of the function inet_proto_csum_replace4
0xffff9e90c12a8d00    [ksoftirqd/5] inet_proto_csum_replace4   16558140095491 netns=4026531992 mark=0x0 ifindex=10 proto=8 mtu=1500 len=60 172.16.178.178:80->172.16.0.57:38944(tcp)
inet_proto_csum_replace4
l4proto_manip_pkt [nf_nat]
nf_nat_ipv4_manip_pkt [nf_nat]
nf_nat_manip_pkt [nf_nat]
nf_nat_ipv4_pre_routing [nf_nat]
nf_hook_slow
br_nf_pre_routing [br_netfilter]
br_handle_frame [bridge]
__netif_receive_skb_core
__netif_receive_skb_one_core
process_backlog
__napi_poll
net_rx_action
__softirqentry_text_start
run_ksoftirqd
smpboot_thread_fn
kthread
ret_from_fork

通过 Linux 内核中的 netfilter 源码以及netfilter在三层协议上的结构图,我们可以看到,NF_IP_PRI_NAT_DST参数表明,pre-routing 链上的 NAT 钩子只会改变报文的目的地址,即在 pre-routing 链上只会发生 DNAT 或者 de-SNAT。

static const struct nf_hook_ops nf_nat_ipv4_ops[] = {
 {
  .hook  = ipt_do_table,
  .pf  = NFPROTO_IPV4,
  .hooknum = NF_INET_PRE_ROUTING,
  .priority = NF_IP_PRI_NAT_DST,
 },
 ...
}

在我们的案例中,SYN/ACK 是回包,所以应当是发生了 de-SNAT。Kube-proxy 在 post-routing 链上添加了 SNAT iptables 规则。请求 service 的报文经过时会在 output 链上打上标记,打上标记的报文在 post-chain 上会发生 SNAT。回包时,报文会在 pre-routing 链上发生 de-SNAT。

-A KUBE-SVC-2CMXP7HKUVJN7L6M ! -s 10.42.0.0/16 -d 10.43.220.155/32 -p tcp -m comment --comment "default/nginx cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully

在 Kubernetes 中,通常客户端请求与后端服务实例在同一个主机节点上,请求不会经过 KUBE-MART-MASQ 链,也就不会发生 SNAT 和 de-SNAT。但是不知道为什么在这个案例中发生了。通过查阅 Kubernetes 官方文档,确定 kube-proxy 的配置项 cluster-cidr 是罪魁祸首。

--cluster-cidr string
The CIDR range of pods in the cluster. When configured, traffic sent to a Service cluster IP from outside this range will be masqueraded and traffic sent from pods to an external LoadBalancer IP will be directed to the respective cluster IP instead

将 cluster-cidr 设置为空,请求成功。

解决方案

从上面的分析中可以知道,解决问题的关键是避免发生不必要的 SNAT。有两个可选的解决方案。

  1. 因为 Harvester 所使用的 canal CNI 不依赖 bridge netfilter,我们可以直接关闭 net.bridge.bridge-nf-call-iptables
  2. 将 kube-proxy cluster-cidr 配置为空。

参考文献

  • [kube-proxy]
    https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/

  • [network-plugin-requirements]
    https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#network-plugin-requirements

  • [Net.bridge.bridge-nf-call and sysctl.conf]
    https://wiki.libvirt.org/page/Net.bridge.bridge-nf-call_and_sysctl.conf

  • [IPTABLES的连接跟踪与NAT分析]
    https://segmentfault.com/a/1190000041259845

  • [Deep Dive kube-proxy with iptables mode]
    https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/

  • [Service cluster ip How to disable snat]
    https://github.com/projectcalico/calico/issues/2999

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值