在 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 条明细网段前缀。Active或Idle:连接失败。 网络不通或对端端口没开。(Policy):被策略拦截。 BGP 认为这不是合法的内部路由,需检查是否已执行no bgp ebgp-requires-policy。
2. 查看详细路由表:show ip bgp
检查路由前缀与下一跳:
\*>符号:代表这是一条有效且被优选的活跃路由。- 如果收到
0.0.0.0/0:说明程序端发送的 NLRI 解析失败。确保IPAddressPrefix结构体中的Prefix和PrefixLen被正确拆分传递。
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查阅详情。