目录

cilium-chain和ipip包

概述

公司测试环境的集群部署 cilium-chain 之后,会导致配置了 ipip tunnel 的容器,无法响应办公网客户端经过 vgw 访问容器的请求。表现为虽然已经将容器 IP 负载均衡的 vip,但办公网环境先通过 vip 无法 curl 通注册的端口。

问题分析

经过运维同事排查,在 Pod 内部无法抓到包,在 Pod 的宿主机上可以抓到经过负载均衡发送的 ipip 协议封装的网络包。

1
2
3
4
5
6
7
# 宿主机上抓包,客户端是10.80.50.125
# curl 10.189.142.25:9200,其中10.189.142.25是负载均衡的vip
# tcpdump -i any host 10.80.50.125
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
14:49:57.939555 ethertype IPv4, IP 10.80.50.125 > 10.189.56.5: IP 10.80.50.125.65449 > 10.189.142.25.wap-wsp: Flags [S], seq 4164061164, win 65535, options [mss 1380,nop,wscale 6,nop,nop,TS val 2135870511 ecr 0,sackOK,eol], length 0 (ipip-proto-4)

经过推断是因为 cilium-agent 通过 chain 方式,会将 ebpf 程序绑定到容器网卡,通过卸载 cilium 的方式验证发现,确实可以访问成功,说明是 cilium 创建的 ebpf 影响了容器网卡正常处理经过 vgw 过来的网络包。cilium-agent 加载 ebpf 程序以及客户端、vgw、Pod之前的访问路径见下图。其中 cil_from_container-vvport959 主要会在外部的网络包到达容器网卡 vvport959 之后的 hook 点进行处理,cil_to_container-vvport959 是在容器将网络包从 vvport959 往外发送的时候的 hook 点。

/cilium-chain%E5%92%8Cipip%E5%8C%85/img.png

源码分析

先分析 cil_from_container-vvport959 的代码逻辑,简单来说,正常情况下,cilium 在 cil_from_container-vvport959 处理包的时候,会打开网络包的协议层直到 tcp 层之后,获取 ip 和 port 信息,创建 cilium 内部维护的 conntrack 表,详见下面的代码调用关系。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/cilium-1.16.1/bpf/bpf_lxc.c
cil_to_container
    |- tail_call_internal(ctx, CILIUM_CALL_IPV4_CT_INGRESS, &ext_err)
    |- TAIL_CT_LOOKUP4(CILIUM_CALL_IPV4_CT_INGRESS, tail_ipv4_ct_ingress, CT_INGRESS, 1, CILIUM_CALL_IPV4_TO_ENDPOINT, tail_ipv4_to_endpoint)
    |- #define TAIL_CT_LOOKUP4(ID, NAME, DIR, CONDITION, TARGET_ID, TARGET_NAME)
    |- 	ct_buffer.ret = ct_lookup4(map, tuple, ctx, ip4, ct_buffer.l4_off, DIR, ct_state, &ct_buffer.monitor)
        |- ct_lookup4(const void *map, struct ipv4_ct_tuple *tuple, struct __ctx_buff *ctx, struct iphdr *ip4, int off, enum ct_dir dir, struct ct_state *ct_state, __u32 *monitor) // /cilium-1.16.1/bpf/lib/conntrack.h
            |- ct_extract_ports4(ctx, ip4, off, dir, tuple, &has_l4_header)
                |- ct_extract_ports4(struct __ctx_buff *ctx, struct iphdr *ip4, int off, enum ct_dir dir, struct ipv4_ct_tuple *tuple, bool *has_l4_header)
                    |- switch (tuple->nexthdr) default: return DROP_CT_UNKNOWN_PROTO
                        |- #define DROP_CT_UNKNOWN_PROTO	-137 // /cilium-1.16.1/bpf/lib/common.h
                    |- 	if (ret < 0) return ret;
        |- 	if (ct_buffer.ret <0) return drop_for_direction(ctx, DIR, ct_buffer.ret, ext_err)
    |- static __always_inline int drop_for_direction(struct __ctx_buff *ctx, enum ct_dir dir, int reason, __s8 ext_err)
        |- return send_drop_notify_ext(ctx, src_label, dst, dst_id, reason, ext_err, CTX_ACT_DROP, m_dir);
