Calico BGP功能介绍:实现
Calico作为一种常用的Kubernetes网络插件,使用BGP协议对各节点的容器网络进行路由交换。本文是《Calico BGP功能介绍》系列的第二篇,介绍Calico中BGP功能的实现。所使用的Calico版本为v3.17.3。
Calico BGP功能
BGP Peer
Calico中通过定义BGP Peer对象,来建立BGP连接。BGP Peer对象的主要参数如下:
参数 | 描述 |
---|---|
node | 指定BGP Peer应用在哪个node上。如果指定此字段,则为node级别,否则为global级别。 |
peerIP | 指定远端的Peer地址,可以是IP加端口的形式,端口可选。支持IPV4和IPV6。 |
asNumber | 远端Peer的AS号。 |
nodeSelector | 用于通过标签来选择一组node,作为BGP Peer应用的节点,注意这里的node为Calico中的node,而非K8s中的node。如果指定了此字段,则node应该为空。 |
peerSelector | 用于通过标签来选择一组node(同样为Calico中的node),作为远端Peer的节点。如果指定了此字段,则peerIP和asNumber都应该为空。 |
keepOriginalNextHop | 对于EBGP,保持并转发原始的next hop,不将自身加入到Path中。 |
password | BGP会话的身份验证。 |
在过去的版本,Calico中包含了BGP Peer
对象和Global BGP Peer
对象,目前已统一为BGP Peer
对象,根据是指定node
参数还是nodeSelector
参数来区分。
BGP Configuration
除了BGP Peer外,Calico通过BGP Configuration对象来控制全局的BGP行为。主要参数包括:
参数 | 描述 | 默认值 |
---|---|---|
nodeToNodeMeshEnabled | 开启Calico节点之间的node-to-node mesh。 | true |
asNumber | Calico node默认的节点AS。 | 64512 |
serviceClusterIPs | Calico需要对外BGP的service ClusterIP地址段。 | |
serviceExternalIPs | Calico需要对外BGP的service ExternalIPs地址段。 | |
communities | 用于定义BGP community,由name和value组成,value支持标准community以及large community。 | |
prefixAdvertisements | 指定网段与community的隶属关系,可以通过communities中的name指定,也可以通过community value直接指定。 |
默认情况下,Calico所有节点通过IBGP来交换各个节点的workload(容器)路由信息,由于从IBGP Peer中学习到的路由不会被再次转发,因此需要使用node-to-node mesh的方式两两互连。
serviceClusterIPs
和serviceExternalIPs
字段的功能类似于MetalLB的BGP模式,可以将K8s Service的访问地址(ClusterIP和ExternalIP)BGP到集群外的设备(例如TOR)。结合ECMP,可以将外部访问K8s Service的流量负载到K8s节点上,由Kube-proxy转发到真正的容器后端。
communities
与prefixAdvertisements
可以控制Calico BGP路由的community字段,支持RFC 1997中的well-known communities
。使用样例如下:
communities:
- name: bgp-large-community
value: 63400:300:100
prefixAdvertisements:
- cidr: 172.218.4.0/26
communities:
- bgp-large-community
- 63400:120
Calico BGP 功能实现
简单来说,Calico是通过confd组件来渲染bird的配置文件,动态配置bird,从而实现BGP功能。其中,为了使confd支持Calico后端存储,在原生的confd上,添加了类型为Calico的backend,主要逻辑是watch后端BGPPeer、BGPConfiguration、Node资源(由libcalico-go中的bgpsyncer
实现),缓存到内存中,供confd渲染使用。
可以看到,主要的配置文件分为6个,其中IPv4和IPv6各3个。以IPv4为例,IPv6类似,bird.cfg为bird的主要配置文件,包括几个主要部分:
1)全局的配置:包括route id、debug属性、listen bgp port,这一部分主要由node、bgp configuration中的字段产生。
2)固定的协议配置:包括kernel、device、direct;其功能在上一篇Calico BGP功能介绍:BIRD简介中有详细介绍。
# Configure synchronization between routing tables and kernel.
protocol kernel {
learn; # Learn all alien routes from the kernel
persist; # Don't remove routes on bird shutdown
scan time 2; # Scan kernel routing table every 2 seconds
import all;
export filter calico_kernel_programming; # Default is export none
graceful restart; # Turn on graceful restart to reduce potential flaps in
# routes when reloading BIRD configuration. With a full
# automatic mesh, there is no way to prevent BGP from
# flapping since multiple nodes update their BGP
# configuration at the same time, GR is not guaranteed to
# work correctly in this scenario.
merge paths on; # Allow export multipath routes (ECMP)
}
# Watch interface up/down events.
protocol device {
{{- template "LOGGING"}}
scan time 2; # Scan interfaces every 2 seconds
}
protocol direct {
{{- template "LOGGING"}}
interface -"cali*", -"kube-ipvs*", "*"; # Exclude cali* and kube-ipvs* but
# include everything else. In
# IPVS-mode, kube-proxy creates a
# kube-ipvs0 interface. We exclude
# kube-ipvs0 because this interface
# gets an address for every in use
# cluster IP. We use static routes
# for when we legitimately want to
# export cluster IPs.
}
kernel中export用到了filter calico_kernel_programming
,其定义在bird_ipam.cfg.template中。主要分两部分:
一是获取/calico/rejectcidrs
的值,对属于此cidr范围内的路由reject。/calico/rejectcidrs
的值是由confd的calico backend
写入,其值为BGP Configuration中设置的serviceClusterIPs
和serviceExternalIPs
。在开启了Calico的advertise service功能后(通过配置BGP Configuration的serviceClusterIPs
和serviceClusterIPs
),可以避免将其他节点发送过来的Service路由写入kernel路由表中。
二是对于属于Calico IPPool的路由,根据Calico IPPool设置的模式(VXLAN或IPIP),来判断是否需要写入kernel路由表。对于VXLAN模式,由felix负责数据包的路由,不再写入kernel中;对于IPIP,根据Calico IPPool的IPIP配置(Always、Cross-subnet)以及BGP协议的bgp_next_hop属性(判断是否跨网段),决定是生成IPIP的路由,还是非IPIP路由。这里使用到的bird参数krt_tunnel
来传递IPIP设备,krt_tunnel
并非原生bird所提供的参数,而是由Calico添加的,以实现bird支持IPIP协议。
{{$network_key := printf "/bgp/v1/host/%s/network_v4" (getenv "NODENAME")}}
filter calico_kernel_programming {
{{- $reject_key := "/rejectcidrs"}}
{{- if ls $reject_key}}
# Don't program static routes into kernel.
{{- range ls $reject_key}}
{{- $parts := split . "-"}}
{{- $cidr := join $parts "/"}}
if ( net ~ {{$cidr}} ) then { reject; }
{{- end}}
{{- end}}
{{- if exists $network_key}}{{$network := getv $network_key}}
{{range ls "/v1/ipam/v4/pool"}}{{$data := json (getv (printf "/v1/ipam/v4/pool/%s" .))}}
if ( net ~ {{$data.cidr}} ) then {
{{- if $data.vxlan_mode}}
# Don't program VXLAN routes into the kernel - these are handled by Felix.
reject;
}
{{- else if $data.ipip_mode}}{{if eq $data.ipip_mode "cross-subnet"}}
if defined(bgp_next_hop) && ( bgp_next_hop ~ {{$network}} ) then
krt_tunnel = ""; {{- /* Destination in ipPool, mode is cross sub-net, route from-host on subnet, do not use IPIP */}}
else
krt_tunnel = "{{$data.ipip}}"; {{- /* Destination in ipPool, mode is cross sub-net, route from-host off subnet, set the tunnel (if IPIP not enabled, value will be "") */}}
accept;
} {{- else}}
krt_tunnel = "{{$data.ipip}}"; {{- /* Destination in ipPool, mode not cross sub-net, set the tunnel (if IPIP not enabled, value will be "") */}}
accept;
} {{- end}} {{- else}}
krt_tunnel = "{{$data.ipip}}"; {{- /* Destination in ipPool, mode field is not present, set the tunnel (if IPIP not enabled, value will be "") */}}
accept;
} {{- end}}
{{end}}
{{- end}}{{/* End of 'exists $network_key' */}}
accept; {{- /* Destination is not in any ipPool, accept */}}
}
3)BGP协议部分。
BGP协议部分是最主要的部分,使用了bird的template,template中export方向使用filter calico_export_to_bgp_peers
过滤,其定义在bird_ipam.cfg.template中,主要功能是:先调用bird.cfg.template中的apply_communities()
方法(根据BGP Configuration中的communities
和prefixAdvertisements
字段),为发送的BGP路由添加community参数;调用bird_aggr.cfg.template中的calico_aggr()
方法,确保宣告的BGP路由的目标地址段为完整的block;最后,判断路由的目标地址是否在/calico/staticroutes
或Calico IPPool所指定的地址范围内,若在,则accept,其他的reject。
Calico中block是容器IP资源池最小的分配单位,最初Calico会为每个节点分配一个block,当某个节点block使用完,则会再次为节点分配一个block
filter calico_export_to_bgp_peers {
# filter code terminates when it calls `accept;` or `reject;`, call apply_communities() before calico_aggr()
apply_communities();
calico_aggr();
{{- $static_key := "/staticroutes"}}
{{- if ls $static_key}}
# Export static routes.
{{- range ls $static_key}}
{{- $parts := split . "-"}}
{{- $cidr := join $parts "/"}}
if ( net ~ {{$cidr}} ) then { accept; }
{{- end}}
{{- end}}
{{range ls "/v1/ipam/v4/pool"}}{{$data := json (getv (printf "/v1/ipam/v4/pool/%s" .))}}
if ( net ~ {{$data.cidr}} ) then {
accept;
}
{{- end}}
reject;
}
/calico/staticroutes
中的值也是由confd的calico backend
写入,其值主要包含两部分:BGP Configuration中设置的serviceClusterIPs
和serviceExternalIPs
;externalTrafficPolicy=Local
的Service地址。因此filter calico_export_to_bgp_peers
保证了Calico只对容器网络相关的路由进行BGP,对于手动配置或从其他EBGP学习到的非容器网络相关的路由,则不会进行BGP。
BGP协议部分主要分三部分:node-to-node mesh的配置、global peers的配置、node-specific peers的配置,都是使用上面的template完成。
其中node-to-node mesh配置部分,会判断两个node ip,当远端peer的node ip“较大”时,开启passive
,也就是说,始终由“较大”IP的node发起BGP连接,保证mesh是单向的。
global peers配置和node-specific peers配置部分基本相同,会根据BGP Peer中的keepOriginalNextHop
、password
配置协议的next hop keep
以及password
属性,另外会根据本节点是否配置route reflector clusterID,决定是否开启rr client
(用于Calico route reflector模式)。
4)static协议部分在bird_aggr.cfg.template中,主要是将本机的block和/calico/staticroutes
中的值配置为Blackhole路由。这样一来,即可通过“bird路由表”,由BGP协议将本机的容器网络和Service网络的路由信息发送出去。而根据上面/calico/staticroutes
的介绍,对于externalTrafficPolicy=Cluster
的Service是以整个ServiceCIRD(BGP Configuration中设置的serviceClusterIPs
和serviceExternalIPs
)作为目标地址进行BGP的,对于externalTrafficPolicy=Local
的Service,则会判断本节点上是否有相应的workloadEndpoint,如果有,则以单个地址(子网掩码/32或/128)作为目标地址进行BGP。以此,Calico实现了对Service externalTrafficPolicy属性的支持。
{{- $block_key := printf "/calico/ipam/v2/host/%s/ipv4/block" (getenv "NODENAME")}}
{{- $static_key := "/calico/staticroutes"}}
{{if or (ls $block_key) (ls $static_key)}}
protocol static {
{{- if ls $block_key}}
# IP blocks for this host.
{{- range ls $block_key}}
{{- $parts := split . "-"}}
{{- $cidr := join $parts "/"}}
route {{$cidr}} blackhole;
{{- end}}
{{- end}}
{{- if ls $static_key}}
# Static routes.
{{- range ls $static_key}}
{{- $parts := split . "-"}}
{{- $cidr := join $parts "/"}}
route {{$cidr}} blackhole;
{{- end}}
{{- end}}
}
{{else}}# No IP blocks or static routes for this host.{{end}}
这里有个问题,由于kernel中的export filter过滤了目标地址属于BGP Configuration的serviceClusterIPs
和serviceClusterIPs
的路由,实际上static协议中的/calico/staticroutes
部分的Blackhole是无法配置到节点的路由表上的(externalTrafficPolicy=Cluster
的Service地址也在这个范围内)。这会导致在使用Calico与TOR进行BGP的场景中,在开启了advertise service后,容器访问某个属于ServiceCIDR范围内,但并未分配给任何Service的地址时,数据包会根据默认路由发送到TOR,再由TOR发送回集群的某个节点,如此反复直至TTL消耗完。而如果尝试将ServiceCIDR对应的Blackhole路由写入系统的路由表中,可以解决这个问题,但又会导致宿主机对Service无法访问,流量直接被丢弃。
参考
https://github.com/projectcalico/confd/pull/322