eBPFRouteTable
# 1.简介
# 1.1.背景
eBPF dataplane会在TC/XDP hook完成Svc负载均衡、CT、策略和转发,这意味着BPF程序必须在流量处理阶段回答对端目标。--- 作用 1.回答目标IP是本节点Pod,远程节点Pod,本机Host还是集群外地址 2.本节点Pod,需要基于路由明确redirect到哪张网卡 3.远程节点Pod,需要明确VXLAN封装的外层IP是什么 4.检查源IP需不需要SNA及NodePort后端需不需要DSR1
2
3
4
5注意
eBPF TC流量处理需要确定目标是本地还是远程及是否需要封装,内核FIB路由提供的系统调用查询频繁执行会影响性能
# 1.2.FIB局限
内核
FIB路由表受规则和链路状态多种因素影响,查询结果不确定及缺乏业务语义,calico需要明确的业务语义驱动RPF/NAT/Policy及封装。--- TC和内核路由时许 1.BPF TC挂载先于内核路由处理流量,BPF程序需要完成CT、NAT、策略和转发,此时内核FIB路由可能还不存在 2.bpf_fib_lookup是系统调用,直接查内核路由无法区分方向及目标,对于包进来就执行内核查询带来性能压力 --- 语义缺失 1.内核路由无法明确:目标IP是本地还是远程,Pod还是Host,是否需要封装或NAT Mask,是否支持DSR 2.calico基于这些语义驱动RPF校验、NAT决策、策略分发和封装,内核FIB只是一个纯粹的转发表 --- 跨节点下一跳 1.BPF对远程Pod进行封装需要隧道对端节点的IP地址作为外层,映射关系来自dataplane计算 2.内核路由会配置Pod CIDR路由,其网关未必是隧道端点IP,更不会携带VXLAN封装的语义标记 3.内核路由基于Route/ARP/FIB配合才能串起VXLAN封装的下一跳,相比于BPF保存的数据稍微复杂一些 --- 确定和隔离 内核路由表受规则和链路状态因素影响,查询结果不确定,BPF数据面更需要基于dataplane维护的确定路由1
2
3
4
5
6
7
8
9
10
11
12
13
14
15注意
eBPF RouteTable会维护cali_v4_routes,主要维护Pod和Node间的路由
# 2.BPF路由
# 2.1.作用
cali_v4_routes不是完全替代内核FIB,两者分工协作,BPF Route处理Pod和Node间的集群内路由,FIB处理L3设备和集群外路由。--- 职责 1.标记目标IP是本地负载、远程负载、本地主机或远端主机 2.本地负载记录if_index用于redirect到veth 3.远端负载/主机记录next_hop,用于VXLAN/IPIP/Wireguard隧道封装端点 4.传递NAT OutGoing、同子网、DSR及IPAM Pool检查语义 5.NodePort后端定位、RPF检查、CT隧道校验、隧道包识别提供依据1
2
3
4
5
6注意
本地
Pod直接redirect,远端Pod基于BPF或FIB系统调用进行封包及转入下一跳,均无法匹配会将流量交回内核栈处理
# 2.2.实现
calico_tc处理流量时,会基于rt_route检查目标IP是本地还是远程,以及转发流量会基于rt_lookup查找下一跳及获取目标表语义。// cali_v4_routes匹配路由 static CALI_BPF_INLINE struct cali_rt *cali_rt_lookup(__be32 addr) { union cali_rt_lpm_key k; k.key.prefixlen = 32; // 永远以/32发起LPM查找 k.key.addr = addr; return cali_v4_routes_lookup_elem(&k); // 查cali_v4_routes } // 目标IP语义匹配 static CALI_BPF_INLINE enum cali_rt_flags cali_rt_lookup_flags(__be32 addr) { struct cali_rt *rt = cali_rt_lookup(addr); if (!rt) { return CALI_RT_UNKNOWN; // 路由未匹配 } return rt->flags; // calico语义,本地Pod/远端Pod/Host... } // 目标是本节点 static CALI_BPF_INLINE bool rt_addr_is_local_host(__be32 addr) { return cali_rt_flags_local_host(cali_rt_lookup_flags(addr)); // 本节点HostIP } // 目标是远端节点 static CALI_BPF_INLINE bool rt_addr_is_remote_host(__be32 addr) { return cali_rt_flags_remote_host(cali_rt_lookup_flags(addr)); // 远程节点HostIP } cali_rt_flags_local_workload(t) ⇔ (t & 0x08) && (t & 0x04) // 有LOCAL且有WORKLOAD cali_rt_flags_remote_workload(t) ⇔ !(t & 0x08) && (t & 0x04) // 无LOCAL且有WORKLOAD1
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注意
eBPFRouteTable维护cali_v4_routes路由,cali_v4_routes未命中流量会回退到内核网络栈处理,具体可以看eBPF TC
# 3.初始化
# 3.1.注册
eBPF dataplane会初始化eBPFRouteTable,基于实际路由和期望路由增量更新cali_v4_routes内容,供TC挂载流量处理决策。func NewIntDataplaneDriver(config Config) *InternalDataplane { ... if config.BPFEnabled { ... // 注册eBPFRouteTable bpfRTMgr := newBPFRouteManager(&config, bpfMaps, dp.loopSummarizer) dp.RegisterManager(bpfRTMgr) ... // kube-proxy启用场景 if config.KubeClientSet != nil { // We have a Kubernetes connection, start watching svc and populating NAT maps. kp, err := bpfproxy.StartKubeProxy(...) ... // 路由/HostIP变化会通知给kube-proxy bpfRTMgr.setHostIPUpdatesCallBack(kp.OnHostIPsUpdate) bpfRTMgr.setRoutesCallBacks(kp.OnRouteUpdate, kp.OnRouteDelete) ... } } return dp } func newBPFRouteManager(config *Config, maps *bpfmap.Maps,...) *bpfRouteManager { ... for _, cidrStr := range config.ExternalNodesCidrs { // ipv6 cide忽略 if strings.Contains(cidrStr, ":") { continue } // 注册cidr cidr, err := ip.ParseCIDROrIP(cidrStr) ... extCIDRs.Add(cidr.(ip.V4CIDR)) dirtyCIDRs.Add(cidr.(ip.V4CIDR)) } ... // dsr cidr,允许由不同网卡直接回包 for _, cidrStr := range config.BPFDSROptoutCIDRs { // ipv6 cide忽略 if strings.Contains(cidrStr, ":") { continue } // 注册cidr cidr, err := ip.ParseCIDROrIP(cidrStr) ... noDsrCIDRs.Update(cidr.(ip.V4CIDR), something) // We need to store something dirtyCIDRs.Add(cidr.(ip.V4CIDR)) } return &bpfRouteManager{ ... desiredRoutes: map[routes.Key]routes.Value{}, // 期望路由 routeMap: maps.RouteMap, // cali_v4_routes对应路由 dirtyRoutes: set.New[routes.Key](), // 待更新路由 resyncScheduled: true, ... } }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注意
eBPFRouteTable和routeTable是互补关系,Pod和Node相关的集群内路由由前者维护,L3设备和集群外路由由后者维护
# 3.2.同步
m.OnUpdate()会将同步的消息进行格式转换更新到对应map缓存及标记dirty,由calc/apply阶段计算差异及更新rt_map。func (m *bpfRouteManager) OnUpdate(msg interface{}) { switch msg := msg.(type) { // ifmonitor监听的网卡更新 case *ifaceStateUpdate: m.onIfaceUpdate(msg) // iface UP/DOWN/Index变化 case *ifaceAddrsUpdate: m.onIfaceAddrsUpdate(msg) // iface addr变化 // calGrapth计算的路由更新 case *proto.RouteUpdate: m.onRouteUpdate(msg) // cidr->node case *proto.RouteRemove: m.onRouteRemove(msg) // cidr route删除 // calGrapth计算的wep对象更新 case *proto.WorkloadEndpointUpdate: m.onWorkloadEndpointUpdate(msg) // podIP->wep网卡 case *proto.WorkloadEndpointRemove: m.onWorkloadEndpointRemove(msg) // 清理 } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19注意
这里更新的都是对应缓存索引,受影响的
cidr会加入dirty集合,不会直接写BPF Map
# 4.缓存
# 4.1.iface
iface状态或地址变化会更新cidr双向索引缓存,关联的cidr标记dirty,将主机IP变化回调通知kube-proxy调整NAT Map。// 这里集中处理的是Pod网卡 func (m *bpfRouteManager) onIfaceUpdate(msg *ifaceStateUpdate) { // DOWN->UP if msg.State == ifacemonitor.StateUp { oldIdx, ok := m.ifaceNameToIdx[msg.Name] // 未注册/index变化 if !ok || oldIdx != msg.Index { m.ifaceNameToIdx[msg.Name] = msg.Index // 更新name->index m.onIfaceIdxChanged(msg.Name) // 关联wep cidr标记dirty } // DOWN } else { _, ok := m.ifaceNameToIdx[msg.Name] // 注册过,清理缓存 if ok { delete(m.ifaceNameToIdx, msg.Name) // 清理name->index m.onIfaceIdxChanged(msg.Name) // 关联wep cidr标记dirty } } } func (m *bpfRouteManager) onIfaceIdxChanged(name string) { wepIDs := m.ifaceNameToWEPIDs[name] if wepIDs == nil { return // 不是Pod网卡,无路由受影响 } // iface关联wep wepIDs.Iter(func(wepID proto.WorkloadEndpointID) error { wep := m.wepIDToWorklaod[wepID] cidrs := getV4WorkloadCIDRs(wep) // wep cidr(/32) m.markCIDRsDirty(cidrs...) // 标记dirty return nil }) } // 主机网卡(HostIP) func (m *bpfRouteManager) onIfaceAddrsUpdate(update *ifaceAddrsUpdate) { ... if update.Addrs == nil { newCIDRs = set.Empty[ip.V4CIDR]() // cidr清空,旧地址走删除分支 } else { ... // 登记变化的addr update.Addrs.Iter(func(cidrStr string) error { cidr := ip.MustParseCIDROrIP(cidrStr) // 仅保留v4全局单播,过滤lo/link-local(169.254.x)非路由地址,这里其实就排除了主机侧的Pod网卡 if v4CIDR, ok := cidr.(ip.V4CIDR); ok && cidr.Addr().AsNetIP().IsGlobalUnicast() { newCIDRs.Add(v4CIDR) } return nil }) } // 旧cidr差量清理 cidrs := m.localIfaceToCIDRs[update.Name] if cidrs != nil { cidrs.Iter(func(cidr ip.V4CIDR) error { if newCIDRs.Contains(cidr) { newCIDRs.Discard(cidr) // 增量集合剔除无变化的 return nil } ... m.cidrToLocalIfaces[cidr].Discard(update.Name) // 反向索引清理本接口 if m.cidrToLocalIfaces[cidr].Len() == 0 { delete(m.cidrToLocalIfaces, cidr) } m.markCIDRsDirty(cidr) // dirty标记 return set.RemoveItem }) } // 增量注册 newCIDRs.Iter(func(cidr ip.V4CIDR) error { ... ifaceNames := m.cidrToLocalIfaces[cidr] ... ifaceNames.Add(update.Name) // 反向索引注册本接口 ... m.markCIDRsDirty(cidr) // dirty标记 cidrs.Add(cidr) // 更新到curCidrs return set.RemoveItem }) // 有变化 if changed { ... // 全量HostIP收集 for cidr := range m.cidrToLocalIfaces { newIPs = append(newIPs, cidr.Addr().AsNetIP()) } m.onHostIPsChange(newIPs) // 回调通知kube-proxy } }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注意
Pod网卡变化会基于wep cidr标记dirty更新,主机网卡变化会更新双向索引缓存及回调通知kube-proxy
# 4.2.route
calGrapth计算的路由对应local_workload/remote_wordload/remote_host的下一跳,会更新到cidr route缓存及标记dirty。func (m *bpfRouteManager) onRouteUpdate(update *proto.RouteUpdate) { cidr := ip.MustParseCIDROrIP(update.Dst) v4CIDR, ok := cidr.(ip.V4CIDR) if !ok { // FIXME IPv6 return } // 本地隧道地址不由rt_route维护 if update.Type == proto.RouteType_LOCAL_TUNNEL { m.onRouteRemove(&proto.RouteRemove{Dst: update.Dst}) // 清理到本地隧道的路由 return } // 无变化 if m.cidrToRoute[v4CIDR] == *update { return } m.cidrToRoute[v4CIDR] = *update // 更新路由缓存 m.dirtyCIDRs.Add(v4CIDR) // 标记dirty } func (m *bpfRouteManager) onRouteRemove(update *proto.RouteRemove) { cidr := ip.MustParseCIDROrIP(update.Dst) v4CIDR, ok := cidr.(ip.V4CIDR) if !ok { // FIXME IPv6 return } // 清理 if _, ok := m.cidrToRoute[v4CIDR]; ok { delete(m.cidrToRoute, v4CIDR) // 路由缓存清理 m.dirtyCIDRs.Add(v4CIDR) // 标记dirty } }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注意
tun route会被过滤掉,eBPFRouteTable不会维护隧道设备路由,还是交给内核FIB表维护的
# 4.3.wep
wep增删会维护cidr->wepID/wepID->wep/iface->wepID索引缓存,相关cidr标记为dirty,后续由calculateRoute重新计算路由。func (m *bpfRouteManager) onWorkloadEndpointUpdate(update *proto.WorkloadEndpointUpdate) { m.removeWEP(update.Id) // 清理旧wep的索引 m.addWEP(update) // 注册新wep索引 } func (m *bpfRouteManager) onWorkloadEndpointRemove(update *proto.WorkloadEndpointRemove) { m.removeWEP(update.Id) // 清理旧wep的索引 } func (m *bpfRouteManager) addWEP(update *proto.WorkloadEndpointUpdate) { // 更新wepID->wep索引 m.wepIDToWorklaod[*update.Id] = update.Endpoint // wep关联cidrs newCIDRs := getV4WorkloadCIDRs(update.Endpoint) // 更新cidr->wepID索引 for _, cidr := range newCIDRs { wepIDs := m.cidrToWEPIDs[cidr] if wepIDs == nil { wepIDs = set.New[proto.WorkloadEndpointID]() m.cidrToWEPIDs[cidr] = wepIDs } wepIDs.Add(*update.Id) } m.markCIDRsDirty(newCIDRs...) // 标记cidr dirty // 更新iface->wepID索引 wepIDs := m.ifaceNameToWEPIDs[update.Endpoint.Name] if wepIDs == nil { wepIDs = set.New[proto.WorkloadEndpointID]() m.ifaceNameToWEPIDs[update.Endpoint.Name] = wepIDs } wepIDs.Add(*update.Id) } func (m *bpfRouteManager) removeWEP(id *proto.WorkloadEndpointID) { oldWEP := m.wepIDToWorklaod[*id] if oldWEP == nil { return // 未注册wep对象 } // 清理 delete(m.wepIDToWorklaod, *id) // cidr->wepID索引清理 oldCIDRs := getV4WorkloadCIDRs(oldWEP) for _, cidr := range oldCIDRs { m.cidrToWEPIDs[cidr].Discard(*id) if m.cidrToWEPIDs[cidr].Len() == 0 { delete(m.cidrToWEPIDs, cidr) } } // 标记cidr dirty m.markCIDRsDirty(oldCIDRs...) // iface->wepID索引清理 m.ifaceNameToWEPIDs[oldWEP.Name].Discard(*id) if m.ifaceNameToWEPIDs[oldWEP.Name].Len() == 0 { delete(m.ifaceNameToWEPIDs, oldWEP.Name) } }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注意
update前会先remove wep再add wep,应对wep addr变更的情况
# 5.更新
# 5.1.calculate
m.recalculateRoutesForDirtyCIDRs()会基于dirty cide计算route,根据增量状态更新desired route及回调通知kube-proxy。func (m *bpfRouteManager) recalculateRoutesForDirtyCIDRs() { // dirty cidr-->desired route m.dirtyCIDRs.Iter(func(cidr ip.V4CIDR) error { dataplaneKey := routes.NewKey(cidr) // rt_map key newValue := m.calculateRoute(cidr) // 计算新路由 oldValue, exists := m.desiredRoutes[dataplaneKey] // 获取旧路由 if newValue != nil { if exists && oldValue == *newValue { return set.RemoveItem // 路由无变化 } m.desiredRoutes[dataplaneKey] = *newValue // 新路由注册到desiredRoutes m.onRouteUpdateCB(dataplaneKey, *newValue) // 回调通知kube-proxy的rtCache } else { if !exists { return set.RemoveItem // 路由已清理 } delete(m.desiredRoutes, dataplaneKey) // 清理desiredRoutes旧路由 m.onRouteDeleteCB(dataplaneKey) // 回调通知kube-proxy的rtCache } m.dirtyRoutes.Add(dataplaneKey) // 路由标记为dirty return set.RemoveItem }) } func (m *bpfRouteManager) calculateRoute(cidr ip.V4CIDR) *routes.Value { ... // ---- iface Flags ---- if _, ok := m.cidrToLocalIfaces[cidr]; ok { flags |= routes.FlagsLocalHost } if m.externalNodeCIDRs.Contains(cidr) { flags |= routes.FlagHost } if m.dsrOptoutCIDRs.Covers(cidr) { flags |= routes.FlagNoDSR } // ---- cidr Flags ---- cgRoute, cgRouteExists := m.cidrToRoute[cidr] if cgRouteExists { if cgRoute.SameSubnet { flags |= routes.FlagSameSubnet } if cgRoute.IpPoolType != proto.IPPoolType_NONE { flags |= routes.FlagInIPAMPool } if cgRoute.NatOutgoing { flags |= routes.FlagNATOutgoing } } ... switch cgRoute.Type { // 本地Pod case proto.RouteType_LOCAL_WORKLOAD: // 本地block黑洞路由归内核 if !cgRoute.LocalWorkload { return nil // 仅是IPAM Block,不是实际Pod } // cidr-->wepID-->wep-->idx // podIP---ifaceIndex wepIDs.Iter(func(wepID proto.WorkloadEndpointID) error { wep := m.wepIDToWorklaod[wepID] if ifaceIdx, ok := m.ifaceNameToIdx[wep.Name]; ok { flags |= routes.FlagsLocalWorkload routeVal := routes.NewValueWithIfIndex(flags, ifaceIdx) // value = if_index route = &routeVal } return nil }) // 对端Pod case proto.RouteType_REMOTE_WORKLOAD: flags |= routes.FlagsRemoteWorkload if m.wgEnabled { flags |= routes.FlagTunneled } // wireGuard启用,全量tunneled switch cgRoute.IpPoolType { case proto.IPPoolType_VXLAN, proto.IPPoolType_IPIP: // VXLAN/IPIP,全量tunneled flags |= routes.FlagTunneled } if cgRoute.DstNodeIp == "" { return nil } // 无目的IP,不处理 // remote podIP---remote nodeIP nodeIP := net.ParseIP(cgRoute.DstNodeIp) routeVal := routes.NewValueWithNextHop(flags, ip.FromNetIP(nodeIP).(ip.V4Addr)) route = &routeVal // value = next_hop // 对端主机 case proto.RouteType_REMOTE_HOST: flags |= routes.FlagsRemoteHost nodeIP := net.ParseIP(cgRoute.DstNodeIp) // remote nodeIP---remote nodeIP,下一跳是对端主机本身,二层直达 routeVal := routes.NewValueWithNextHop(flags, ip.FromNetIP(nodeIP).(ip.V4Addr)) route = &routeVal // 对端隧道接口 case proto.RouteType_REMOTE_TUNNEL: flags |= routes.FlagsRemoteTunneledHost // remote vtepIP---remote vtep,下一跳是对端隧道本身,BPF不会用这里封装,用的是上一步的nodeIP routeVal := routes.NewValueWithNextHop(flags, cidr.Addr().(ip.V4Addr)) route = &routeVal // 本机 case proto.RouteType_LOCAL_HOST: // 目的是本机无下一跳 flags |= routes.FlagsLocalHost fallthrough default: if flags != 0 { // We have something to say about this route. routeVal := routes.NewValue(flags) route = &routeVal } } return route }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注意
1.
remote tunnel相关路由这里不会用到,本地Pod会网卡直接,远程Pod基于remote nodeIP封包2.
remote host路由主要用于拆封源校验及nodePort/SVC特殊转发场景(client-->local-->remote nodeIP服务)
# 5.2.resync
m.resyncWithDataplane()会扫描Pin Map内容进行全量对账,将BPF Map的真实内容和内存期望路由修正为一致,自愈路由漂移。func (m *bpfRouteManager) resyncWithDataplane() { ... m.dirtyRoutes.Clear() // 先清空dirty route for k := range m.desiredRoutes { // desired route先视为dirty m.dirtyRoutes.Add(k) } // 扫描rt_route Pin文件内容修正dirty route // 文件: /sys/fs/bpf/tc/globals/cali_v4_routes err := m.routeMap.Iter(func(k, v []byte) maps.IteratorAction { ... copy(key[:], k) copy(value[:], v) // route已经生效 if desired, ok := m.desiredRoutes[key]; ok && desired == value { // 清理dirty route m.dirtyRoutes.Discard(key) // route内容变化 } else if ok { // Route is present but incorrect (and we'll have marked it dirty above). ... // 多余的route } else { ... // Route is not in the desired map so it needs to be deleted. m.dirtyRoutes.Add(key) } return maps.IterNone }) ... }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注意
rt_route的全量对账由首次启用或周期调用触发,用于修正或清理漂移的route内容
# 5.3.complete
m.CompleteDeferredWork()是rt_route更新入口,基于dirty cide计算dirty route及对账底,更新或删除rt_route内容。func (m *bpfRouteManager) CompleteDeferredWork() error { m.recalculateRoutesForDirtyCIDRs() // dirty CIDR-->desiredRoute if m.resyncScheduled { m.resyncWithDataplane() // 全量resync BPF MAP m.resyncScheduled = false } m.applyUpdates() // dirtyRoute-->BPF Map return nil } func (m *bpfRouteManager) applyUpdates() (numDels uint, numAdds uint) { // 扫描dirty route m.dirtyRoutes.Iter(func(key routes.Key) error { value, present := m.desiredRoutes[key] // route过期 if !present { ... // bpftool map delete... m.routeMap.Delete(key[:]) ... return set.RemoveItem } // bpftool map update... m.routeMap.Update(key[:], value[:]) ... return set.RemoveItem }) return }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注意
首次启动后,
dataplane会基于timer间隔周期提交对账检查请求,触发resyncWithDataplane