|- #define send_drop_notify_ext(ctx, src, dst, dst_id, reason, ext_err, exitcode, direction) // /cilium-1.16.1/bpf/lib/drop.h        

正常情况下 cilium 的处理罗就是,查看一个 ip 包的下一层协议,除开代码里写好的相关协议,其他协议默认会丢弃,在我们的 case 下,cilium 打开查看 ip 层之后的协议头还是 ip 协议,这样 cilium 会直接 Drop 处理,这个也可以在 cilium monitor 上看到 DROP_CT_UNKNOWN_PROTO 这样的信息。

1
2
3
4
5
6
Ethernet    {Contents=[..14..] Payload=[..90..] SrcMAC=a0:f4:79:03:b6:16 DstMAC=02:02:0a:bd:51:60 EthernetType=IPv4 Length=0}
IPv4    {Contents=[..20..] Payload=[..44..] Version=4 IHL=5 TOS=0 Length=64 Id=0 Flags= FragOffset=0 TTL=53 Protocol=TCP Checksum=45077 SrcIP=10.80.50.125 DstIP=10.189.142.25 Options=[] Padding=[]}
# 这一行属于解开上面的IP包之后,发下下一层还是一个IP包,所以重复了
IPv4    {Contents=[..20..] Payload=[..44..] Version=4 IHL=5 TOS=0 Length=64 Id=0 Flags= FragOffset=0 TTL=53 Protocol=TCP Checksum=45077 SrcIP=10.80.50.125 DstIP=10.189.142.25 Options=[] Padding=[]}
TCP {Contents=[..44..] Payload=[] SrcPort=55795 DstPort=9200(wap-wsp) Seq=2198077010 Ack=0 DataOffset=11 FIN=false SYN=true RST=false PSH=false ACK=false URG=false ECE=false CWR=false NS=false Window=65535 Checksum=22905 Urgent=0 Options=[..8..] Padding=[0]}
CPU 06: MARK 0x37f686c1 FROM 1326 DROP: 98 bytes, reason CT: Unknown L4 protocol, identity world->16696, to endpoint 1326

正常情况下从 cilium monitor 可以看到正常的 IP 包再到 TCP 包的过程,然后 conntrack 表记录创建成功,状态转为 new。

1
2
3
4
Ethernet    {Contents=[..14..] Payload=[..62..] SrcMAC=a0:f4:79:03:b6:0e DstMAC=02:02:0a:bd:4a:ba EthernetType=IPv4 Length=0}
IPv4    {Contents=[..20..] Payload=[..40..] Version=4 IHL=5 TOS=0 Length=60 Id=26151 Flags=DF FragOffset=0 TTL=63 Protocol=TCP Checksum=1842 SrcIP=10.189.110.47 DstIP=10.189.74.186 Options=[] Padding=[]}
TCP {Contents=[..40..] Payload=[] SrcPort=14372 DstPort=6080 Seq=3305950202 Ack=0 DataOffset=10 FIN=false SYN=true RST=false PSH=false ACK=false URG=false ECE=false CWR=false NS=false Window=64240 Checksum=19025 Urgent=0 Options=[..5..] Padding=[]}
CPU 23: MARK 0xf8703ac5 FROM 451 to-endpoint: 74 bytes (74 captured), state new, interface vvport249, , identity world->59433, orig-ip 10.189.110.47, to endpoint 451

为了解决上面的情况,目前的方案是在创建 conntrack 表记录之前,判断如果是 IPIP 协议的包,直接 return CTX_ACK_OK,这样就跳过了创建 conntrack 表的过程,直接回到内核,这样容器网卡就可以接到 vgw 的 IPIP 包了。解决了网络包进入容器之后,再进行测试,发现又遇到了网络包从容器网卡出去的问题。通过 cilium monitor 可以看到下面的信息。

