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中。
passwordBGP会话的身份验证。

在过去的版本,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
asNumberCalico node默认的节点AS。64512
serviceClusterIPsCalico需要对外BGP的service ClusterIP地址段。
serviceExternalIPsCalico需要对外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的方式两两互连。

serviceClusterIPsserviceExternalIPs字段的功能类似于MetalLB的BGP模式,可以将K8s Service的访问地址(ClusterIP和ExternalIP)BGP到集群外的设备(例如TOR)。结合ECMP,可以将外部访问K8s Service的流量负载到K8s节点上,由Kube-proxy转发到真正的容器后端。

communitiesprefixAdvertisements可以控制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中设置的serviceClusterIPsserviceExternalIPs。在开启了Calico的advertise service功能后(通过配置BGP Configuration的serviceClusterIPsserviceClusterIPs),可以避免将其他节点发送过来的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中的communitiesprefixAdvertisements字段),为发送的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中设置的serviceClusterIPsserviceExternalIPsexternalTrafficPolicy=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中的keepOriginalNextHoppassword配置协议的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中设置的serviceClusterIPsserviceExternalIPs)作为目标地址进行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的serviceClusterIPsserviceClusterIPs的路由,实际上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

https://github.com/projectcalico/confd

https://github.com/projectcalico/calico/issues/3689