Kubernetes - 连接跟踪不会将包破坏回原始目标 IP (DNAT)

Kubernetes - Connection tracking does not mangle packages back to the original destination IP (DNAT)

我们有一个 Kubernetes 集群设置,使用我们使用 KOPS 创建的 AWS EC2 实例。我们在通过 kubernetes 服务进行内部 pod 通信时遇到问题(这将在目标 pods 之间负载平衡流量)。当源和目标 pod 在同一个 EC2 实例(节点)上时,问题就会出现。 Kubernetes 设置有 flannel,用于使用 vxlan 进行节点间通信,而 kubernetes 服务由 kube-proxy 使用 iptables 管理。

在以下情况下:

如果我们进入 PodA 并执行 "curl -v http://ServiceB/",则没有收到任何答复,最后会产生超时。

当我们检查流量时(实例 1 中的 cni0 接口),我们观察到:

  1. PodA向ServiceB IP发送SYN包
  2. 包损坏,目标 IP 从 ServiceB IP 更改为 PodB IP
  3. Conntrack 寄存器变化:

    root@ip-172-20-121-84:/home/admin# conntrack -L|grep 100.67.30.133
    tcp      6 118 SYN_SENT src=100.96.54.240 dst=100.67.30.133 sport=53084 dport=80 [UNREPLIED] src=100.96.54.247 dst=100.96.54.240 sport=80 dport=43534 mark=0 use=1
    
  4. PodB向PodA发送SYN+ACK包

  5. SYN+ACK 包的源 IP 没有从 PodB IP 恢复到 ServiceB IP
  6. PodA 收到来自 PodB 的 SYN+ACK 包,这不是预期的,它发回了一个 RESET 包
  7. PodA超时后再次发送SYN包给ServiceB,重复整个过程

这里是 tcpdump 注释的详细信息:

root@ip-172-20-121-84:/home/admin# tcpdump -vv -i cni0 -n "src host 100.96.54.240 or dst host 100.96.54.240"
TCP SYN:
15:26:01.221833 IP (tos 0x0, ttl 64, id 2160, offset 0, flags [DF], proto TCP (6), length 60)
    100.96.54.240.43534 > 100.67.30.133.80: Flags [S], cksum 0x1e47 (incorrect -> 0x3e31), seq 506285654, win 26733, options [mss 8911,sackOK,TS val 153372198 ecr 0,nop,wscale 9], length 0
15:26:01.221866 IP (tos 0x0, ttl 63, id 2160, offset 0, flags [DF], proto TCP (6), length 60)
    100.96.54.240.43534 > 100.96.54.247.80: Flags [S], cksum 0x36d6 (incorrect -> 0x25a2), seq 506285654, win 26733, options [mss 8911,sackOK,TS val 153372198 ecr 0,nop,wscale 9], length 0

Level 2:
15:26:01.221898 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 100.96.54.240 tell 100.96.54.247, length 28
15:26:01.222050 ARP, Ethernet (len 6), IPv4 (len 4), Reply 100.96.54.240 is-at 0a:58:64:60:36:f0, length 28

TCP SYN+ACK:
15:26:01.222151 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    100.96.54.247.80 > 100.96.54.240.43534: Flags [S.], cksum 0x36d6 (incorrect -> 0xc318), seq 2871879716, ack 506285655, win 26697, options [mss 8911,sackOK,TS val 153372198 ecr 153372198,nop,wscale 9], length 0

TCP RESET:
15:26:01.222166 IP (tos 0x0, ttl 64, id 32433, offset 0, flags [DF], proto TCP (6), length 40)
    100.96.54.240.43534 > 100.96.54.247.80: Flags [R], cksum 0x6256 (correct), seq 506285655, win 0, length 0

TCP SYN (2nd time):
15:26:02.220815 IP (tos 0x0, ttl 64, id 2161, offset 0, flags [DF], proto TCP (6), length 60)
    100.96.54.240.43534 > 100.67.30.133.80: Flags [S], cksum 0x1e47 (incorrect -> 0x3d37), seq 506285654, win 26733, options [mss 8911,sackOK,TS val 153372448 ecr 0,nop,wscale 9], length 0
15:26:02.220855 IP (tos 0x0, ttl 63, id 2161, offset 0, flags [DF], proto TCP (6), length 60)
    100.96.54.240.43534 > 100.96.54.247.80: Flags [S], cksum 0x36d6 (incorrect -> 0x24a8), seq 506285654, win 26733, options [mss 8911,sackOK,TS val 153372448 ecr 0,nop,wscale 9], length 0
15:26:02.220897 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    100.96.54.247.80 > 100.96.54.240.43534: Flags [S.], cksum 0x36d6 (incorrect -> 0x91f0), seq 2887489130, ack 506285655, win 26697, options [mss 8911,sackOK,TS val 153372448 ecr 153372448,nop,wscale 9], length 0
15:26:02.220915 IP (tos 0x0, ttl 64, id 32492, offset 0, flags [DF], proto TCP (6), length 40)
    100.96.54.240.43534 > 100.96.54.247.80: Flags [R], cksum 0x6256 (correct), seq 506285655, win 0, length 0

实例1(ip-172-20-121-84,us-east-1c)上的相关iptable规则(由kube-proxy自动管理):

-A INPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

-A KUBE-SERVICES ! -s 100.96.0.0/11 -d 100.67.30.133/32 -p tcp -m comment --comment "prod/export: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 100.67.30.133/32 -p tcp -m comment --comment "prod/export: cluster IP" -m tcp --dport 80 -j KUBE-SVC-3IL52ANAN3BQ2L74