1
2
3
4
Ethernet {Contents=[..14..] Payload=[..66..] SrcMAC=02:02:0a:bd:45:5b DstMAC=a0:f4:79:03:b6:0a EthernetType=IPv4 Length=0}
IPv4 {Contents=[..20..] Payload=[..40..] Version=4 IHL=5 TOS=0 Length=60 Id=0 Flags=DF FragOffset=0 TTL=64 Protocol=TCP Checksum=25881 SrcIP=10.189.142.25 DstIP=10.80.50.125 Options=[] Padding=[]}
TCP {Contents=[..40..] Payload=[] SrcPort=9200(wap-wsp) DstPort=50260 Seq=2458082448 Ack=32113577 DataOffset=10 FIN=false SYN=true RST=false PSH=false ACK=true URG=false ECE=false CWR=false NS=false Window=64768 Checksum=54737 Urgent=0 Options=[..5..] Padding=[]}
CPU 20: MARK 0x5af79fff FROM 34 DROP: 74 bytes, reason Invalid source ip, identity 44482->unknown

reason Invalid source ip,这次还是被 Drop 了,但是是另外的理由,大概就是 source ip 不是有效的容器 ip,或者是 cilium 不认识的 ip。从代码里跟踪这个错误代码的产生,简单梳理了容器网络包经过 cil_to_container-vvport959 发出的过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/cilium-1.16.1/bpf/bpf_lxc.c
cil_from_container
    |- tail_call_internal(ctx, CILIUM_CALL_IPV4_FROM_LXC, &ext_err)
        |- int tail_handle_ipv4(struct __ctx_buff *ctx)
            |- __tail_handle_ipv4(ctx, &ext_err)
                |- static __always_inline int __tail_handle_ipv4(struct __ctx_buff *ctx, __s8 *ext_err __maybe_unused)
                    |- if (unlikely(!is_valid_lxc_src_ipv4(ip4))) return DROP_INVALID_SIP;
                        |- int is_valid_lxc_src_ipv4(const struct iphdr *ip4 __maybe_unused) // /cilium-1.16.1/bpf/lib/lxc.h
                            |- return ip4->saddr == LXC_IPV4;
            |- return send_drop_notify_error_ext(ctx, SECLABEL_IPV4, ret, ext_err, CTX_ACT_DROP, METRIC_EGRESS)
writeStaticData(devices []string, fw io.Writer, e datapath.EndpointConfiguration) // /cilium-1.16.1/pkg/datapath/linux/config/config.go
    |- fmt.Fprint(fw, defineIPv4("LXC_IPV4", e.IPv4Address().AsSlice()))
result["LXC_IPV4"] = uint64(byteorder.NetIPv4ToHost32(net.IP(ipv4))) // /cilium-1.16.1/pkg/datapath/loader/template.go

简单来说,is_valid_lxc_src_ipv4 是用来判断 IP 包的 src ip 是否为 cilium 记录下来的 LXC_IPV4 也就是容器 ip,很显然,在我们的场景里,肯定不是,因为此时我们的 src ip 为 vgw 的 vip,cilium 并不认识这个 vip,于是在 cilium 就把整个包丢弃了。为了解决这个问题,我们查看代码之后,发现 ENABLE_SIP_VERIFICATION 这个参数在当前的版本中被写死成 true,那么改成 false 之后,跳过这个检查,这种包就能发出去了。

/cilium-chain%E5%92%8Cipip%E5%8C%85/img_1.png

问题总结

通过修改 cilium 的源码,可以让 cilium 支持 vgw 的 ipip 协议头的网络包,以上的改动都比较小,至少应该不影响正常的容器网络,但是以上的改动,导致 vgw ip 是可以跳过 cilium 的网络的安全策略,这样会影响我们在生产环境中,关于数据库相关的访问策略的配置,如何更既能让 cilium 支持 vgw 的 ipip 协议的处理,又能让 vgw 的 ip 也能够被 cilium 的网络策略覆盖还需要更多的工作。