在 OpenWrt 系统中动态宣告 Kubernetes 集群路由

作者:Administrator 发布时间: 2026-02-20 阅读量:1 评论数:0

在 OpenWrt 系统中动态宣告 Kubernetes 集群路由

一、 在 OpenWrt 系统中配置 BGP 监听路由

OpenWrt 原生不包含 BGP 功能,我们需要依赖开源路由套件 FRRouting (FRR)

1. 安装与启动 FRR 核心组件

在 OpenWrt 终端执行以下命令,安装 BGP 所需的守护进程:

# 1. 更新软件源并安装核心包
opkg update
opkg install frr-zebra frr-bgpd frr-watchfrr frr-staticd

# 2. 开启 zebra 和 bgpd 守护进程
sed -i 's/bgpd=no/bgpd=yes/g' /etc/frr/daemons
sed -i 's/zebra=no/zebra=yes/g' /etc/frr/daemons

# 3. 启动 FRR 服务
/etc/init.d/frr enable
/etc/init.d/frr restart

2. 配置 BGP 动态邻居 (Dynamic Peering)

进入 FRR 专属的控制台进行配置:

vtysh

在控制台中依次输入以下命令(假设路由器 IP 为 192.168.6.1,路由器 ASN 为 65000,K8s 节点 ASN 为 65001):

configure terminal
router bgp 65000
  bgp router-id 192.168.6.1
  
  # 关闭 eBGP 强制策略要求(极度关键!否则会拒收路由并显示 Policy 状态)
  no bgp ebgp-requires-policy
  
  # 创建一个对等体组,分配给 K8s 集群使用
  neighbor K8S_NODES peer-group
  neighbor K8S_NODES remote-as 65001
  
  # 允许局域网内任意 IP 动态接入该 BGP 组
  bgp listen range 192.168.6.0/24 peer-group K8S_NODES
  
  exit
exit
write memory

⚠️ 关键清理: 配置完成后,必须删除 OpenWrt 中之前手动添加的指向 K8s 节点的旧静态路由(例如 ip route del 10.112.0.0/12 via 192.168.6.234),将路由权柄完全交给 BGP 管理。


二、 编写 Go 程序宣告 K8s 路由

该 Go 守护程序兼具 Informer 机制与内嵌 BGP 引擎,能同时支持在集群外(通过 kubeconfig)或集群内(作为 Pod 运行)读取 Node 数据,并实现 K8s Pod 网段向路由器的全自动动态宣告。

1. Go 核心程序代码

初始化工程:

mkdir k8s-bgp-speaker && cd k8s-bgp-speaker
go mod init k8s-bgp-speaker
go get k8s.io/client-go@latest k8s.io/api@latest k8s.io/apimachinery@latest github.com/osrg/gobgp/v3@latest google.golang.org/protobuf/types/known/anypb

创建 main.go

package main

import (
	"context"
	"flag"
	"log"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	api "github.com/osrg/gobgp/v3/api"
	bgpserver "github.com/osrg/gobgp/v3/pkg/server"
	"google.golang.org/protobuf/types/known/anypb"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

const (
	MyASN       = 65001
	MyRouterID  = "192.168.6.254" // 插件对外的伪装 IP
	iStoreOSIP  = "192.168.6.1"
	iStoreOSASN = 65000
)

func main() {
	ctx := context.Background()

	// 1. 初始化 GoBGP 服务
	s := bgpserver.NewBgpServer()
	go s.Serve()

	if err := s.StartBgp(ctx, &api.StartBgpRequest{
		Global: &api.Global{Asn: MyASN, RouterId: MyRouterID, ListenPort: -1},
	}); err != nil {
		log.Fatalf("BGP 启动失败: %v", err)
	}

	if err := s.AddPeer(ctx, &api.AddPeerRequest{
		Peer: &api.Peer{Conf: &api.PeerConf{NeighborAddress: iStoreOSIP, PeerAsn: iStoreOSASN}},
	}); err != nil {
		log.Fatalf("连接 OpenWrt 失败: %v", err)
	}

	// 2. 初始化 K8s 客户端 (优先尝试集群内配置,再尝试本地 kubeconfig)
	config, err := rest.InClusterConfig()
	if err != nil {
		var kubeconfig *string
		if home := homedir.HomeDir(); home != "" {
			kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "kubeconfig path")
		} else {
			kubeconfig = flag.String("kubeconfig", "", "kubeconfig path")
		}
		flag.Parse()
		config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
		if err != nil {
			log.Fatalf("构建 K8s 配置失败: %v", err)
		}
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		log.Fatalf("创建 K8s 客户端失败: %v", err)
	}

	// 3. 监听 Node 事件
	factory := informers.NewSharedInformerFactory(clientset, time.Minute*10)
	nodeInformer := factory.Core().V1().Nodes().Informer()

	nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) { handleNodeEvent(ctx, s, obj.(*corev1.Node), false) },
		UpdateFunc: func(old, new interface{}) {
			newNode, oldNode := new.(*corev1.Node), old.(*corev1.Node)
			if oldNode.Spec.PodCIDR != newNode.Spec.PodCIDR {
				handleNodeEvent(ctx, s, newNode, false)
			}
		},
		DeleteFunc: func(obj interface{}) { handleNodeEvent(ctx, s, obj.(*corev1.Node), true) },
	})

	stopCh := make(chan struct{})
	factory.Start(stopCh)
	factory.WaitForCacheSync(stopCh)
	<-stopCh
}

