From e548092064e934853d963872dd8f75b0593bfd88 Mon Sep 17 00:00:00 2001 From: Cat C Date: Mon, 8 Sep 2025 22:42:11 -0700 Subject: [PATCH 1/5] Consolidate node peer annotations to address #1393 --- go.mod | 3 +- go.sum | 2 + .../routing/network_routes_controller.go | 293 ++++++++++++------ .../routing/network_routes_controller_test.go | 155 ++++++++- pkg/controllers/routing/utils.go | 20 +- 5 files changed, 366 insertions(+), 107 deletions(-) diff --git a/go.mod b/go.mod index 1ac5d85b26..c73e51b26e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/ccoveille/go-safecast v1.6.1 github.com/coreos/go-iptables v0.8.0 github.com/docker/docker v28.4.0+incompatible + github.com/goccy/go-yaml v1.18.0 + github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-version v1.7.0 github.com/moby/ipvs v1.1.0 github.com/onsi/ginkgo v1.16.5 @@ -65,7 +67,6 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index 18df6bc7e3..97b4ee560d 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/pkg/controllers/routing/network_routes_controller.go b/pkg/controllers/routing/network_routes_controller.go index 08d0fca4a7..c60a9d570d 100644 --- a/pkg/controllers/routing/network_routes_controller.go +++ b/pkg/controllers/routing/network_routes_controller.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/goccy/go-yaml" "google.golang.org/protobuf/types/known/anypb" "github.com/ccoveille/go-safecast" @@ -43,12 +44,14 @@ const ( nodeCustomImportRejectAnnotation = "kube-router.io/node.bgp.customimportreject" pathPrependASNAnnotation = "kube-router.io/path-prepend.as" pathPrependRepeatNAnnotation = "kube-router.io/path-prepend.repeat-n" - peerASNAnnotation = "kube-router.io/peer.asns" - peerIPAnnotation = "kube-router.io/peer.ips" - peerLocalIPAnnotation = "kube-router.io/peer.localips" + + peerASNAnnotation = "kube-router.io/peer.asns" + peerIPAnnotation = "kube-router.io/peer.ips" + peerLocalIPAnnotation = "kube-router.io/peer.localips" //nolint:gosec // this is not a hardcoded password peerPasswordAnnotation = "kube-router.io/peer.passwords" peerPortAnnotation = "kube-router.io/peer.ports" + peersAnnotation = "kube-router.io/peers" rrClientAnnotation = "kube-router.io/rr.client" rrServerAnnotation = "kube-router.io/rr.server" svcLocalAnnotation = "kube-router.io/service.local" @@ -155,7 +158,8 @@ type NetworkRoutingController struct { // Run runs forever until we are notified on stop channel func (nrc *NetworkRoutingController) Run(healthChan chan<- *healthcheck.ControllerHeartbeat, stopCh <-chan struct{}, - wg *sync.WaitGroup) { + wg *sync.WaitGroup, +) { var err error if nrc.enableCNI { nrc.updateCNIConfig() @@ -1084,106 +1088,22 @@ func (nrc *NetworkRoutingController) startBgpServer(grpcServer bool) error { // If the global routing peer is configured then peer with it // else attempt to get peers from node specific BGP annotations. if len(nrc.globalPeerRouters) == 0 { - // Get Global Peer Router ASN configs - nodeBgpPeerAsnsAnnotation, ok := node.Annotations[peerASNAnnotation] - if !ok { - klog.Infof("Could not find BGP peer info for the node in the node annotations so " + - "skipping configuring peer.") - return nil - } - - asnStrings := stringToSlice(nodeBgpPeerAsnsAnnotation, ",") - peerASNs, err := stringSliceToUInt32(asnStrings) + klog.V(2).Infof("Attempting to construct peer configs from annotation: %+v", node.Annotations) + peerCfgs, err := bgpPeerConfigsFromAnnotations(node.Annotations) if err != nil { err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) if err2 != nil { klog.Errorf("Failed to stop bgpServer: %s", err2) } - return fmt.Errorf("failed to parse node's Peer ASN Numbers Annotation: %s", err) + return err } - - // Get Global Peer Router IP Address configs - nodeBgpPeersAnnotation, ok := node.Annotations[peerIPAnnotation] - if !ok { - klog.Infof("Could not find BGP peer info for the node in the node annotations " + - "so skipping configuring peer.") + // Early exist because no BGP peer info was set in annotations for the node + if peerCfgs == nil { return nil } - ipStrings := stringToSlice(nodeBgpPeersAnnotation, ",") - peerIPs, err := stringSliceToIPs(ipStrings) - if err != nil { - err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) - if err2 != nil { - klog.Errorf("Failed to stop bgpServer: %s", err2) - } - - return fmt.Errorf("failed to parse node's Peer Addresses Annotation: %s", err) - } - - // Get Global Peer Router ASN configs - nodeBgpPeerPortsAnnotation, ok := node.Annotations[peerPortAnnotation] - // Default to default BGP port if port annotation is not found - var peerPorts = make([]uint32, 0) - if ok { - portStrings := stringToSlice(nodeBgpPeerPortsAnnotation, ",") - peerPorts, err = stringSliceToUInt32(portStrings) - if err != nil { - err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) - if err2 != nil { - klog.Errorf("Failed to stop bgpServer: %s", err2) - } - return fmt.Errorf("failed to parse node's Peer Port Numbers Annotation: %s", err) - } - } - - // Get Global Peer Router Password configs - var peerPasswords []string - nodeBGPPasswordsAnnotation, ok := node.Annotations[peerPasswordAnnotation] - if !ok { - klog.Infof("Could not find BGP peer password info in the node's annotations. Assuming no passwords.") - } else { - passStrings := stringToSlice(nodeBGPPasswordsAnnotation, ",") - peerPasswords, err = stringSliceB64Decode(passStrings) - if err != nil { - err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) - if err2 != nil { - klog.Errorf("Failed to stop bgpServer: %s", err2) - } - return fmt.Errorf("failed to parse node's Peer Passwords Annotation") - } - } - - // Get Global Peer Router LocalIP configs - var peerLocalIPs []string - nodeBGPPeerLocalIPs, ok := node.Annotations[peerLocalIPAnnotation] - if !ok { - klog.Infof("Could not find BGP peer local ip info in the node's annotations. Assuming node IP.") - } else { - peerLocalIPs = stringToSlice(nodeBGPPeerLocalIPs, ",") - err = func() error { - for _, s := range peerLocalIPs { - if s != "" { - ip := net.ParseIP(s) - if ip == nil { - return fmt.Errorf("could not parse \"%s\" as an IP", s) - } - } - } - - return nil - }() - if err != nil { - err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) - if err2 != nil { - klog.Errorf("Failed to stop bgpServer: %s", err2) - } - - return fmt.Errorf("failed to parse node's Peer Local Addresses Annotation: %s", err) - } - } // Create and set Global Peer Router complete configs - nrc.globalPeerRouters, err = newGlobalPeers(peerIPs, peerPorts, peerASNs, peerPasswords, peerLocalIPs, + nrc.globalPeerRouters, err = newGlobalPeers(peerCfgs.RemoteIPs(), peerCfgs.Ports(), peerCfgs.RemoteASNs(), peerCfgs.Passwords(), peerCfgs.LocalIPs(), nrc.bgpHoldtime, nrc.krNode.GetPrimaryNodeIP().String()) if err != nil { err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) @@ -1194,7 +1114,7 @@ func (nrc *NetworkRoutingController) startBgpServer(grpcServer bool) error { return fmt.Errorf("failed to process Global Peer Router configs: %s", err) } - nrc.nodePeerRouters = ipStrings + nrc.nodePeerRouters = peerCfgs.RemoteIPStrings() } if len(nrc.globalPeerRouters) != 0 { @@ -1270,8 +1190,8 @@ func (nrc *NetworkRoutingController) setupHandlers(node *v1core.Node) error { func NewNetworkRoutingController(clientset kubernetes.Interface, kubeRouterConfig *options.KubeRouterConfig, nodeInformer cache.SharedIndexInformer, svcInformer cache.SharedIndexInformer, - epSliceInformer cache.SharedIndexInformer, ipsetMutex *sync.Mutex) (*NetworkRoutingController, error) { - + epSliceInformer cache.SharedIndexInformer, ipsetMutex *sync.Mutex, +) (*NetworkRoutingController, error) { var err error nrc := NetworkRoutingController{ipsetMutex: ipsetMutex} @@ -1496,3 +1416,182 @@ func NewNetworkRoutingController(clientset kubernetes.Interface, return &nrc, nil } + +type bgpPeerConfig struct { + LocalIP *string `yaml:"localip"` + Password *base64String `yaml:"password"` + Port *uint32 `yaml:"port"` + RemoteASN *uint32 `yaml:"remoteasn"` + RemoteIP *net.IP `yaml:"remoteip"` + remoteIPString string +} + +type bgpPeerConfigs []bgpPeerConfig + +func (b bgpPeerConfigs) LocalIPs() []string { + localIPs := make([]string, 0) + for _, cfg := range b { + if cfg.LocalIP != nil { + localIPs = append(localIPs, *cfg.LocalIP) + } + } + return localIPs +} + +func (b bgpPeerConfigs) Passwords() []string { + passwords := make([]string, 0) + for _, cfg := range b { + if cfg.Password != nil { + passwords = append(passwords, string(*cfg.Password)) + } + } + return passwords +} + +func (b bgpPeerConfigs) Ports() []uint32 { + ports := make([]uint32, 0) + for _, cfg := range b { + if cfg.Port != nil { + ports = append(ports, *cfg.Port) + } + } + return ports +} + +func (b bgpPeerConfigs) RemoteASNs() []uint32 { + asns := make([]uint32, 0) + for _, cfg := range b { + if cfg.RemoteASN != nil { + asns = append(asns, *cfg.RemoteASN) + } + } + return asns +} + +func (b bgpPeerConfigs) RemoteIPs() []net.IP { + remoteIPs := make([]net.IP, 0) + for _, cfg := range b { + if cfg.RemoteIP != nil { + remoteIPs = append(remoteIPs, *cfg.RemoteIP) + } + } + return remoteIPs +} + +func (b bgpPeerConfigs) RemoteIPStrings() []string { + remoteIPs := make([]string, 0) + for _, cfg := range b { + if cfg.remoteIPString != "" { + remoteIPs = append(remoteIPs, cfg.remoteIPString) + } + } + return remoteIPs +} + +func bgpPeerConfigsFromAnnotations(nodeAnnotations map[string]string) (bgpPeerConfigs, error) { + nodeBgpPeersAnnotation, ok := nodeAnnotations[peersAnnotation] + if !ok { + klog.Infof("%s annotation not set, using individual node annotations to configure BGP peer info", peersAnnotation) + return bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations) + } + + var peerConfigs []bgpPeerConfig + if err := yaml.Unmarshal([]byte(nodeBgpPeersAnnotation), &peerConfigs); err != nil { + return nil, fmt.Errorf("failed to parse %s annotation: %w", peersAnnotation, err) + } + klog.Infof("Peer config from %s annotation: %+v", peersAnnotation, peerConfigs) + return peerConfigs, nil +} + +func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) (bgpPeerConfigs, error) { + // Get Global Peer Router ASN configs + nodeBgpPeerAsnsAnnotation, ok := nodeAnnotations[peerASNAnnotation] + if !ok { + klog.Infof("Could not find BGP peer info for the node in the node annotations so " + + "skipping configuring peer.") + return nil, nil + } + asnStrings := stringToSlice(nodeBgpPeerAsnsAnnotation, ",") + peerASNs, err := stringSliceToUInt32(asnStrings) + if err != nil { + return nil, fmt.Errorf("failed to parse node's Peer ASN Numbers Annotation: %w", err) + } + peerConfigs := make([]bgpPeerConfig, len(peerASNs)) + for i, peerASN := range peerASNs { + peerConfigs[i].RemoteASN = &peerASN + } + + // Get Global Peer Router IP Address configs + nodeBgpPeersAnnotation, ok := nodeAnnotations[peerIPAnnotation] + if !ok { + klog.Infof("Could not find BGP peer info for the node in the node annotations " + + "so skipping configuring peer.") + return nil, nil + } + ipStrings := stringToSlice(nodeBgpPeersAnnotation, ",") + peerIPs, err := stringSliceToIPs(ipStrings) + if err != nil { + return nil, fmt.Errorf("failed to parse node's Peer Addresses Annotation: %w", err) + } + for i, peerIP := range peerIPs { + peerConfigs[i].remoteIPString = ipStrings[i] + peerConfigs[i].RemoteIP = &peerIP + } + + // Get Global Peer Router ASN configs + nodeBgpPeerPortsAnnotation, ok := nodeAnnotations[peerPortAnnotation] + // Default to default BGP port if port annotation is not found + if ok { + var ports []uint32 + portStrings := stringToSlice(nodeBgpPeerPortsAnnotation, ",") + ports, err = stringSliceToUInt32(portStrings) + if err != nil { + return nil, fmt.Errorf("failed to parse node's Peer Port Numbers Annotation: %w", err) + } + for i, port := range ports { + peerConfigs[i].Port = &port + } + } + + // Get Global Peer Router Password configs + nodeBGPPasswordsAnnotation, ok := nodeAnnotations[peerPasswordAnnotation] + if !ok { + klog.Infof("Could not find BGP peer password info in the node's annotations. Assuming no passwords.") + } else { + var passwords []string + passStrings := stringToSlice(nodeBGPPasswordsAnnotation, ",") + passwords, err = stringSliceB64Decode(passStrings) + if err != nil { + return nil, fmt.Errorf("failed to parse node's Peer Passwords Annotation: %w", err) + } + for i, password := range passwords { + bpassword := base64String(password) + peerConfigs[i].Password = &bpassword + } + } + + // Get Global Peer Router LocalIP configs + nodeBGPPeerLocalIPs, ok := nodeAnnotations[peerLocalIPAnnotation] + if !ok { + klog.Infof("Could not find BGP peer local ip info in the node's annotations. Assuming node IP.") + } else { + localIPs := stringToSlice(nodeBGPPeerLocalIPs, ",") + err = func() error { + for i, s := range localIPs { + if s != "" { + ip := net.ParseIP(s) + if ip == nil { + return fmt.Errorf("could not parse \"%s\" as an IP", s) + } + peerConfigs[i].LocalIP = &s + } + } + return nil + }() + if err != nil { + return nil, fmt.Errorf("failed to parse node's Peer Local Addresses Annotation: %w", err) + } + } + + return peerConfigs, nil +} diff --git a/pkg/controllers/routing/network_routes_controller_test.go b/pkg/controllers/routing/network_routes_controller_test.go index 66ac257567..0f9532b407 100644 --- a/pkg/controllers/routing/network_routes_controller_test.go +++ b/pkg/controllers/routing/network_routes_controller_test.go @@ -12,7 +12,11 @@ import ( "github.com/cloudnativelabs/kube-router/v2/pkg/k8s/indexers" "github.com/cloudnativelabs/kube-router/v2/pkg/utils" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" . "github.com/onsi/ginkgo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" v1core "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -34,7 +38,6 @@ func Test_advertiseClusterIPs(t *testing.T) { // the key is the subnet from the watch event watchEvents map[string]bool }{ - { "add bgp path for service with ClusterIP", &NetworkRoutingController{ @@ -1818,11 +1821,11 @@ func Test_nodeHasEndpointsForService(t *testing.T) { Endpoints: []discoveryv1.Endpoint{ { Addresses: []string{"172.20.1.1"}, - NodeName: ptrToString("node-1"), + NodeName: valToPtr("node-1"), }, { Addresses: []string{"172.20.1.2"}, - NodeName: ptrToString("node-2"), + NodeName: valToPtr("node-2"), }, }, }, @@ -1863,11 +1866,11 @@ func Test_nodeHasEndpointsForService(t *testing.T) { Endpoints: []discoveryv1.Endpoint{ { Addresses: []string{"172.20.1.1"}, - NodeName: ptrToString("node-2"), + NodeName: valToPtr("node-2"), }, { Addresses: []string{"172.20.1.2"}, - NodeName: ptrToString("node-3"), + NodeName: valToPtr("node-3"), }, }, }, @@ -1902,7 +1905,6 @@ func Test_nodeHasEndpointsForService(t *testing.T) { t.Logf("actual nodeHasEndpoints: %v", nodeHasEndpoints) t.Error("unexpected nodeHasEndpoints") } - }) } } @@ -2618,7 +2620,133 @@ func Test_routeReflectorConfiguration(t *testing.T) { } }) } +} + +func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { + testCases := []struct { + name string + nodeAnnotations map[string]string + expectedBgpPeerConfigs bgpPeerConfigs + expectError bool + }{ + { + "node annotations are empty", + map[string]string{}, + nil, + false, + }, + { + "combined bgp peers config annotation", + map[string]string{ + peersAnnotation: `- remoteip: 10.0.0.1 + remoteasn: 64640 + password: cGFzc3dvcmQ= + localip: 192.168.0.1 +- remoteip: 10.0.0.2 + remoteasn: 64641 + password: cGFzc3dvcmQ= + localip: 192.168.0.2`, + }, + bgpPeerConfigs{ + bgpPeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.1")), + RemoteASN: valToPtr(uint32(64640)), + Password: valToPtr(base64String("password")), + LocalIP: valToPtr("192.168.0.1"), + }, + bgpPeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.2")), + RemoteASN: valToPtr(uint32(64641)), + Password: valToPtr(base64String("password")), + LocalIP: valToPtr("192.168.0.2"), + }, + }, + false, + }, + { + "combined bgp peers config annotation without all fields set", + map[string]string{ + peersAnnotation: `- remoteip: 10.0.0.1 + remoteasn: 64640 +- remoteip: 10.0.0.2 + remoteasn: 64641 + password: cGFzc3dvcmQ= + localip: 192.168.0.2`, + }, + bgpPeerConfigs{ + bgpPeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.1")), + RemoteASN: valToPtr(uint32(64640)), + }, + bgpPeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.2")), + RemoteASN: valToPtr(uint32(64641)), + Password: valToPtr(base64String("password")), + LocalIP: valToPtr("192.168.0.2"), + }, + }, + false, + }, + { + "individual bgp peers config annotations", + map[string]string{ + peerIPAnnotation: "10.0.0.1,10.0.0.2", + peerASNAnnotation: "64640,64641", + peerPasswordAnnotation: "cGFzc3dvcmQ=,cGFzc3dvcmQ=", + peerLocalIPAnnotation: "192.168.0.1,192.168.0.2", + }, + bgpPeerConfigs{ + bgpPeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.1")), + RemoteASN: valToPtr(uint32(64640)), + Password: valToPtr(base64String("password")), + LocalIP: valToPtr("192.168.0.1"), + }, + bgpPeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.2")), + RemoteASN: valToPtr(uint32(64641)), + Password: valToPtr(base64String("password")), + LocalIP: valToPtr("192.168.0.2"), + }, + }, + false, + }, + { + "individual bgp peers config annotations without peer ASN annotation", + map[string]string{ + peerASNAnnotation: "64640,64641", + peerPasswordAnnotation: "cGFzc3dvcmQ=,cGFzc3dvcmQ=", + peerLocalIPAnnotation: "192.168.0.1,192.168.0.2", + }, + nil, + false, + }, + { + "individual bgp peers config annotations without peer IP annotation", + map[string]string{ + peerIPAnnotation: "10.0.0.1,10.0.0.2", + peerPasswordAnnotation: "cGFzc3dvcmQ=,cGFzc3dvcmQ=", + peerLocalIPAnnotation: "192.168.0.1,192.168.0.2", + }, + nil, + false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bgpPeerCfgs, err := bgpPeerConfigsFromAnnotations(tc.nodeAnnotations) + if tc.expectError { + assert.Error(t, err) + return + } + require.NoError(t, err) + if !cmp.Equal(tc.expectedBgpPeerConfigs, bgpPeerCfgs, cmpopts.IgnoreUnexported(bgpPeerConfig{})) { + diff := cmp.Diff(tc.expectedBgpPeerConfigs, bgpPeerCfgs, cmpopts.IgnoreUnexported(bgpPeerConfig{})) + t.Errorf("BGP peer config mismatch:\n%s", diff) + } + }) + } } /* Disabling test for now. OnNodeUpdate() behaviour is changed. test needs to be adopted. @@ -2866,6 +2994,17 @@ func waitForListerWithTimeout(lister cache.Indexer, timeout time.Duration, t *te } } -func ptrToString(str string) *string { - return &str +type value interface { + string | uint32 | net.IP | base64String +} + +func valToPtr[V value](v V) *V { + return &v +} + +func ptrToVal[V value](v *V) V { + if v == nil { + return *new(V) + } + return *v } diff --git a/pkg/controllers/routing/utils.go b/pkg/controllers/routing/utils.go index 5c904afe56..a3bfbd5005 100644 --- a/pkg/controllers/routing/utils.go +++ b/pkg/controllers/routing/utils.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/goccy/go-yaml" gobgpapi "github.com/osrg/gobgp/v3/api" v1core "k8s.io/api/core/v1" @@ -110,6 +111,22 @@ func statementsEqualByName(a, b []*gobgpapi.Statement) bool { return true } +// Wrapper type to automatically handles decoding b64 encoded strings upon unmarshalling +type base64String string + +func (b *base64String) UnmarshalYAML(raw []byte) error { + var tmp string + if err := yaml.Unmarshal(raw, &tmp); err != nil { + return fmt.Errorf("failed to unmarshal string into base64string type: %w", err) + } + decoded, err := base64.StdEncoding.DecodeString(tmp) + if err != nil { + return fmt.Errorf("failed to base64 decode field: %w", err) + } + *b = base64String(string(decoded)) + return nil +} + // getPodCIDRsFromAllNodeSources gets the pod CIDRs for all available sources on a given node in a specific order. The // order of preference is: // 1. From the kube-router.io/pod-cidr annotation (preserves backwards compatibility) @@ -155,7 +172,8 @@ func getPodCIDRsFromAllNodeSources(node *v1core.Node) (podCIDRs []string) { // based upon whether it is an IPv4 address or an IPv6 address. Returns slash notation subnet as uint32 suitable for // sending to GoBGP and an error if it is unable to determine the subnet automatically func (nrc *NetworkRoutingController) getBGPRouteInfoForVIP(vip string) (subnet uint32, nh string, - afiFamily gobgpapi.Family_Afi, err error) { + afiFamily gobgpapi.Family_Afi, err error, +) { ip := net.ParseIP(vip) if ip == nil { err = fmt.Errorf("could not parse VIP: %s", vip) From 9fefd3b7fbd8cbf455d8ccb2a5fa49ea936f60bf Mon Sep 17 00:00:00 2001 From: Cat C Date: Sat, 11 Oct 2025 12:53:15 -0700 Subject: [PATCH 2/5] Re-organize structure of bgpPeerConfigs into separate packages --- pkg/bgp/peer_config.go | 107 ++++++++++++++++++ .../routing/network_routes_controller.go | 87 ++------------ .../routing/network_routes_controller_test.go | 37 +++--- pkg/controllers/routing/utils.go | 17 --- pkg/utils/base64.go | 25 ++++ 5 files changed, 160 insertions(+), 113 deletions(-) create mode 100644 pkg/bgp/peer_config.go create mode 100644 pkg/utils/base64.go diff --git a/pkg/bgp/peer_config.go b/pkg/bgp/peer_config.go new file mode 100644 index 0000000000..c9e658a886 --- /dev/null +++ b/pkg/bgp/peer_config.go @@ -0,0 +1,107 @@ +package bgp + +import ( + "fmt" + "net" + + "github.com/cloudnativelabs/kube-router/v2/pkg/utils" + "github.com/goccy/go-yaml" +) + +type PeerConfig struct { + LocalIP *string `yaml:"localip"` + Password *utils.Base64String `yaml:"password"` + Port *uint32 `yaml:"port"` + RemoteASN *uint32 `yaml:"remoteasn"` + RemoteIP *net.IP `yaml:"remoteip"` +} + +func (p *PeerConfig) UnmarshalYAML(raw []byte) error { + tmp := struct { + LocalIP *string `yaml:"localip"` + Password *utils.Base64String `yaml:"password"` + Port *uint32 `yaml:"port"` + RemoteASN *uint32 `yaml:"remoteasn"` + RemoteIP string `yaml:"remoteip"` + }{} + + if err := yaml.Unmarshal(raw, &tmp); err != nil { + return fmt.Errorf("failed to unmarshal peer config: %w", err) + } + + p.LocalIP = tmp.LocalIP + p.Password = tmp.Password + p.Port = tmp.Port + p.RemoteASN = tmp.RemoteASN + + if tmp.RemoteIP != "" { + ip := net.ParseIP(tmp.RemoteIP) + if ip == nil { + return fmt.Errorf("%s is not a valid IP address", tmp.RemoteIP) + } + p.RemoteIP = &ip + } + return nil +} + +type PeerConfigs []PeerConfig + +func (p PeerConfigs) LocalIPs() []string { + localIPs := make([]string, 0) + for _, cfg := range p { + if cfg.LocalIP != nil { + localIPs = append(localIPs, *cfg.LocalIP) + } + } + return localIPs +} + +func (p PeerConfigs) Passwords() []string { + passwords := make([]string, 0) + for _, cfg := range p { + if cfg.Password != nil { + passwords = append(passwords, string(*cfg.Password)) + } + } + return passwords +} + +func (p PeerConfigs) Ports() []uint32 { + ports := make([]uint32, 0) + for _, cfg := range p { + if cfg.Port != nil { + ports = append(ports, *cfg.Port) + } + } + return ports +} + +func (p PeerConfigs) RemoteASNs() []uint32 { + asns := make([]uint32, 0) + for _, cfg := range p { + if cfg.RemoteASN != nil { + asns = append(asns, *cfg.RemoteASN) + } + } + return asns +} + +func (p PeerConfigs) RemoteIPs() []net.IP { + remoteIPs := make([]net.IP, 0) + for _, cfg := range p { + if cfg.RemoteIP != nil { + remoteIPs = append(remoteIPs, *cfg.RemoteIP) + } + } + return remoteIPs +} + +func (p PeerConfigs) RemoteIPStrings() []string { + remoteIPs := make([]string, 0) + for _, cfg := range p { + if cfg.RemoteIP != nil { + remoteIPs = append(remoteIPs, cfg.RemoteIP.String()) + } + } + return remoteIPs +} diff --git a/pkg/controllers/routing/network_routes_controller.go b/pkg/controllers/routing/network_routes_controller.go index c60a9d570d..465116bdf2 100644 --- a/pkg/controllers/routing/network_routes_controller.go +++ b/pkg/controllers/routing/network_routes_controller.go @@ -45,13 +45,16 @@ const ( pathPrependASNAnnotation = "kube-router.io/path-prepend.as" pathPrependRepeatNAnnotation = "kube-router.io/path-prepend.repeat-n" + // Prefer using this consolidated BGP config annotation, as the + // individual annotation config options are deprecated. + peersAnnotation = "kube-router.io/peers" + peerASNAnnotation = "kube-router.io/peer.asns" peerIPAnnotation = "kube-router.io/peer.ips" peerLocalIPAnnotation = "kube-router.io/peer.localips" //nolint:gosec // this is not a hardcoded password peerPasswordAnnotation = "kube-router.io/peer.passwords" peerPortAnnotation = "kube-router.io/peer.ports" - peersAnnotation = "kube-router.io/peers" rrClientAnnotation = "kube-router.io/rr.client" rrServerAnnotation = "kube-router.io/rr.server" svcLocalAnnotation = "kube-router.io/service.local" @@ -1417,85 +1420,14 @@ func NewNetworkRoutingController(clientset kubernetes.Interface, return &nrc, nil } -type bgpPeerConfig struct { - LocalIP *string `yaml:"localip"` - Password *base64String `yaml:"password"` - Port *uint32 `yaml:"port"` - RemoteASN *uint32 `yaml:"remoteasn"` - RemoteIP *net.IP `yaml:"remoteip"` - remoteIPString string -} - -type bgpPeerConfigs []bgpPeerConfig - -func (b bgpPeerConfigs) LocalIPs() []string { - localIPs := make([]string, 0) - for _, cfg := range b { - if cfg.LocalIP != nil { - localIPs = append(localIPs, *cfg.LocalIP) - } - } - return localIPs -} - -func (b bgpPeerConfigs) Passwords() []string { - passwords := make([]string, 0) - for _, cfg := range b { - if cfg.Password != nil { - passwords = append(passwords, string(*cfg.Password)) - } - } - return passwords -} - -func (b bgpPeerConfigs) Ports() []uint32 { - ports := make([]uint32, 0) - for _, cfg := range b { - if cfg.Port != nil { - ports = append(ports, *cfg.Port) - } - } - return ports -} - -func (b bgpPeerConfigs) RemoteASNs() []uint32 { - asns := make([]uint32, 0) - for _, cfg := range b { - if cfg.RemoteASN != nil { - asns = append(asns, *cfg.RemoteASN) - } - } - return asns -} - -func (b bgpPeerConfigs) RemoteIPs() []net.IP { - remoteIPs := make([]net.IP, 0) - for _, cfg := range b { - if cfg.RemoteIP != nil { - remoteIPs = append(remoteIPs, *cfg.RemoteIP) - } - } - return remoteIPs -} - -func (b bgpPeerConfigs) RemoteIPStrings() []string { - remoteIPs := make([]string, 0) - for _, cfg := range b { - if cfg.remoteIPString != "" { - remoteIPs = append(remoteIPs, cfg.remoteIPString) - } - } - return remoteIPs -} - -func bgpPeerConfigsFromAnnotations(nodeAnnotations map[string]string) (bgpPeerConfigs, error) { +func bgpPeerConfigsFromAnnotations(nodeAnnotations map[string]string) (bgp.PeerConfigs, error) { nodeBgpPeersAnnotation, ok := nodeAnnotations[peersAnnotation] if !ok { klog.Infof("%s annotation not set, using individual node annotations to configure BGP peer info", peersAnnotation) return bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations) } - var peerConfigs []bgpPeerConfig + var peerConfigs bgp.PeerConfigs if err := yaml.Unmarshal([]byte(nodeBgpPeersAnnotation), &peerConfigs); err != nil { return nil, fmt.Errorf("failed to parse %s annotation: %w", peersAnnotation, err) } @@ -1503,7 +1435,7 @@ func bgpPeerConfigsFromAnnotations(nodeAnnotations map[string]string) (bgpPeerCo return peerConfigs, nil } -func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) (bgpPeerConfigs, error) { +func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) (bgp.PeerConfigs, error) { // Get Global Peer Router ASN configs nodeBgpPeerAsnsAnnotation, ok := nodeAnnotations[peerASNAnnotation] if !ok { @@ -1516,7 +1448,7 @@ func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) if err != nil { return nil, fmt.Errorf("failed to parse node's Peer ASN Numbers Annotation: %w", err) } - peerConfigs := make([]bgpPeerConfig, len(peerASNs)) + peerConfigs := make([]bgp.PeerConfig, len(peerASNs)) for i, peerASN := range peerASNs { peerConfigs[i].RemoteASN = &peerASN } @@ -1534,7 +1466,6 @@ func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) return nil, fmt.Errorf("failed to parse node's Peer Addresses Annotation: %w", err) } for i, peerIP := range peerIPs { - peerConfigs[i].remoteIPString = ipStrings[i] peerConfigs[i].RemoteIP = &peerIP } @@ -1565,7 +1496,7 @@ func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) return nil, fmt.Errorf("failed to parse node's Peer Passwords Annotation: %w", err) } for i, password := range passwords { - bpassword := base64String(password) + bpassword := utils.Base64String(password) peerConfigs[i].Password = &bpassword } } diff --git a/pkg/controllers/routing/network_routes_controller_test.go b/pkg/controllers/routing/network_routes_controller_test.go index 0f9532b407..2c3fd40472 100644 --- a/pkg/controllers/routing/network_routes_controller_test.go +++ b/pkg/controllers/routing/network_routes_controller_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/cloudnativelabs/kube-router/v2/pkg/bgp" "github.com/cloudnativelabs/kube-router/v2/pkg/k8s/indexers" "github.com/cloudnativelabs/kube-router/v2/pkg/utils" "github.com/google/go-cmp/cmp" @@ -2626,7 +2627,7 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { testCases := []struct { name string nodeAnnotations map[string]string - expectedBgpPeerConfigs bgpPeerConfigs + expectedBgpPeerConfigs bgp.PeerConfigs expectError bool }{ { @@ -2647,17 +2648,17 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { password: cGFzc3dvcmQ= localip: 192.168.0.2`, }, - bgpPeerConfigs{ - bgpPeerConfig{ + bgp.PeerConfigs{ + bgp.PeerConfig{ RemoteIP: valToPtr(net.ParseIP("10.0.0.1")), RemoteASN: valToPtr(uint32(64640)), - Password: valToPtr(base64String("password")), + Password: valToPtr(utils.Base64String("password")), LocalIP: valToPtr("192.168.0.1"), }, - bgpPeerConfig{ + bgp.PeerConfig{ RemoteIP: valToPtr(net.ParseIP("10.0.0.2")), RemoteASN: valToPtr(uint32(64641)), - Password: valToPtr(base64String("password")), + Password: valToPtr(utils.Base64String("password")), LocalIP: valToPtr("192.168.0.2"), }, }, @@ -2673,15 +2674,15 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { password: cGFzc3dvcmQ= localip: 192.168.0.2`, }, - bgpPeerConfigs{ - bgpPeerConfig{ + bgp.PeerConfigs{ + bgp.PeerConfig{ RemoteIP: valToPtr(net.ParseIP("10.0.0.1")), RemoteASN: valToPtr(uint32(64640)), }, - bgpPeerConfig{ + bgp.PeerConfig{ RemoteIP: valToPtr(net.ParseIP("10.0.0.2")), RemoteASN: valToPtr(uint32(64641)), - Password: valToPtr(base64String("password")), + Password: valToPtr(utils.Base64String("password")), LocalIP: valToPtr("192.168.0.2"), }, }, @@ -2695,17 +2696,17 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { peerPasswordAnnotation: "cGFzc3dvcmQ=,cGFzc3dvcmQ=", peerLocalIPAnnotation: "192.168.0.1,192.168.0.2", }, - bgpPeerConfigs{ - bgpPeerConfig{ + bgp.PeerConfigs{ + bgp.PeerConfig{ RemoteIP: valToPtr(net.ParseIP("10.0.0.1")), RemoteASN: valToPtr(uint32(64640)), - Password: valToPtr(base64String("password")), + Password: valToPtr(utils.Base64String("password")), LocalIP: valToPtr("192.168.0.1"), }, - bgpPeerConfig{ + bgp.PeerConfig{ RemoteIP: valToPtr(net.ParseIP("10.0.0.2")), RemoteASN: valToPtr(uint32(64641)), - Password: valToPtr(base64String("password")), + Password: valToPtr(utils.Base64String("password")), LocalIP: valToPtr("192.168.0.2"), }, }, @@ -2741,8 +2742,8 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { return } require.NoError(t, err) - if !cmp.Equal(tc.expectedBgpPeerConfigs, bgpPeerCfgs, cmpopts.IgnoreUnexported(bgpPeerConfig{})) { - diff := cmp.Diff(tc.expectedBgpPeerConfigs, bgpPeerCfgs, cmpopts.IgnoreUnexported(bgpPeerConfig{})) + if !cmp.Equal(tc.expectedBgpPeerConfigs, bgpPeerCfgs, cmpopts.IgnoreUnexported(bgp.PeerConfig{})) { + diff := cmp.Diff(tc.expectedBgpPeerConfigs, bgpPeerCfgs, cmpopts.IgnoreUnexported(bgp.PeerConfig{})) t.Errorf("BGP peer config mismatch:\n%s", diff) } }) @@ -2995,7 +2996,7 @@ func waitForListerWithTimeout(lister cache.Indexer, timeout time.Duration, t *te } type value interface { - string | uint32 | net.IP | base64String + string | uint32 | net.IP | utils.Base64String } func valToPtr[V value](v V) *V { diff --git a/pkg/controllers/routing/utils.go b/pkg/controllers/routing/utils.go index a3bfbd5005..1d03033c50 100644 --- a/pkg/controllers/routing/utils.go +++ b/pkg/controllers/routing/utils.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" - "github.com/goccy/go-yaml" gobgpapi "github.com/osrg/gobgp/v3/api" v1core "k8s.io/api/core/v1" @@ -111,22 +110,6 @@ func statementsEqualByName(a, b []*gobgpapi.Statement) bool { return true } -// Wrapper type to automatically handles decoding b64 encoded strings upon unmarshalling -type base64String string - -func (b *base64String) UnmarshalYAML(raw []byte) error { - var tmp string - if err := yaml.Unmarshal(raw, &tmp); err != nil { - return fmt.Errorf("failed to unmarshal string into base64string type: %w", err) - } - decoded, err := base64.StdEncoding.DecodeString(tmp) - if err != nil { - return fmt.Errorf("failed to base64 decode field: %w", err) - } - *b = base64String(string(decoded)) - return nil -} - // getPodCIDRsFromAllNodeSources gets the pod CIDRs for all available sources on a given node in a specific order. The // order of preference is: // 1. From the kube-router.io/pod-cidr annotation (preserves backwards compatibility) diff --git a/pkg/utils/base64.go b/pkg/utils/base64.go new file mode 100644 index 0000000000..5b1a869c29 --- /dev/null +++ b/pkg/utils/base64.go @@ -0,0 +1,25 @@ +package utils + +import ( + "encoding/base64" + "fmt" + + "github.com/goccy/go-yaml" +) + +// Base64String is a wrapper type that handles automatic b64 decoding of a +// string when unmarshalling +type Base64String string + +func (b *Base64String) UnmarshalYAML(raw []byte) error { + var tmp string + if err := yaml.Unmarshal(raw, &tmp); err != nil { + return fmt.Errorf("failed to unmarshal string into base64string type: %w", err) + } + decoded, err := base64.StdEncoding.DecodeString(tmp) + if err != nil { + return fmt.Errorf("failed to base64 decode field: %w", err) + } + *b = Base64String(string(decoded)) + return nil +} From dfaebb6d3bdde83730c53b294e48894e50d37d3a Mon Sep 17 00:00:00 2001 From: Cat C Date: Tue, 21 Oct 2025 19:59:59 -0700 Subject: [PATCH 3/5] Break out bgp peer config into separate package, add tests --- pkg/bgp/peer_config.go | 98 +++++++++++++++++++ pkg/bgp/peer_config_test.go | 68 +++++++++++++ pkg/controllers/routing/bgp_peers.go | 35 ++----- .../routing/network_routes_controller.go | 47 ++++----- .../routing/network_routes_controller_test.go | 28 +++++- 5 files changed, 220 insertions(+), 56 deletions(-) create mode 100644 pkg/bgp/peer_config_test.go diff --git a/pkg/bgp/peer_config.go b/pkg/bgp/peer_config.go index c9e658a886..f25b32f496 100644 --- a/pkg/bgp/peer_config.go +++ b/pkg/bgp/peer_config.go @@ -1,9 +1,12 @@ package bgp import ( + "errors" "fmt" "net" + "strconv" + "github.com/cloudnativelabs/kube-router/v2/pkg/options" "github.com/cloudnativelabs/kube-router/v2/pkg/utils" "github.com/goccy/go-yaml" ) @@ -56,6 +59,7 @@ func (p PeerConfigs) LocalIPs() []string { return localIPs } +// Returns b64 decoded passwords func (p PeerConfigs) Passwords() []string { passwords := make([]string, 0) for _, cfg := range p { @@ -105,3 +109,97 @@ func (p PeerConfigs) RemoteIPStrings() []string { } return remoteIPs } + +func (p *PeerConfigs) UnmarshalYAML(raw []byte) error { + type tmpPeerConfigs PeerConfigs + tmp := (*tmpPeerConfigs)(p) + + if err := yaml.Unmarshal(raw, tmp); err != nil { + return err + } + + return p.Validate() +} + +func (p PeerConfigs) Validate() error { + return validatePeerConfigs(p.RemoteIPStrings(), p.RemoteASNs(), p.Ports(), p.Passwords(), p.LocalIPs(), "") +} + +func NewPeerConfigs( + remoteIPs []string, + remoteASNs []uint32, + ports []uint32, + b64EncodedPasswords []string, + localIPs []string, + localAddress string, +) (PeerConfigs, error) { + if err := validatePeerConfigs(remoteIPs, remoteASNs, ports, b64EncodedPasswords, localIPs, localAddress); err != nil { + return nil, err + } + + peerCfgs := make(PeerConfigs, len(remoteIPs)) + for i, remoteIP := range remoteIPs { + ip := net.ParseIP(remoteIP) + if ip == nil { + return nil, fmt.Errorf("invalid IP address: %s", remoteIP) + } + peerCfgs[i].RemoteIP = &ip + peerCfgs[i].RemoteASN = &remoteASNs[i] + + if len(ports) != 0 { + peerCfgs[i].Port = &ports[i] + } + + if len(b64EncodedPasswords) != 0 { + pw := utils.Base64String(b64EncodedPasswords[i]) + peerCfgs[i].Password = &pw + } + + if len(localIPs) != 0 && localIPs[i] != "" { + peerCfgs[i].LocalIP = &localIPs[i] + } + } + + return peerCfgs, nil +} + +func validatePeerConfigs( + remoteIPs []string, + remoteASNs []uint32, + ports []uint32, + b64EncodedPasswords []string, + localIPs []string, + localAddress string, +) error { + if len(remoteIPs) != len(remoteASNs) { + return errors.New("invalid peer router config, the number of IPs and ASN numbers must be equal") + } + if len(remoteIPs) != len(b64EncodedPasswords) && len(b64EncodedPasswords) != 0 { + return errors.New("invalid peer router config. The number of passwords should either be zero, or " + + "one per peer router. Use blank items if a router doesn't expect a password. Example: \"pass,,pass\" " + + "OR [\"pass\",\"\",\"pass\"]") + } + if len(remoteIPs) != len(ports) && len(ports) != 0 { + return fmt.Errorf("invalid peer router config. The number of ports should either be zero, or "+ + "one per peer router. If blank items are used, it will default to standard BGP port, %s. "+ + "Example: \"port,,port\" OR [\"port\",\"\",\"port\"]", strconv.Itoa(options.DefaultBgpPort)) + } + if len(remoteIPs) != len(localIPs) && len(localIPs) != 0 { + return fmt.Errorf("invalid peer router config. The number of localIPs should either be zero, or "+ + "one per peer router. If blank items are used, it will default to nodeIP, %s. "+ + "Example: \"10.1.1.1,,10.1.1.2\" OR [\"10.1.1.1\",\"\",\"10.1.1.2\"]", localAddress) + } + + for _, asn := range remoteASNs { + if (asn < 1 || asn > 23455) && + (asn < 23457 || asn > 63999) && + (asn < 64512 || asn > 65534) && + (asn < 131072 || asn > 4199999999) && + (asn < 4200000000 || asn > 4294967294) { + return fmt.Errorf("reserved ASN number \"%d\" for global BGP peer", + asn) + } + } + + return nil +} diff --git a/pkg/bgp/peer_config_test.go b/pkg/bgp/peer_config_test.go new file mode 100644 index 0000000000..da06545c98 --- /dev/null +++ b/pkg/bgp/peer_config_test.go @@ -0,0 +1,68 @@ +package bgp + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_NewPeerConfigs(t *testing.T) { + tcs := []struct { + name string + remoteIPs []string + remoteASNs []uint32 + ports []uint32 + b64EncodedPasswords []string + localIPs []string + localAddress string + errContains string + }{ + { + name: "all fields set to nil", + }, + { + name: "number of remote IPs and remote ASNs don't match", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{1234}, + errContains: "the number of IPs and ASN numbers must be equal", + }, + { + name: "number of remote IPs and passwords don't match", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{1234, 2345}, + b64EncodedPasswords: []string{"fakepassword"}, + errContains: "number of passwords", + }, + { + name: "number of remote IPs and ports don't match", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{1234, 2345}, + ports: []uint32{8080}, + errContains: "number of ports", + }, + { + name: "number of remote IPs and local IPs don't match", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{1234, 2345}, + localIPs: []string{"1.1.1.1"}, + errContains: "number of localIPs", + }, + { + name: "remoteASN contains a reserved ASN number", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{0, 2345}, + errContains: "reserved ASN", + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + _, err := NewPeerConfigs(tc.remoteIPs, tc.remoteASNs, tc.ports, tc.b64EncodedPasswords, tc.localIPs, tc.localAddress) + if tc.errContains != "" { + assert.ErrorContains(t, err, tc.errContains) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/controllers/routing/bgp_peers.go b/pkg/controllers/routing/bgp_peers.go index 3f290575f0..9ef5cf0dc5 100644 --- a/pkg/controllers/routing/bgp_peers.go +++ b/pkg/controllers/routing/bgp_peers.go @@ -2,13 +2,13 @@ package routing import ( "context" - "errors" "fmt" "net" "strconv" "strings" "time" + "github.com/cloudnativelabs/kube-router/v2/pkg/bgp" "github.com/cloudnativelabs/kube-router/v2/pkg/metrics" "github.com/cloudnativelabs/kube-router/v2/pkg/options" "github.com/cloudnativelabs/kube-router/v2/pkg/utils" @@ -205,7 +205,8 @@ func (nrc *NetworkRoutingController) syncInternalPeers() { // connectToExternalBGPPeers adds all the configured eBGP peers (global or node specific) as neighbours func (nrc *NetworkRoutingController) connectToExternalBGPPeers(server *gobgp.BgpServer, peerNeighbors []*gobgpapi.Peer, bgpGracefulRestart bool, bgpGracefulRestartDeferralTime time.Duration, bgpGracefulRestartTime time.Duration, - peerMultihopTTL uint8) error { + peerMultihopTTL uint8, +) error { for _, n := range peerNeighbors { neighborIPStr := n.Conf.NeighborAddress neighborIP := net.ParseIP(neighborIPStr) @@ -285,32 +286,14 @@ func (nrc *NetworkRoutingController) connectToExternalBGPPeers(server *gobgp.Bgp } // Does validation and returns neighbor configs -func newGlobalPeers(ips []net.IP, ports []uint32, asns []uint32, passwords []string, localips []string, - holdtime float64, localAddress string) ([]*gobgpapi.Peer, error) { +func newGlobalPeers(peerConfigs bgp.PeerConfigs, holdtime float64, localAddress string) ([]*gobgpapi.Peer, error) { peers := make([]*gobgpapi.Peer, 0) - // Validations - if len(ips) != len(asns) { - return nil, errors.New("invalid peer router config, the number of IPs and ASN numbers must be equal") - } - - if len(ips) != len(passwords) && len(passwords) != 0 { - return nil, errors.New("invalid peer router config. The number of passwords should either be zero, or " + - "one per peer router. Use blank items if a router doesn't expect a password. Example: \"pass,,pass\" " + - "OR [\"pass\",\"\",\"pass\"]") - } - - if len(ips) != len(ports) && len(ports) != 0 { - return nil, fmt.Errorf("invalid peer router config. The number of ports should either be zero, or "+ - "one per peer router. If blank items are used, it will default to standard BGP port, %s. "+ - "Example: \"port,,port\" OR [\"port\",\"\",\"port\"]", strconv.Itoa(options.DefaultBgpPort)) - } - - if len(ips) != len(localips) && len(localips) != 0 { - return nil, fmt.Errorf("invalid peer router config. The number of localIPs should either be zero, or "+ - "one per peer router. If blank items are used, it will default to nodeIP, %s. "+ - "Example: \"10.1.1.1,,10.1.1.2\" OR [\"10.1.1.1\",\"\",\"10.1.1.2\"]", localAddress) - } + ips := peerConfigs.RemoteIPs() + asns := peerConfigs.RemoteASNs() + passwords := peerConfigs.Passwords() + ports := peerConfigs.Ports() + localips := peerConfigs.LocalIPs() for i := 0; i < len(ips); i++ { if (asns[i] < 1 || asns[i] > 23455) && diff --git a/pkg/controllers/routing/network_routes_controller.go b/pkg/controllers/routing/network_routes_controller.go index 465116bdf2..f514593982 100644 --- a/pkg/controllers/routing/network_routes_controller.go +++ b/pkg/controllers/routing/network_routes_controller.go @@ -1106,8 +1106,7 @@ func (nrc *NetworkRoutingController) startBgpServer(grpcServer bool) error { } // Create and set Global Peer Router complete configs - nrc.globalPeerRouters, err = newGlobalPeers(peerCfgs.RemoteIPs(), peerCfgs.Ports(), peerCfgs.RemoteASNs(), peerCfgs.Passwords(), peerCfgs.LocalIPs(), - nrc.bgpHoldtime, nrc.krNode.GetPrimaryNodeIP().String()) + nrc.globalPeerRouters, err = newGlobalPeers(peerCfgs, nrc.bgpHoldtime, nrc.krNode.GetPrimaryNodeIP().String()) if err != nil { err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) if err2 != nil { @@ -1373,8 +1372,17 @@ func NewNetworkRoutingController(clientset kubernetes.Interface, } } - nrc.globalPeerRouters, err = newGlobalPeers(kubeRouterConfig.PeerRouters, peerPorts, - peerASNs, peerPasswords, nil, nrc.bgpHoldtime, nrc.krNode.GetPrimaryNodeIP().String()) + peerRouterIPs := make([]string, len(kubeRouterConfig.PeerRouters)) + for i, pr := range kubeRouterConfig.PeerRouters { + peerRouterIPs[i] = pr.String() + } + + peerCfgs, err := bgp.NewPeerConfigs(peerRouterIPs, peerASNs, peerPorts, peerPasswords, nil, nrc.krNode.GetPrimaryNodeIP().String()) + if err != nil { + return nil, err + } + + nrc.globalPeerRouters, err = newGlobalPeers(peerCfgs, nrc.bgpHoldtime, nrc.krNode.GetPrimaryNodeIP().String()) if err != nil { return nil, fmt.Errorf("error processing Global Peer Router configs: %s", err) } @@ -1448,10 +1456,6 @@ func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) if err != nil { return nil, fmt.Errorf("failed to parse node's Peer ASN Numbers Annotation: %w", err) } - peerConfigs := make([]bgp.PeerConfig, len(peerASNs)) - for i, peerASN := range peerASNs { - peerConfigs[i].RemoteASN = &peerASN - } // Get Global Peer Router IP Address configs nodeBgpPeersAnnotation, ok := nodeAnnotations[peerIPAnnotation] @@ -1461,60 +1465,46 @@ func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) return nil, nil } ipStrings := stringToSlice(nodeBgpPeersAnnotation, ",") - peerIPs, err := stringSliceToIPs(ipStrings) - if err != nil { - return nil, fmt.Errorf("failed to parse node's Peer Addresses Annotation: %w", err) - } - for i, peerIP := range peerIPs { - peerConfigs[i].RemoteIP = &peerIP - } // Get Global Peer Router ASN configs + var ports []uint32 nodeBgpPeerPortsAnnotation, ok := nodeAnnotations[peerPortAnnotation] // Default to default BGP port if port annotation is not found if ok { - var ports []uint32 portStrings := stringToSlice(nodeBgpPeerPortsAnnotation, ",") ports, err = stringSliceToUInt32(portStrings) if err != nil { return nil, fmt.Errorf("failed to parse node's Peer Port Numbers Annotation: %w", err) } - for i, port := range ports { - peerConfigs[i].Port = &port - } } + var passwords []string // Get Global Peer Router Password configs nodeBGPPasswordsAnnotation, ok := nodeAnnotations[peerPasswordAnnotation] if !ok { klog.Infof("Could not find BGP peer password info in the node's annotations. Assuming no passwords.") } else { - var passwords []string passStrings := stringToSlice(nodeBGPPasswordsAnnotation, ",") passwords, err = stringSliceB64Decode(passStrings) if err != nil { return nil, fmt.Errorf("failed to parse node's Peer Passwords Annotation: %w", err) } - for i, password := range passwords { - bpassword := utils.Base64String(password) - peerConfigs[i].Password = &bpassword - } } // Get Global Peer Router LocalIP configs + var localIPs []string nodeBGPPeerLocalIPs, ok := nodeAnnotations[peerLocalIPAnnotation] if !ok { klog.Infof("Could not find BGP peer local ip info in the node's annotations. Assuming node IP.") } else { - localIPs := stringToSlice(nodeBGPPeerLocalIPs, ",") + localIPs = stringToSlice(nodeBGPPeerLocalIPs, ",") err = func() error { - for i, s := range localIPs { + for _, s := range localIPs { if s != "" { ip := net.ParseIP(s) if ip == nil { return fmt.Errorf("could not parse \"%s\" as an IP", s) } - peerConfigs[i].LocalIP = &s } } return nil @@ -1524,5 +1514,6 @@ func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) } } - return peerConfigs, nil + // TODO: Add local address to the end arg? + return bgp.NewPeerConfigs(ipStrings, peerASNs, ports, passwords, localIPs, "") } diff --git a/pkg/controllers/routing/network_routes_controller_test.go b/pkg/controllers/routing/network_routes_controller_test.go index 2c3fd40472..856e9c757b 100644 --- a/pkg/controllers/routing/network_routes_controller_test.go +++ b/pkg/controllers/routing/network_routes_controller_test.go @@ -2665,7 +2665,7 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { false, }, { - "combined bgp peers config annotation without all fields set", + "combined bgp peers config annotation without matching number of peer config fields set", map[string]string{ peersAnnotation: `- remoteip: 10.0.0.1 remoteasn: 64640 @@ -2686,7 +2686,7 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { LocalIP: valToPtr("192.168.0.2"), }, }, - false, + true, }, { "individual bgp peers config annotations", @@ -2712,6 +2712,30 @@ func Test_bgpPeerConfigsFromAnnotations(t *testing.T) { }, false, }, + { + "individual bgp peers config annotations without matching number of peer config fields set", + map[string]string{ + peerIPAnnotation: "10.0.0.1,10.0.0.2", + peerASNAnnotation: "64640,64641", + peerPasswordAnnotation: "cGFzc3dvcmQ=", + peerLocalIPAnnotation: "192.168.0.2", + }, + bgp.PeerConfigs{ + bgp.PeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.1")), + RemoteASN: valToPtr(uint32(64640)), + Password: valToPtr(utils.Base64String("password")), + LocalIP: valToPtr("192.168.0.1"), + }, + bgp.PeerConfig{ + RemoteIP: valToPtr(net.ParseIP("10.0.0.2")), + RemoteASN: valToPtr(uint32(64641)), + Password: valToPtr(utils.Base64String("password")), + LocalIP: valToPtr("192.168.0.2"), + }, + }, + true, + }, { "individual bgp peers config annotations without peer ASN annotation", map[string]string{ From 300c759fcfb586f64685a14de14911ed70aa4bb2 Mon Sep 17 00:00:00 2001 From: Cat C Date: Tue, 21 Oct 2025 21:30:28 -0700 Subject: [PATCH 4/5] Update markdown docs --- docs/bgp.md | 78 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/docs/bgp.md b/docs/bgp.md index a68b444c1f..45690cc1b7 100644 --- a/docs/bgp.md +++ b/docs/bgp.md @@ -10,7 +10,7 @@ pod IPs, service IPs, etc.). This is the default mode. All nodes in the clusters form iBGP peering relationships with rest of the nodes forming a full node-to-node mesh. Each node advertise the pod CIDR allocated to the nodes with its peers (the rest of the nodes in -the cluster). There is no configuration required in this mode. All the nodes in the cluster are associated with the +the cluster). There is no configuration required in this mode. All the nodes in the cluster are associated with the private ASN 64512 implicitly (which can be configured with `--cluster-asn` flag) and users are transparent to use of iBGP. This mode is suitable in public cloud environments or small cluster deployments. @@ -30,7 +30,7 @@ kubectl annotate node "kube-router.io/node.asn=64512" Only nodes within same ASN form full mesh. Two nodes with different ASNs never get peered. -### Route-Reflector setup Without Full Mesh +### Route-Reflector setup Without Full Mesh This model supports the common scheme of using a Route Reflector Server node to concentrate peering from client peers. This has the big advantage of not needing full mesh, and will scale better. In this mode kube-router expects each node @@ -75,11 +75,45 @@ For example: ### Node Specific External BGP Peers -Alternatively, each node can be configured with one or more node specific BGP peers. Information regarding node specific -BGP peer is read from node API object annotations: +Each node can be configured with one or more node specific BGP peers using the `kube-router.io/peers` node annotation. +Previously, these settings were configured using individual `kube-router.io/peer.*` annotations. +While these individual annotations are still supported, they're now deprecated and +will be removed in a future release. + +#### Using Consolidated Annotation + +The `kube-router.io/peers` annotation accepts peer configurations in YAML format with the following fields: + +- `remoteip` (required): The IP address of the peer +- `remoteasn` (required): The ASN of the peer +- `localip` (optional): Local IP address to use for this peer connection +- `password` (optional): Base64 encoded password for BGP authentication +- `port` (optional): BGP port (defaults to 179 if not specified) + +```shell +kubectl annotate node \ +kube-router.io/peers="$(cat <<'EOF' +- remoteip: 192.168.1.99 + remoteasn: 65000 + password: U2VjdXJlUGFzc3dvcmQK, +- remoteip: 192.168.1.100 + remoteasn: 65000' + password: U2VjdXJlUGFzc3dvcmQK, +EOF +)" +``` + +#### Using Individual Annotations (Deprecated) + +> **NOTE:** The individual peer annotations listed below are deprecated in favor of the consolidated `kube-router.io/peers` +> annotation. They are maintained for backward compatibility but will be removed in a future release. + +Node-specific BGP peer configs can also be set via individual node API object annotations: - `kube-router.io/peer.ips` - `kube-router.io/peer.asns` +- `kube-router.io/peer.passwords` +- `kube-router.io/peer.localips` For example, users can annotate node object with below commands: @@ -106,26 +140,23 @@ kubectl annotate node "kube-router.io/path-prepend.repeat-n=5" ### BGP Peer Local IP configuration -In some setups it might be desirable to set a local IP address used for connecting external BGP peers. This can be -accomplished on nodes with annotations: +In some setups it might be desirable to set a local IP address used for connecting external BGP peers. -- `kube-router.io/peer.localips` - -If set, this must be a list with a local IP address for each peer, or left empty to use nodeIP. +When using the `kube-router.io/peers` annotation, specify the `localip` field for each peer as shown in the +[Node Specific External BGP Peers](#node-specific-external-bgp-peers) section above. -Example: +When using individual annotations, you can specify the local IP address using `kube-router.io/peer.localips`: ```shell kubectl annotate node "kube-router.io/peer.localips=10.1.1.1,10.1.1.2" ``` -This will instruct kube-router to use IP `10.1.1.1` for first BGP peer as a local address, and use `10.1.1.2`for the -second. +If set, this must be a list with a local IP address for each peer, or left empty to use nodeIP. ### BGP Peer Password Authentication -The examples above have assumed there is no password authentication with BGP peer routers. If you need to use a password -for peering, you can use the `--peer-router-passwords` command-line option, the `kube-router.io/peer.passwords` node +If you need to use a password for peering with BGP peer routers, you can configure it using the `kube-router.io/peers` +annotation, the `--peer-router-passwords` command-line option, the deprecated `kube-router.io/peer.passwords` node annotation, or the `--peer-router-passwords-file` command-line option. #### Base64 Encoding Passwords @@ -142,7 +173,14 @@ U2VjdXJlUGFzc3dvcmQ= #### Password Configuration Examples -In this CLI flag example the first router (192.168.1.99) uses a password, while the second (192.168.1.100) does not. +**Using the consolidated annotation (recommended):** + +When using the `kube-router.io/peers` annotation, specify the `password` field with a base64 encoded password for each +peer that requires authentication. See the [Node Specific External BGP Peers](#node-specific-external-bgp-peers) section for an example. + +**Using CLI flags:** + +In this example the first router (192.168.1.99) uses a password, while the second (192.168.1.100) does not: ```sh --peer-router-ips="192.168.1.99,192.168.1.100" @@ -152,7 +190,9 @@ In this CLI flag example the first router (192.168.1.99) uses a password, while Note the comma indicating the end of the first password. -Here's the same example but configured as node annotations: +**Using individual annotations (deprecated):** + +Here's the same example but configured with individual node annotations: ```shell kubectl annotate node "kube-router.io/peer.ips=192.168.1.99,192.168.1.100" @@ -160,6 +200,8 @@ kubectl annotate node "kube-router.io/peer.asns=65000,65000" kubectl annotate node "kube-router.io/peer.passwords=U2VjdXJlUGFzc3dvcmQK," ``` +**Using a password file:** + Finally, to include peer passwords as a file you would run kube-router with the following option: ```shell @@ -168,8 +210,8 @@ Finally, to include peer passwords as a file you would run kube-router with the --peer-router-passwords-file="/etc/kube-router/bgp-passwords.conf" ``` -The password file, closely follows the syntax of the command-line and node annotation options. -Here, the first peer IP (192.168.1.99) would be configured with a password, while the second would not. +The password file closely follows the syntax of the command-line and node annotation options. +Here, the first peer IP (192.168.1.99) would be configured with a password, while the second would not: ```sh U2VjdXJlUGFzc3dvcmQK, From fce47fcd2ffe9592b20216fc7130cf3e050be50d Mon Sep 17 00:00:00 2001 From: Cat C Date: Mon, 27 Oct 2025 19:03:43 -0700 Subject: [PATCH 5/5] Update docs changes --- docs/bgp.md | 3 +- pkg/bgp/peer_config_test.go | 44 +++++++++---------- .../routing/network_routes_controller.go | 11 +++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/bgp.md b/docs/bgp.md index 45690cc1b7..330de66df9 100644 --- a/docs/bgp.md +++ b/docs/bgp.md @@ -176,7 +176,8 @@ U2VjdXJlUGFzc3dvcmQ= **Using the consolidated annotation (recommended):** When using the `kube-router.io/peers` annotation, specify the `password` field with a base64 encoded password for each -peer that requires authentication. See the [Node Specific External BGP Peers](#node-specific-external-bgp-peers) section for an example. +peer that requires authentication. See the +[Node Specific External BGP Peers](#node-specific-external-bgp-peers) section for an example. **Using CLI flags:** diff --git a/pkg/bgp/peer_config_test.go b/pkg/bgp/peer_config_test.go index da06545c98..d3f4f69dbe 100644 --- a/pkg/bgp/peer_config_test.go +++ b/pkg/bgp/peer_config_test.go @@ -15,51 +15,51 @@ func Test_NewPeerConfigs(t *testing.T) { b64EncodedPasswords []string localIPs []string localAddress string - errContains string + errStringContains string }{ { name: "all fields set to nil", }, { - name: "number of remote IPs and remote ASNs don't match", - remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, - remoteASNs: []uint32{1234}, - errContains: "the number of IPs and ASN numbers must be equal", + name: "number of remote IPs and remote ASNs don't match", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{1234}, + errStringContains: "the number of IPs and ASN numbers must be equal", }, { name: "number of remote IPs and passwords don't match", remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, remoteASNs: []uint32{1234, 2345}, b64EncodedPasswords: []string{"fakepassword"}, - errContains: "number of passwords", + errStringContains: "number of passwords", }, { - name: "number of remote IPs and ports don't match", - remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, - remoteASNs: []uint32{1234, 2345}, - ports: []uint32{8080}, - errContains: "number of ports", + name: "number of remote IPs and ports don't match", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{1234, 2345}, + ports: []uint32{8080}, + errStringContains: "number of ports", }, { - name: "number of remote IPs and local IPs don't match", - remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, - remoteASNs: []uint32{1234, 2345}, - localIPs: []string{"1.1.1.1"}, - errContains: "number of localIPs", + name: "number of remote IPs and local IPs don't match", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{1234, 2345}, + localIPs: []string{"1.1.1.1"}, + errStringContains: "number of localIPs", }, { - name: "remoteASN contains a reserved ASN number", - remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, - remoteASNs: []uint32{0, 2345}, - errContains: "reserved ASN", + name: "remoteASN contains a reserved ASN number", + remoteIPs: []string{"10.0.0.1", "10.0.0.2"}, + remoteASNs: []uint32{0, 2345}, + errStringContains: "reserved ASN", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { _, err := NewPeerConfigs(tc.remoteIPs, tc.remoteASNs, tc.ports, tc.b64EncodedPasswords, tc.localIPs, tc.localAddress) - if tc.errContains != "" { - assert.ErrorContains(t, err, tc.errContains) + if tc.errStringContains != "" { + assert.ErrorContains(t, err, tc.errStringContains) } else { assert.NoError(t, err) } diff --git a/pkg/controllers/routing/network_routes_controller.go b/pkg/controllers/routing/network_routes_controller.go index f514593982..9202dc0f61 100644 --- a/pkg/controllers/routing/network_routes_controller.go +++ b/pkg/controllers/routing/network_routes_controller.go @@ -1092,7 +1092,7 @@ func (nrc *NetworkRoutingController) startBgpServer(grpcServer bool) error { // else attempt to get peers from node specific BGP annotations. if len(nrc.globalPeerRouters) == 0 { klog.V(2).Infof("Attempting to construct peer configs from annotation: %+v", node.Annotations) - peerCfgs, err := bgpPeerConfigsFromAnnotations(node.Annotations) + peerCfgs, err := bgpPeerConfigsFromAnnotations(node.Annotations, nrc.krNode.GetPrimaryNodeIP().String()) if err != nil { err2 := nrc.bgpServer.StopBgp(context.Background(), &gobgpapi.StopBgpRequest{}) if err2 != nil { @@ -1428,11 +1428,11 @@ func NewNetworkRoutingController(clientset kubernetes.Interface, return &nrc, nil } -func bgpPeerConfigsFromAnnotations(nodeAnnotations map[string]string) (bgp.PeerConfigs, error) { +func bgpPeerConfigsFromAnnotations(nodeAnnotations map[string]string, localAddress string) (bgp.PeerConfigs, error) { nodeBgpPeersAnnotation, ok := nodeAnnotations[peersAnnotation] if !ok { klog.Infof("%s annotation not set, using individual node annotations to configure BGP peer info", peersAnnotation) - return bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations) + return bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations, localAddress) } var peerConfigs bgp.PeerConfigs @@ -1443,7 +1443,7 @@ func bgpPeerConfigsFromAnnotations(nodeAnnotations map[string]string) (bgp.PeerC return peerConfigs, nil } -func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) (bgp.PeerConfigs, error) { +func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string, localAddress string) (bgp.PeerConfigs, error) { // Get Global Peer Router ASN configs nodeBgpPeerAsnsAnnotation, ok := nodeAnnotations[peerASNAnnotation] if !ok { @@ -1514,6 +1514,5 @@ func bgpPeerConfigsFromIndividualAnnotations(nodeAnnotations map[string]string) } } - // TODO: Add local address to the end arg? - return bgp.NewPeerConfigs(ipStrings, peerASNs, ports, passwords, localIPs, "") + return bgp.NewPeerConfigs(ipStrings, peerASNs, ports, passwords, localIPs, localAddress) }