-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.10000000009 -j KUBE-SEP-4XYJJELQ3E7C4ILJ
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.11110999994 -j KUBE-SEP-2ARYYMMMNDJELHE4
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.12500000000 -j KUBE-SEP-OAQPXBQCZ2RBB4R7
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.14286000002 -j KUBE-SEP-SCYIBWIJAXIRXS6R
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.16667000018 -j KUBE-SEP-G4DTLZEMDSEVF3G4
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.20000000019 -j KUBE-SEP-NXPFCT6ZBXHAOXQN
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-7DUMGWOXA5S7CFHJ
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-LNIY4F5PIJA3CQPM
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-SLBETXT7UIBTZCPK
-A KUBE-SVC-3IL52ANAN3BQ2L74 -m comment --comment "prod/export:" -j KUBE-SEP-FMCOTKNLEICO2V37

-A KUBE-SEP-OAQPXBQCZ2RBB4R7 -s 100.96.54.247/32 -m comment --comment "prod/export:" -j KUBE-MARK-MASQ
-A KUBE-SEP-OAQPXBQCZ2RBB4R7 -p tcp -m comment --comment "prod/export:" -m tcp -j DNAT --to-destination 100.96.54.247:80

-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

这是服务定义:

root@adsvm010:/yamls# kubectl describe service export
Name:              export
Namespace:         prod
Labels:            <none>
Annotations:       <none>
Selector:          run=export
Type:              ClusterIP
IP:                100.67.30.133
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         100.96.5.44:80,100.96.54.235:80,100.96.54.247:80 + 7 more...
Session Affinity:  None
Events:            <none>

如果我们直接使用 PodB IP 而不是服务(这样就不需要破坏包),连接就可以工作。

如果我们使用该服务但随机选择的目标 pod 在不同的实例中 运行,那么连接跟踪机制会正常工作并且它会破坏数据包,以便 PodA 将 SYN+ACK 数据包视为它期望它(来自 ServiceB IP)。在这种情况下,流量通过 cni0 和 flannel.0 接口。

这种行为是几周前开始的,那时我们还没有观察到任何问题(超过一年),我们不记得对集群设置或 pods 我们 运行 有任何重大更改.有没有人有任何想法可以解释为什么 SYN+ACK 包没有被破坏回预期的 src/dst IP?

我终于找到了答案。 cni0 接口与所有 pod 虚拟接口处于桥接模式(每个 pod 一个 veth0 运行ning 在该节点上):

root@ip-172-20-121-84:/home/admin# brctl show
bridge name bridge id       STP enabled interfaces
cni0        8000.0a5864603601   no      veth05420679
                                        veth078b53a1
                                        veth0a60985d
...


root@ip-172-20-121-84:/home/admin# ip addr
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8951 qdisc noqueue state UP group default qlen 1000
    link/ether 0a:58:64:60:36:01 brd ff:ff:ff:ff:ff:ff
    inet 100.96.54.1/24 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::1c66:76ff:feb6:2122/64 scope link
       valid_lft forever preferred_lft forever

from/to 桥接接口 to/from 其他接口的流量由 netfilter/iptables 处理,但不离开桥接接口的流量(例如从一个 veth0 到另一个,都属于同一个桥)不被netfilter/iptables处理。

在我在问题中公开的示例中,PodA(100.96.54.240)向不在cni0子网(100.96.54.1/24)中的ServiceB(100.67.30.133)发送了一个SYN包,因此该包不会留在桥接的 cni0 接口和 iptable 处理它。这就是为什么我们看到 DNAT 发生了并且它在 conntrack 中注册了。但是,如果所选目标 pod 在同一节点中,例如 PodB (100.96.54.247),则 PodB 会看到 SYN 包并使用 SYN+ACK 进行响应,其中源为 100.96.54.247,目标为 100.96.54.240。这些是 cni0 子网内的 IP,不需要离开它,因此 netfilter/iptables 不会处理它,也不会根据 conntrack 信息处理回包(即,真正的源 100.96.54.247 不会被替换为预期来源 100.67.30.133).

幸运的是,有 bridge-netfilter 内核模块可以使 netfilter/iptables 处理桥接接口中发生的流量:

root@ip-172-20-121-84:/home/admin# modprobe br_netfilter
root@ip-172-20-121-84:/home/admin# cat /proc/sys/net/bridge/bridge-nf-call-iptables
1

要在使用 KOPS (credits) 的 Kubernetes 集群设置中修复此问题,请使用 kops edit cluster 编辑集群清单,并在 spec: 下包括:

hooks:
- name: fix-bridge.service
  roles:
  - Node
  - Master
  before:
  - network-pre.target
  - kubelet.service
  manifest: |
    Type=oneshot
    ExecStart=/sbin/modprobe br_netfilter
    [Unit]
    Wants=network-pre.target
    [Install]
    WantedBy=multi-user.target

这将在您的节点中的 /lib/systemd/system/fix-bridge.service 中创建一个系统服务,该服务将在启动时 运行 并确保 br_netfilter 模块在 kubernetes(即 kubelet)之前加载开始。如果我们不这样做,我们在 AWS EC2 实例(Debian Jessie 图像)中遇到的情况是,有时模块在启动期间加载,有时则没有(我不知道为什么会有这样的可变性),所以取决于问题可能会出现,也可能不会出现。