gobgp 本地调试

概要目的

本文主要提供本地开发调试 BGP 程序的一个技巧,同时学习 BGP 协议,以及 go 包 gobgp https://github.com/osrg/gobgp

学习 BGP 协议必要性:K8s 容器网络目前我们使用 Cilium/Calico,都是使用 BGP 来宣告路由,使得 pod/service cidr 在 K8s 之外可达。所以,想要了解容器网络,有必要了解 BGP。

BGP 基本概念

BGP <https://datatracker.ietf.org/doc/html/rfc4271 是一个建立在 TCP 上的边界网关协议,可以用来动态学习路由。>

在 K8s Cilium/Calico 网络插件生态里,主要使用 BGP 协议宣告 pod/service 路由给交换机,使得 pod/service ip 可以在 K8s 之外访问。

比如,下图是把目的地址为 service ip 的路由宣告给交换机,可以见青云的 Porter 项目 https://github.com/kubesphere/openelb/blob/master/doc/bgp-mode.md

该项目比较简单,通过 k8s 这一层获取 service ip,和定制选择几个 Node IP 作为下一跳Next Hop,构造成一个路由,宣告给交换机。当然,也可以直接把 service cidr 宣告给交换机而不是一个个 service ip:

image

porter 项目和 metallbhttps://github.com/metallb/metallb项目基本一致,主要区别在porter 使用强大的 gobgp 库来实现 bgphttps://github.com/osrg/gobgp,且通过 K8s CRD 来配置,而 metallb 自己实现的一个简陋版本的 BGP 实现。两者都自己实现了 IPAM。对比可以参考:https://github.com/kubesphere/openelb/blob/master/doc/zh/compared\_with\_metallb.md,

感觉porter 更友好点,不过做的事情几乎一样。

pod cidr 也是通过 BGP 协议宣告给交换机,目前我们 K8s 集群的 Cilium/Calico 使用 bird 程序(C语言写的) 来宣告每一个 pod cidr subnet 给交换机,本机 Node IP 作为下一跳。比如 pod cidr 为 10.20.30.40/19,集群有两个node 100.100.100.100、200.200.200.200 ,并且切分成两个 pod cidr

subnet10.20.30.40/20、10.20.30.41/20,则两个 node 上的 bird 分别会把类似如下路由宣告给交换机:

10.20.30.40/20 via 100.100.100.100
10.20.30.41/20 via 200.200.200.200

流量到了交换机那一侧,如果 pod ip 在10.20.30.40/20 子网内,则下一跳去100.100.100.100 node;如果 pod ip 在10.20.30.41/20 子网内,则下一跳去200.200.200.200 node。

调试 BGP 程序

需要本地开发一个 BGP 程序,比如类似 porter/metallb 那样的项目,但是没有真实的交换机/路由器,那就不太方便本地开发调试了,可以使用 gobgp <https://github.com/osrg/gobgp包来在本地运行作为 route server(类似交换机那一侧)。而且,建议使用强大的gobgp BGP 包,go 语言写的。>

或者也可以使用 bird,C 语言写的。不过程序需要集成 gobgp 包,也建议 gobgp。

先安装下 gobgpd 和 gobgp 两个 bin 文件,gobgp 是客户端:

wget -c https://github.com/osrg/gobgp/releases/download/v3.15.0/gobgp_3.15.0_linux_amd64.tar.gz

tar -xf -C /usr/local/bin gobgp_3.15.0_linux_amd64.tar.gz

然后本地运行 route-server,可以参考<https://github.com/osrg/gobgp/blob/master/docs/sources/route-server.md,配置文档 route-server-conf>.

conf 如下:

[global.config]
 as = 64512
 router-id = "1.1.1.1"
 port = 1790
 local-address-list = ["127.0.0.1"]
[[neighbors]]
 [neighbors.config]
 neighbor-address = "127.0.0.1"
 peer-as = 65001
 [neighbors.transport.config]
 remote-port = 1791
 passive-mode = true
 [neighbors.route-server.config]
 route-server-client = true

本地运行服务

gobgpd -f ./route-server-conf.conf -l debug --api-hosts ":50052" --pprof-disable

这时,以上 route server 就相当于交换机那一侧,等待 node 这一侧宣告路由过去。然后就是编写一个 bgp 程序,并使用 gobgp 包。一个简单的代码如

下所示,当然在可以加上k8s那一侧控制平面获取 pod/service cidr 等逻辑,类似 porter/metallb 项目。

测试代码里,本地这一侧会宣告一个路由: 目标地址 10.20.30.0/24 下一跳 30.30.30.30

package bgp

import (
 "context"
 "github.com/golang/protobuf/ptypes"
 api "github.com/osrg/gobgp/api"
 gobgp "github.com/osrg/gobgp/pkg/server"
 "google.golang.org/protobuf/types/known/anypb"
 "testing"
)

// gobgpd -f ./route-server-conf.conf -l debug --api-hosts ":50052" --pprof-disable
// gobgpd -f ./route-client-conf.conf -l debug --api-hosts ":50053" --pprof-disable
// nodegobgp -p 50053 -d global rib add -a ipv4 100.0.0.0/24 nexthop 20.20.20.20
// gobgp -p 50052 -d neighbor 127.0.0.1 adj-in
// gobgp -p 50052 -d global rib add -a ipv4 200.0.0.0/24 nexthop 20.20.20.20
// nodegobgp -p 50053 -d neighbor 127.0.0.1 adj-in
func TestRouteServer(test *testing.T) {
 log.SetLevel(log.DebugLevel)
 ch := make(chan struct{})

 // bgp1
 s := gobgp.NewBgpServer(gobgp.GrpcListenAddress(":50053"))
 go s.Serve()
 _ = s.StartBgp(context.Background(), &api.StartBgpRequest{
  Global: &api.Global{
   As:         65001,     // AS Number, NetOPS API AS Number
   RouterId:   "2.2.2.2", // IP
   ListenPort: 1791,
  },
 })
 defer s.StopBgp(context.Background(), &api.StopBgpRequest{})

 // route-server() bgp-server
 p1 := &api.Peer{
  Conf: &api.PeerConf{
   NeighborAddress: "127.0.0.1",
   PeerAs:          64512,
  },
  Transport: &api.Transport{
   RemotePort: 1790,
  },
 }
 _ = s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: p1})

 nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{
  Prefix:    "10.20.30.0",
  PrefixLen: 24,
 })
 a1, _ := ptypes.MarshalAny(&api.OriginAttribute{
  Origin: 0,
 })
 a2, _ := ptypes.MarshalAny(&api.NextHopAttribute{
  NextHop: "30.30.30.30",
 })
 attrs := []*anypb.Any{a1, a2}
 s.AddPath(context.TODO(), &api.AddPathRequest{
  Path: &api.Path{
   Family: &api.Family{Afi: api.Family_AFI_IP, Safi: api.Family_SAFI_UNICAST},
   Nlri:   nlri,
   Pattrs: attrs,
  },
 })

 <-ch
}

k8s 技能树

image