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[1]中的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 简介[2]中有详细介绍。
代码语言:javascript复制# 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[3]功能后(通过配置 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。
代码语言:javascript复制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[4]
- https://github.com/projectcalico/confd[5]
- https://github.com/projectcalico/calico/issues/3689[6]
脚注
[1]RFC 1997: http://www.rfc-editor.org/info/rfc1997
[2]Calico BGP 功能介绍:BIRD 简介: https://maao.cloud/2021/01/26/Calico-BGP功能介绍:BIRD简介/
[3]advertise service: https://docs.projectcalico.org/networking/advertise-service-ips
[4]https://github.com/projectcalico/confd/pull/322: https://github.com/projectcalico/confd/pull/322
[5]https://github.com/projectcalico/confd: https://github.com/projectcalico/confd
[6]https://github.com/projectcalico/calico/issues/3689: https://github.com/projectcalico/calico/issues/3689
原文链接:https://maao.cloud/2021/02/23/Calico-BGP功能介绍:实现/