func handleNodeEvent(ctx context.Context, s *bgpserver.BgpServer, node *corev1.Node, isDelete bool) {
	if node.Spec.PodCIDR == "" {
		return
	}
	parts := strings.Split(node.Spec.PodCIDR, "/")
	if len(parts) != 2 {
		return
	}
	prefixLen, _ := strconv.Atoi(parts[1])

	var nextHop string
	for _, addr := range node.Status.Addresses {
		if addr.Type == corev1.NodeInternalIP {
			nextHop = addr.Address
			break
		}
	}
	if nextHop == "" {
		return
	}

	nlri, _ := anypb.New(&api.IPAddressPrefix{Prefix: parts[0], PrefixLen: uint32(prefixLen)})
	origin, _ := anypb.New(&api.OriginAttribute{Origin: 0})
	nh, _ := anypb.New(&api.NextHopAttribute{NextHop: nextHop})
	path := &api.Path{
		Family: &api.Family{Afi: api.Family_AFI_IP, Safi: api.Family_SAFI_UNICAST},
		Nlri:   nlri, Pattrs: []*anypb.Any{origin, nh},
	}

	if isDelete {
		_ = s.DeletePath(ctx, &api.DeletePathRequest{Path: path})
		log.Printf("[-] 撤销路由: %s via %s", node.Spec.PodCIDR, nextHop)
	} else {
		_, err := s.AddPath(ctx, &api.AddPathRequest{Path: path})
		if err == nil {
			log.Printf("[+] 宣告路由: %s via %s (Node: %s)", node.Spec.PodCIDR, nextHop, node.Name)
		}
	}
}

2. 生成专用的 kubeconfig(外部运行模式)

若要在集群外独立运行此程序,需要生成具备只读权限的 kubeconfig。 创建 rbac.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: bgp-speaker
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-reader
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: bgp-speaker-binding
subjects:
- kind: ServiceAccount
  name: bgp-speaker
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: node-reader
  apiGroup: rbac.authorization.k8s.io

提取 Token 组装 config:

kubectl apply -f rbac.yaml
kubectl create token bgp-speaker -n kube-system

3. Dockerfile 容器化(集群内运行模式)

编写 Dockerfile 实现多阶段构建,产出极小镜像:

FROM golang:1.21-alpine AS builder
WORKDIR /app
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o k8s-bgp-speaker main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
COPY --from=builder /app/k8s-bgp-speaker .
CMD ["./k8s-bgp-speaker"]

三、 BGP / vtysh 常见命令与运维排查指南

在 OpenWrt 终端输入 vtysh 进入交互界面。核心排障命令如下:

1. 查看邻居状态:show ip bgp summary

观察最右侧的 State/PfxRcd 列:

  • 数字 (例如 6)正常。 BGP 隧道已打通,且成功接收了 6 条明细网段前缀。
  • ActiveIdle连接失败。 网络不通或对端端口没开。
  • (Policy)被策略拦截。 BGP 认为这不是合法的内部路由,需检查是否已执行 no bgp ebgp-requires-policy

2. 查看详细路由表:show ip bgp

检查路由前缀与下一跳:

  • \*> 符号:代表这是一条有效且被优选的活跃路由。
  • 如果收到 0.0.0.0/0:说明程序端发送的 NLRI 解析失败。确保 IPAddressPrefix 结构体中的 PrefixPrefixLen 被正确拆分传递。

3. 重置 BGP 会话:clear ip bgp * soft

当修改了路由策略后,使用该命令发起“软重置”,强制两端重新交换路由表而不切断底层 TCP 握手连接。


四、 BGP 生效后路由规则解析 (Linux 内核)

当 Go 插件成功宣告路由后,FRR 会将路由注入 Linux 系统底层。在普通终端执行 ip route | grep 10.112,可见如下输出:

10.112.0.0/24 nhid 21 via 192.168.6.234 dev br-lan proto bgp metric 20
10.112.1.0/24 nhid 2291 via 192.168.6.126 dev br-lan proto bgp metric 20
10.112.7.0/24 nhid 2290 via 192.168.200.18 dev TENCENT_WG_BR proto bgp metric 20

1. 路由条目字段详解

10.112.7.0/24 nhid 2290 via 192.168.200.18 dev TENCENT_WG_BR proto bgp metric 20 为例:

  • 10.112.7.0/24 (目标网段):K8s 节点(如跨云端节点)分配到的专属容器网段。
  • via 192.168.200.18 (下一跳):前往上述网段,必须将包送至该宿主机的物理/隧道 IP。
  • dev TENCENT_WG_BR (出站接口):Linux 自动计算出的出网网卡。通过 BGP,内核精确识别出本地局域网走 br-lan,而远端节点走 WireGuard 隧道,彻底消除了手工维护静态路由的梦魇。
  • proto bgp (路由来源):标记该路由由 FRR 动态下发。若 Go 程序下线或 K8s 节点宕机,该路由将被瞬间撤销,防止流量黑洞。
  • metric 20:eBGP 路由的默认优先级。

2. nhid (Next-Hop ID) 的作用

现代 Linux 内核(5.3+)引入了 Nexthop Objects(下一跳对象)以优化路由表性能。

  • nhid 2290 是一个独立的内存对象,存储了下一跳的网关和网卡信息。
  • 当有大量路由指向同一网关时,只需复用该 ID 即可。若网关状态变更,内核只需修改这一处 nhid 对象,所有引用它的路由均自动生效。可通过 ip nexthop show 查阅详情。

评论