eBPF和CTLB
eBPF的学习涉及大量BPF Hook实现和CTLB加速处理的协作,直接看eBPFEndpointMgr会非常吃力,建议先了解该章节介绍的前置内容
# 1.eBPF
# 1.1.简介
eBPF是一项革命性的技术,起源于Linux内核,基于操作系统运行沙河程序扩展内核的功能,无需更改内核源代码或加载内核模块。--- 特点 1.程序支持加载到内核,附着到钩子,特定事件发生会触发钩子调用 2.程序加载到内核是危险行为,内核会基于严格的静态校验器运行所有程序 3.静态校验器对程序进行沙河处理,确保仅访问允许的内存部分1
2
3
4
注意
1.
calico ebpf模式通过内置的小kube-proxy同步service--endpoint至BPF Map,利用BPF程序读取实现service流量接管2.
calico ebpf模式下,cni流量管理的iptables/conntrack/arp/fib...配置都会转移到不同的BPF Map,供BPF程序读取处理流量
# 1.2.挂载点
calico会提供三类BPF Hook挂载点,利用这些挂载点拦截及处理出入站流量,这些挂载覆盖容器网络的全部数据处理路径,先于网络栈执行。BPF TC
calico利用同一份tc.c代码,通过编译宏CALI_COMPILE_FLAGS为不同接口类型和方向生成程序变体,利用tail-call跳转到Prog程序
# 1.3.TC映射
eBPFEndpointMgr会根据网卡名和接口类型,为不同类型的网卡挂载不同编译项的TC程序,部分用不到的模块会利用宏编译掉,防止流量泄漏。接口类型 代表设备 Section FIB VXLAN NAT WEP cali*Pod网卡cali_from_wep_ep/cali_to_wep_ep✓ (from侧) ✗ ✓ Host (HEP) eth0/ens3cali_from_hep_ep/cali_to_hep_ep✓ ✓ (from侧) ✓ LO locali_from_lo_ep/cali_to_lo_ep✓ ✓ (from侧) ✓ NAT bpfout.calicali_from_nat_ep/cali_to_nat_ep✗ 编译掉 ✗ ✓ Tunnel tunl0(IPIP/VXLAN)cali_from_tunnel_ep/cali_to_tunnel_ep✓ ✗ ✓ L3Device wireguard.calicali_from_l3dev_ep/cali_to_l3dev_ep✗ ✗ ✓ bpfin.cali CTLB Workaround路由入口 不挂任何TC程序 — — — 注意
WEP的Hook方向是反的,Pod Ingress对应主机Egress,所以WEP Ingress会挂在Pod网卡的Egress Hook
# 1.4.TC程序
BPF TC主程序是共享入口,会宏编译CALI_F_*裁剪为不同版本,程序执行依次查询多个Global BPF Map完成连接跟踪、NAT、路由和策略决策。// tc.c:55-229 calico_tc() static CALI_BPF_INLINE int calico_tc(struct __sk_buff *skb) { // 解析IP头/L4头 tc_state_fill_from_iphdr(ctx); tc_state_fill_from_nexthdr(ctx); // 连接跟踪查找,查cali_v4_ct3(LRU Hash) ctx->state->ct_result = calico_ct_v4_lookup(ctx); // CT命中→ESTABLISHED→skip_policy直接转发 // key=(srcIP,dstIP,srcPort,dstPort,proto) → CT entry(flags+nat_ip+nat_port+tun_ip) calico_tc_process_ct_lookup(ctx); // CT未命中: NAT查找 nat_lookup_result nat_res = NAT_LOOKUP_ALLOW; if (CALI_F_TO_HOST || (CALI_F_FROM_HOST && !skb_seen(ctx->skb))) { // 查cali_v4_nat_fe: (dstIP,port,proto) → {id,count,affinity} // 有亲和性,查cali_v4_nat_aff,命中服用ordinal,未命中随机选一个ordinal,更新cali_v4_nat_aff // 查cali_v4_nat_be: 基于ordinal取后端池PodIP ctx->nat_dest = calico_v4_nat_lookup2(ctx->state->ip_src, ctx->state->ip_dst, ctx->state->ip_proto, ctx->state->dport, ctx->state->tun_ip != 0, &nat_res); } // 未命中后端,tail call到ICMP程序,回复ICMP port unreachable if (nat_res == NAT_NO_BACKEND) { /* ICMP port unreachable → PROG_INDEX_ICMP */ } // 上边的NAT查询仅会处理Service流量,先命中cali_v4_nat_be才会执行NAT // CTLB反向NAT查找,先恢复socket cookie __u64 cookie = bpf_get_socket_cookie(ctx->skb); if (cookie) { // 走到这里说明CTLB NAT过,查cali_v4_ct_nats(通过socket cookie还原CTLB改前的原始目的地址) struct sendrecv4_val *revnat = cali_v4_ct_nats_lookup_elem(&ct_nkey); if (revnat) { ctx->state->pre_nat_ip_dst = revnat->ip; } } // 查cali_v4_routes(eBPFRouteTable)匹配下一跳 struct cali_rt *dest_rt = cali_rt_lookup(ctx->state->post_nat_ip_dst); // ->hOST流量,查cali_v4_fsafes2 if (is_failsafe_in(...)) { goto skip_policy; } // 白名单端口跳过策略 // 接口就绪检查: 查cali_iface2(Hash) // 内部查ifstate确认CALI_ST_IFACE_READY,未就绪则failsafe fib_approve() // 尾调用策略程序: 查cali_jump3 → PROG_INDEX_POLICY(2) // → 动态加载的策略字节码执行NetworkPolicy规则匹配 // → 使用 cali_v4_ip_sets做IPset/CIDR匹配 // → DROP → PROG_INDEX_DROP; ACCEPT → PROG_INDEX_ALLOWED // 策略通过后继续走 calico_tc_skb_accepted_entrypoint CALI_JUMP_TO(ctx->skb, PROG_INDEX_POLICY); } // tc.c:602 calico_tc_skb_accepted() static CALI_BPF_INLINE struct fwd calico_tc_skb_accepted(struct cali_tc_ctx *ctx, struct cali_nat_dest *nat_dest) { // 基于CT状态分支创建/更新cali_v4_ct3 // 普通包建普通CT,DNAT包建NAT反向CT,为后续包提供快速路径 calico_ct_create(ctx, &ct_ctx_nat); // DNAT改写IP头+传输层端口,更新L3/L4 checksum if (nat_dest) { ip_hdr(ctx)->daddr = state->post_nat_ip_dst; // DNAT(外部->Pod) bpf_l3_csum_replace(...); } // NAT_OUTGOING,BPF这里只打mark,实际SNAT由iptables做 if (state->flags & CALI_ST_NAT_OUTGOING) { ip_hdr(ctx)->saddr = HOST_IP; // SNAT(Pod→集群外) seen_mark = CALI_SKB_MARK_NAT_OUT; } // 路由转发决策(FIB模式) if (fib && !CALI_ST_SKIP_FIB) { // rt_route查到本地Pod,查cali_v4_arp2,会redirect到Pod veth if (cali_rt_flags_local_workload(dest_rt->flags)) { fwd_set_redirect(fwd, dest_rt->if_index); } // rt_route查到对端Pod,使用L3工作模式,BPF封包 else if (cali_rt_flags_remote_workload(dest_rt->flags) && cali_rt_is_tunneled(dest_rt)) { // 目标IP包装NodeIP encap_and_vxlan_set_fwd(ctx); } // 主机发起的DNAT后的流量,rt_route查到远端Pod,发到bpfin,由TC触发路由 else if (!r || cali_rt_flags_remote_workload(r->flags)) { fwd_set_natin_fwd(fwd); } // rt_route找不到下一跳/外部流量 else { // 内核FIB路由 fib_rc = bpf_fib_lookup(skb, &fib_params, BPF_FIB_LOOKUP_DIRECT, 0); if (fib_rc == BPF_FIB_LKUP_RET_SUCCESS) { // 命中送到下一跳 fwd_set_redirect_mac(fwd, fib_params.ifindex, fib_params.dmac); } else { // FIB失败,回内核网络栈/iptables TC_ACT_UNSPEC; } } } // 计数器: cali_counters2 + cali_rule_ctrs2 counter_inc(ctx, CALI_REASON_ACCEPTED_BY_POLICY); update_rule_counters(ctx->state); }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107注意
1.
cali_state2(per-CPU entry)是跨tail-call的状态传递通道,主程序解析的五元组/CT/NAT/路由flags都写入cali_tc_state2.
calico的Prog/Accepted程序等tail-call目标都从同一块per-CPU内存读取上下文,避免了寄存器传递和栈深度限制
# 1.5.Pin路径
BPF程序本身是无状态的,持久化状态存储在BPF Map,通过Pin挂到/sys/fs/bpf,不同挂载点的BPF程序通过Pin路径复用Map FD。/sys/fs/bpf/ ├── tc/ // libbpf加载(TC/HEP-XDP/CTLB主数据面) │ ├── globals/ // 全局共享Map(所有程序共用) │ │ ├── cali_v4_nat_fe3 // NAT前端(Service VIP) │ │ ├── cali_v4_nat_be // NAT后端Hash(ID,ordinal→PodIP:Port) │ │ ├── cali_v4_nat_aff // NAT会话亲和性LRU Hash │ │ ├── cali_v4_ct3 // 连接跟踪Hash │ │ ├── cali_v4_routes // 路由表,启用eBPFRouteTable会用到 │ │ ├── cali_v4_arp2 // ARP表LRU Hash,基于WEP/HEP维护静态MAC │ │ ├── cali_iface2 // 接口状态Hash(ifindex:flags+ifname) │ │ ├── cali_v4_ip_sets // IPSet成员(策略匹配) │ │ ├── cali_state2 // Percpu全局状态,跨Hook共享 │ │ ├── cali_v4_fsafes2 // 故障安全端口 │ │ ├── cali_v4_srmsg // UDP sendmsg/recvmsg反向NAT │ │ ├── cali_v4_ct_nats // CT额外NAT信息 │ │ ├── cali_counters2 // Percpu报文计数器 │ │ └── cali_rule_ctrs2 // Percpu规则计数器 │ │ │ ├── eth0_igr/ → cali_jump3 // per-iface per-hook尾调用表(扁平结构) │ ├── eth0_egr/ → cali_jump3 │ ├── eth0_xdp/ → cali_jump3 // HEP策略XDP尾调用表 │ ├── bpfoutcali_igr/egr/ → cali_jump3 // bpfin/bpfout尾调链 │ ├── bpfincali_igr/egr/ → cali_jump3 │ ├── lo_igr/egr/ → cali_jump3 │ └── calixxxxxxx_igr/egr/ → cali_jump3 // Pod veth │ ├── calico/ // bpftool加载(XDP预过滤+sockmap) │ ├── calico_failsafe_ports_v1 // XDP预过滤故障安全端口 │ ├── xdp/ │ │ ├── prefilter_v1_eth0 // pinned XDP预过滤程序 │ │ └── eth0_ipv4_v1_blacklist // CIDR黑名单 │ └── sockmap/ │ ├── calico_sock_map_v1 // sockhash(Pod间TCP加速) │ ├── calico_sk_endpoints_v1 // sockmap端点 │ ├── calico_sockops_v1 // pinned sockops程序 │ └── calico_sk_msg_v1 // pinned sk_msg程序 │ └── calico_connect4/ // CTLB(install前清空,空目录)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38注意
service-->endpoint会放在globals目录,供不同挂载点TC NAT复用
# 2.CTLB
# 2.1.定义
CTLB(连接时负载均衡)是Calico替代kube-proxy的核心机制,用于在TC挂载前将数据包DNAT,内核连接用的就是NAT后的地址。--- 传统iptables/kube-proxy app → connect(ClusterIP:Port) → 内核路由 → 网卡 → iptables DNAT → PodIP --- CTLB app → connect(ClusterIP:Port) → [cgroup BPF: 改socket dst=PodIP:Port] → 内核直接路由到Pod1
2
3
4
5注意
CTLB是一种加速策略,不启用的情况下service流量会交给经过的网卡TC挂载处理,主机进程访问service没有命中的网卡会出问题
# 2.2.安装
CTLB启用情况下,dataplane会执行InstallConnectTimeLoadBalancer()向cgroup根目录挂载BPF实现,覆盖之前提到的六个程序。// Go侧挂载 func installProgram(name, ipver, bpfMount, cgroupPath, ...) error { progPinDir := path.Join(bpfMount, "calico_connect4") // sys/fs/bpf/calico_connect4 _ = os.RemoveAll(progPinDir) // 清空pin目录(不pin程序) // /usr/lib/calico/bpf/connect_time_%s_v4/6.o obj, _ := libbpf.OpenObject(filename) defer obj.Close() for m, err := obj.FirstMap(); m != nil; m, err = m.NextMap() { if m.IsMapInternal() { // .rodata全局变量,配置CTLB参数 libbpf.CTLBSetGlobals(m, udpNotSeen, excludeUDP) continue } // Map全部pin到/sys/fs/bpf/tc/globals/,与TC程序共享 // cali_v4_nat_fe/cali_v4_ct/cali_v4_rt m.SetPinPath(path.Join(bpfdefs.GlobalPinDir, m.Name())) } // CTLB BPF加载到内核 obj.Load() // 挂载到/run/calico/cgroup/{calico_connect_v4/6,calico_sendmsg_v4/6,calico_recvmsg_v4/6} _, _ = obj.AttachCGroup(cgroupPath, progName) return nil } // BPF C侧NAT // connect(10.96.0.10:80, TCP)--->查cali_v4_nat_fe--->查cali_v4_nat_aff--->查cali_v4_nat_be key=(id:42, ordinal) static CALI_BPF_INLINE int do_nat_common(struct bpf_sock_addr *ctx, __u8 proto, __be32 *dst, bool connect) { nat_lookup_result res = NAT_LOOKUP_ALLOW; __u16 dport_he = (__u16)(bpf_ntohl(ctx->user_port)>>16); // 查NAT FE+BE Map选后端Pod nat_dest = calico_v4_nat_lookup(0, *dst, proto, dport_he, false, &res, ...); // FE匹配到无后端这里才会生效 if (!nat_dest) { if (res == NAT_NO_BACKEND) err = -1; goto out; } // 记ct_nats映射供TC层反向NAT(响应回包用) cali_v4_ct_nats_update_elem(&natk, &val, 0); // 直接改socket目的地址——此时包还没构造,这样就实现加速 *dst = nat_dest->addr; // 目的IP→PodIP ctx->user_port = dport_be; // 目的端口→PodPort out: return err; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47补充
CTLB模式会创建一对veth pair(bpfin/bpfout)用于回退,部分内核CTLB不生效,回退兼容可以避免主机进程访问ClusterIP异常
# 2.3.NAT兼容
部分旧内核
cgroup BPF存在兼容性问题,calico通过一对虚拟网卡bpfin/bpfout构造回环路径,让数据包到达bpfout TC钩子做DNAT。// GO侧路由导流 // bpf_ep_mgr.go func (m *bpfEndpointManager) setRoute(cidr ip.V4CIDR) { // ip route add <ServiceCIDR> via 169.254.1.1 dev bpfin.cali m.routeTable.RouteUpdate(bpfInDev, routetable.Target{ Type: routetable.TargetTypeGlobalUnicast, CIDR: cidr, // 这里用169.254.1.1虚拟网关作为下一跳,避免VIP单独配ARP // bpfin配永久邻居,MAC指向bpfout GW: bpfnatGW, // }) } // BPF TC Ingress C侧 // NAT lookup触发 ctx->nat_dest = calico_v4_nat_lookup2(...); // 远程隧道后端SNAT if (CALI_F_TO_HOST && CALI_F_NAT_IF) { if (r && cali_rt_flags_remote_workload(r->flags) && cali_rt_is_tunneled(r->flags)) { ct_ctx_nat.src = HOST_TUNNEL_IP; } } // FIB编译掉,不做encap // 改写IP头后goto allow→TC_ACT_UNSPEC ip_hdr(ctx)->daddr = state->post_nat_ip_dst; goto allow; // 交内核按新IP重新路由1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28为什么需要一对设备?
1.本机进程发出的包由
Lo/物理网卡发出时,内核路由决策已经完成,TC程序修改完成无法基于新IP重新路由2.这里的
veth pair利用内核机制——一端egress发出的包从另一端ingress进入——制造了一个新入向包,内核会对DNAT后的地址路由