Repository: metallb/metallb
Pull request #2605 (2024-11-22 15:12:40)

Add unnumbered BGP peering for frrk8s backend 
/kind feature

**What this PR does / why we need it**:

**Special notes for your reviewer**:

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. Follow the instructions for writing a release note from k8s: https://git.k8s.io/community/contributors/guide/release-notes.md
3. If no release note is required, just write "NONE".
-->

```release-note
Add unnumbered BGP peering for frrk8s backend 
```


karampok: 


> can you update the `DiscardFRROnly` func to block interface? also, if we decide this is only for frr-k8s I guess we should block it in frr mode only as well (currently they share the validation)

The plan is to proceed next to support that in the frr backend AFAIK, not sure worth spliting the validation.
karampok: @fedepaol please take a look, thank you
fedepaol: LGTM, thanks!

Patch:
diff --git a/api/v1beta2/bgppeer_types.go b/api/v1beta2/bgppeer_types.go
index 3269439d..d984f5ee 100644
--- a/api/v1beta2/bgppeer_types.go
+++ b/api/v1beta2/bgppeer_types.go
@@ -45,7 +45,16 @@ type BGPPeerSpec struct {
 	DynamicASN DynamicASNMode `json:"dynamicASN,omitempty"`
 
 	// Address to dial when establishing the session.
-	Address string `json:"peerAddress"`
+	// +optional
+	Address string `json:"peerAddress,omitempty"`
+
+	// Interface is the node interface over which the unnumbered BGP peering will
+	// be established. No API validation takes place as that string value
+	// represents an interface name on the host and if user provides an invalid
+	// value, only the actual BGP session will not be established.
+	// Address and Interface are mutually exclusive and one of them must be specified.
+	// +optional
+	Interface string `json:"interface,omitempty"`
 
 	// Source address to use when establishing the session.
 	// +optional
@@ -53,7 +62,7 @@ type BGPPeerSpec struct {
 
 	// Port to dial when establishing the session.
 	// +optional
-	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:validation:Minimum=1
 	// +kubebuilder:validation:Maximum=16384
 	// +kubebuilder:default:=179
 	Port uint16 `json:"peerPort,omitempty"`
diff --git a/charts/metallb/charts/crds/templates/crds.yaml b/charts/metallb/charts/crds/templates/crds.yaml
index 383c8fea..e8527f94 100644
--- a/charts/metallb/charts/crds/templates/crds.yaml
+++ b/charts/metallb/charts/crds/templates/crds.yaml
@@ -555,6 +555,14 @@ spec:
                 holdTime:
                   description: Requested BGP hold time, per RFC4271.
                   type: string
+                interface:
+                  description: |-
+                    Interface is the node interface over which the unnumbered BGP peering will
+                    be established. No API validation takes place as that string value
+                    represents an interface name on the host and if user provides an invalid
+                    value, only the actual BGP session will not be established.
+                    Address and Interface are mutually exclusive and one of them must be specified.
+                  type: string
                 keepaliveTime:
                   description: Requested BGP keepalive time, per RFC4271.
                   type: string
@@ -649,7 +657,7 @@ spec:
                   default: 179
                   description: Port to dial when establishing the session.
                   maximum: 16384
-                  minimum: 0
+                  minimum: 1
                   type: integer
                 routerID:
                   description: BGP router ID to advertise to the peer
@@ -664,7 +672,6 @@ spec:
                   type: string
               required:
                 - myASN
-                - peerAddress
               type: object
             status:
               description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/crd/bases/metallb.io_bgppeers.yaml b/config/crd/bases/metallb.io_bgppeers.yaml
index 1b740778..3fd406d0 100644
--- a/config/crd/bases/metallb.io_bgppeers.yaml
+++ b/config/crd/bases/metallb.io_bgppeers.yaml
@@ -224,6 +224,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -323,7 +331,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -338,7 +346,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/manifests/metallb-frr-k8s-prometheus.yaml b/config/manifests/metallb-frr-k8s-prometheus.yaml
index ef1d2082..3d6ed07f 100644
--- a/config/manifests/metallb-frr-k8s-prometheus.yaml
+++ b/config/manifests/metallb-frr-k8s-prometheus.yaml
@@ -582,6 +582,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +689,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +704,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/manifests/metallb-frr-k8s.yaml b/config/manifests/metallb-frr-k8s.yaml
index 29f78f49..69483c67 100644
--- a/config/manifests/metallb-frr-k8s.yaml
+++ b/config/manifests/metallb-frr-k8s.yaml
@@ -582,6 +582,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +689,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +704,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/manifests/metallb-frr-prometheus.yaml b/config/manifests/metallb-frr-prometheus.yaml
index d6203147..b54b73e5 100644
--- a/config/manifests/metallb-frr-prometheus.yaml
+++ b/config/manifests/metallb-frr-prometheus.yaml
@@ -582,6 +582,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +689,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +704,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index 156f4e48..c3d7c303 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -582,6 +582,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +689,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +704,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/manifests/metallb-native-prometheus.yaml b/config/manifests/metallb-native-prometheus.yaml
index 73076c3d..effb028a 100644
--- a/config/manifests/metallb-native-prometheus.yaml
+++ b/config/manifests/metallb-native-prometheus.yaml
@@ -582,6 +582,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +689,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +704,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index 22db4d36..3e3d6853 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -582,6 +582,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +689,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +704,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/internal/bgp/bgp.go b/internal/bgp/bgp.go
index 78fb2d9b..1daf58d9 100644
--- a/internal/bgp/bgp.go
+++ b/internal/bgp/bgp.go
@@ -64,6 +64,8 @@ type Session interface {
 
 type SessionParameters struct {
 	PeerAddress     string
+	PeerPort        uint16
+	PeerInterface   string
 	SourceAddress   net.IP
 	MyASN           uint32
 	RouterID        net.IP
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index 492ece0a..fa8c35e5 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -7,7 +7,6 @@ import (
 	"net"
 	"os"
 	"sort"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -254,17 +253,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 
 		neighborName := NeighborName(s.PeerAddress, s.PeerASN, s.DynamicASN, s.VRFName)
 		if neighbor, exist = rout.neighbors[neighborName]; !exist {
-			host, port, err := net.SplitHostPort(s.PeerAddress)
-			if err != nil {
-				return nil, err
-			}
-
-			portUint, err := strconv.ParseUint(port, 10, 16)
-			if err != nil {
-				return nil, err
-			}
-
-			family := ipfamily.ForAddress(net.ParseIP(host))
+			family := ipfamily.ForAddress(net.ParseIP(s.PeerAddress))
 
 			var connectTime int64
 			if s.ConnectTime != nil {
@@ -286,8 +275,8 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 				Name:            neighborName,
 				IPFamily:        family,
 				ASN:             asnFor(s.PeerASN, s.DynamicASN),
-				Addr:            host,
-				Port:            uint16(portUint),
+				Addr:            s.PeerAddress,
+				Port:            s.PeerPort,
 				HoldTime:        holdTime,
 				KeepaliveTime:   keepaliveTime,
 				ConnectTime:     connectTime,
diff --git a/internal/bgp/frrk8s/frrk8s.go b/internal/bgp/frrk8s/frrk8s.go
index b0d9cf1c..9cec413d 100644
--- a/internal/bgp/frrk8s/frrk8s.go
+++ b/internal/bgp/frrk8s/frrk8s.go
@@ -6,7 +6,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"net"
 	"reflect"
 	"sort"
 	"strconv"
@@ -48,13 +47,23 @@ type session struct {
 	logger         log.Logger
 }
 
-// sessionName() defines the format of the key of the 'sessions' map in
-// the 'frrState' struct.
+// sessionName() defines the format of a map key.
 func sessionName(s session) string {
-	baseName := fmt.Sprintf("%d@%s-%d@%s", s.PeerASN, s.PeerAddress, s.MyASN, s.SourceAddress)
+	asn := strconv.FormatUint(uint64(s.PeerASN), 10)
+	if s.DynamicASN != "" {
+		asn = s.DynamicASN
+	}
+
+	peerName := s.PeerAddress
+	if s.PeerInterface != "" {
+		peerName = s.PeerInterface
+	}
+
+	baseName := fmt.Sprintf("%s@%s-%d@%s", asn, peerName, s.MyASN, s.SourceAddress)
 	if s.VRFName == "" {
 		return baseName
 	}
+
 	return baseName + "/" + s.VRFName
 }
 
@@ -227,19 +236,8 @@ func (sm *sessionManager) updateConfig() error {
 			routers[routerName] = rout
 		}
 
-		neighborName := frr.NeighborName(s.PeerAddress, s.PeerASN, s.DynamicASN, s.VRFName)
+		neighborName := sessionName(*s)
 		if neighbor, exist = rout.neighbors[neighborName]; !exist {
-			host, port, err := net.SplitHostPort(s.PeerAddress)
-			if err != nil {
-				return err
-			}
-
-			portUint, err := strconv.ParseUint(port, 10, 16)
-			if err != nil {
-				return err
-			}
-			portUint16 := uint16(portUint)
-
 			if !reflect.DeepEqual(s.PasswordRef, corev1.SecretReference{}) && s.Password != "" {
 				return fmt.Errorf("invalid session with password and secret set: %s", sessionName(*s))
 			}
@@ -259,10 +257,11 @@ func (sm *sessionManager) updateConfig() error {
 			}
 
 			neighbor = frrv1beta1.Neighbor{
+				Address:               s.PeerAddress,
+				Interface:             s.PeerInterface,
 				ASN:                   s.PeerASN,
 				DynamicASN:            frrv1beta1.DynamicASNMode(s.DynamicASN),
-				Address:               host,
-				Port:                  &portUint16,
+				Port:                  &s.PeerPort,
 				HoldTime:              holdTime,
 				KeepaliveTime:         keepaliveTime,
 				ConnectTime:           connectTime,
diff --git a/internal/bgp/native/native.go b/internal/bgp/native/native.go
index eea36036..9413d3a0 100644
--- a/internal/bgp/native/native.go
+++ b/internal/bgp/native/native.go
@@ -44,6 +44,9 @@ type session struct {
 	nextHop        net.IP
 	advertised     map[string]*bgp.Advertisement
 	new            map[string]*bgp.Advertisement
+
+	// peerName identifies this BGP session to be used for metrics
+	peerName string
 }
 
 // The 'Native' implementation does not require a session manager .
@@ -71,6 +74,7 @@ func (sm *sessionManager) NewSession(l log.Logger, args bgp.SessionParameters) (
 		logger:            log.With(l, "peer", args.PeerAddress, "localASN", args.MyASN, "peerASN", args.PeerASN),
 		newHoldTime:       make(chan bool, 1),
 		advertised:        map[string]*bgp.Advertisement{},
+		peerName:          fmt.Sprintf("%s:%d", args.PeerAddress, args.PeerPort),
 	}
 	ret.cond = sync.NewCond(&ret.mu)
 	go ret.sendKeepalives()
@@ -100,7 +104,7 @@ func (sm *sessionManager) SetEventCallback(func(interface{})) {}
 
 // run tries to stay connected to the peer, and pumps route updates to it.
 func (s *session) run() {
-	defer stats.DeleteSession(s.PeerAddress)
+	defer stats.DeleteSession(s.peerName)
 	for {
 		if err := s.connect(); err != nil {
 			if err == errClosed {
@@ -111,7 +115,7 @@ func (s *session) run() {
 			time.Sleep(backoff)
 			continue
 		}
-		stats.SessionUp(s.PeerAddress)
+		stats.SessionUp(s.peerName)
 		s.backoff.Reset()
 
 		level.Info(s.logger).Log("event", "sessionUp", "msg", "BGP session established")
@@ -119,7 +123,7 @@ func (s *session) run() {
 		if !s.sendUpdates() {
 			return
 		}
-		stats.SessionDown(s.PeerAddress)
+		stats.SessionDown(s.peerName)
 		level.Warn(s.logger).Log("event", "sessionDown", "msg", "BGP session down")
 	}
 }
@@ -150,9 +154,9 @@ func (s *session) sendUpdates() bool {
 			level.Error(s.logger).Log("op", "sendUpdate", "ip", c, "error", err, "msg", "failed to send BGP update")
 			return true
 		}
-		stats.UpdateSent(s.PeerAddress)
+		stats.UpdateSent(s.peerName)
 	}
-	stats.AdvertisedPrefixes(s.PeerAddress, len(s.advertised))
+	stats.AdvertisedPrefixes(s.peerName, len(s.advertised))
 
 	for {
 		for s.new == nil && s.conn != nil {
@@ -183,7 +187,7 @@ func (s *session) sendUpdates() bool {
 				level.Error(s.logger).Log("op", "sendUpdate", "prefix", c, "error", err, "msg", "failed to send BGP update")
 				return true
 			}
-			stats.UpdateSent(s.PeerAddress)
+			stats.UpdateSent(s.peerName)
 		}
 
 		wdr := []*net.IPNet{}
@@ -200,10 +204,10 @@ func (s *session) sendUpdates() bool {
 				}
 				return true
 			}
-			stats.UpdateSent(s.PeerAddress)
+			stats.UpdateSent(s.peerName)
 		}
 		s.advertised, s.new = s.new, nil
-		stats.AdvertisedPrefixes(s.PeerAddress, len(s.advertised))
+		stats.AdvertisedPrefixes(s.peerName, len(s.advertised))
 	}
 }
 
@@ -220,7 +224,7 @@ func (s *session) connect() error {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
 	deadline, _ := ctx.Deadline()
-	conn, err := dialMD5(ctx, s.PeerAddress, s.SourceAddress, s.Password)
+	conn, err := dialMD5(ctx, fmt.Sprintf("%s:%d", s.PeerAddress, s.PeerPort), s.SourceAddress, s.Password)
 	if err != nil {
 		return fmt.Errorf("dial %q: %s", s.PeerAddress, err)
 	}
@@ -473,7 +477,8 @@ func (s *session) Set(advs ...*bgp.Advertisement) error {
 	}
 
 	s.new = newAdvs
-	stats.PendingPrefixes(s.PeerAddress, len(s.new))
+
+	stats.PendingPrefixes(s.peerName, len(s.new))
 	s.cond.Broadcast()
 
 	return nil
@@ -485,13 +490,14 @@ func (s *session) abort() {
 	if s.conn != nil {
 		s.conn.Close()
 		s.conn = nil
-		stats.SessionDown(s.PeerAddress)
+		stats.SessionDown(s.peerName)
 	}
 	// Next time we retry the connection, we can just skip straight to
 	// the desired end state.
 	if s.new != nil {
 		s.advertised, s.new = s.new, nil
-		stats.PendingPrefixes(s.PeerAddress, len(s.advertised))
+
+		stats.PendingPrefixes(s.peerName, len(s.advertised))
 	}
 	s.cond.Broadcast()
 }
diff --git a/internal/bgp/native/stats.go b/internal/bgp/native/stats.go
index 001d469f..616ed78d 100644
--- a/internal/bgp/native/stats.go
+++ b/internal/bgp/native/stats.go
@@ -53,13 +53,6 @@ func init() {
 	prometheus.MustRegister(stats.pendingPrefixes)
 }
 
-func (m *metrics) NewSession(addr string) {
-	m.sessionUp.WithLabelValues(addr).Set(0)
-	m.prefixes.WithLabelValues(addr).Set(0)
-	m.pendingPrefixes.WithLabelValues(addr).Set(0)
-	m.updatesSent.WithLabelValues(addr).Add(0) // just creates the metric
-}
-
 func (m *metrics) DeleteSession(addr string) {
 	m.sessionUp.DeleteLabelValues(addr)
 	m.prefixes.DeleteLabelValues(addr)
diff --git a/internal/config/config.go b/internal/config/config.go
index 99bd24b8..c6d28be1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -102,6 +102,9 @@ type Peer struct {
 	DynamicASN string
 	// Address to dial when establishing the session.
 	Addr net.IP
+	// Iface is the Interface to use for Unnumbered BGP peering.
+	// Addr field must be nil.
+	Iface string
 	// Source address to use when establishing the session.
 	SrcAddr net.IP
 	// Port to dial when establishing the session.
@@ -280,7 +283,7 @@ func peersFor(resources ClusterResources, BFDProfiles map[string]*BFDProfile) (m
 	for _, p := range resources.Peers {
 		peer, err := peerFromCR(p, resources.PasswordSecrets)
 		if err != nil {
-			return nil, errors.Join(err, fmt.Errorf("parsing peer %s", p.Name))
+			return nil, fmt.Errorf("parsing peer %s %w", p.Name, err)
 		}
 		if peer.BFDProfile != "" {
 			if _, ok := BFDProfiles[peer.BFDProfile]; !ok {
@@ -398,9 +401,12 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 	if p.Spec.ASN == p.Spec.MyASN && p.Spec.EBGPMultiHop {
 		return nil, errors.New("invalid ebgp-multihop parameter set for an ibgp peer")
 	}
-	ip := net.ParseIP(p.Spec.Address)
-	if ip == nil {
-		return nil, fmt.Errorf("invalid BGPPeer address %q", p.Spec.Address)
+	if p.Spec.Address == "" && p.Spec.Interface == "" {
+		return nil, fmt.Errorf("peer has no Address or Interface specified")
+	}
+
+	if p.Spec.Address != "" && p.Spec.Interface != "" {
+		return nil, fmt.Errorf("peer has both Address and Interface specified")
 	}
 
 	holdTime, keepaliveTime, err := parseTimers(p.Spec.HoldTime, p.Spec.KeepaliveTime)
@@ -408,6 +414,14 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 		return nil, fmt.Errorf("invalid BGPPeer timers: %w", err)
 	}
 
+	var ip net.IP
+	if p.Spec.Address != "" {
+		ip = net.ParseIP(p.Spec.Address)
+		if ip == nil {
+			return nil, fmt.Errorf("invalid BGPPeer address %q", p.Spec.Address)
+		}
+	}
+
 	// Ideally we would set a default RouterID here, instead of having
 	// to do it elsewhere in the code. Unfortunately, we don't know
 	// the node IP here.
@@ -464,6 +478,7 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 		ASN:                   p.Spec.ASN,
 		DynamicASN:            string(p.Spec.DynamicASN),
 		Addr:                  ip,
+		Iface:                 p.Spec.Interface,
 		SrcAddr:               src,
 		Port:                  p.Spec.Port,
 		HoldTime:              holdTime,
diff --git a/internal/config/validation.go b/internal/config/validation.go
index 2d1759e1..66ece4b7 100644
--- a/internal/config/validation.go
+++ b/internal/config/validation.go
@@ -54,6 +54,9 @@ func DiscardFRROnly(c ClusterResources) error {
 		if p.Spec.DynamicASN != "" {
 			return fmt.Errorf("peer %s has dynamicASN set on native bgp mode", p.Spec.Address)
 		}
+		if p.Spec.Interface != "" {
+			return fmt.Errorf("peer %s has interface set on native bgp mode", p.Spec.Address)
+		}
 	}
 	if len(c.BFDProfiles) > 0 {
 		return errors.New("bfd profiles section set")
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 47061ef6..655e1b88 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -19,7 +19,6 @@ import (
 	"net"
 	"reflect"
 	"sort"
-	"strconv"
 	"sync"
 
 	"github.com/go-kit/log"
@@ -59,6 +58,7 @@ const (
 type peer struct {
 	cfg     *config.Peer
 	session bgp.Session
+	id      string
 }
 
 type bgpController struct {
@@ -90,9 +90,15 @@ newPeers:
 				continue newPeers
 			}
 		}
+		id := p.Iface
+		if p.Addr != nil {
+			id = p.Addr.String()
+		}
+
 		// No existing peers match, create a new one.
 		newPeers = append(newPeers, &peer{
 			cfg: p,
+			id:  id,
 		})
 	}
 
@@ -103,14 +109,14 @@ newPeers:
 		if p == nil {
 			continue
 		}
-		level.Info(l).Log("event", "peerRemoved", "peer", p.cfg.Addr, "reason", "removedFromConfig", "msg", "peer deconfigured, closing BGP session")
+		level.Info(l).Log("event", "peerRemoved", "peer", p.id, "reason", "removedFromConfig", "msg", "peer deconfigured, closing BGP session")
 
 		if p.session != nil {
 			if err := p.session.Close(); err != nil {
-				level.Error(l).Log("op", "setConfig", "error", err, "peer", p.cfg.Addr, "msg", "failed to shut down BGP session")
+				level.Error(l).Log("op", "setConfig", "error", err, "peer", p.id, "msg", "failed to shut down BGP session")
 			}
 		}
-		level.Debug(l).Log("event", "peerRemoved", "peer", p.cfg.Addr, "reason", "removedFromConfig", "msg", "peer deconfigured, BGP session closed")
+		level.Debug(l).Log("event", "peerRemoved", "peer", p.id, "reason", "removedFromConfig", "msg", "peer deconfigured, BGP session closed")
 	}
 
 	err := c.syncBFDProfiles(cfg.BFDProfiles)
@@ -222,22 +228,28 @@ func (c *bgpController) syncPeers(l log.Logger) error {
 		// Now, compare current state to intended state, and correct.
 		if p.session != nil && !shouldRun {
 			// Oops, session is running but shouldn't be. Shut it down.
-			level.Info(l).Log("event", "peerRemoved", "peer", p.cfg.Addr, "reason", "filteredByNodeSelector", "msg", "peer deconfigured, closing BGP session")
+			level.Info(l).Log("event", "peerRemoved", "peer", p.id, "reason", "filteredByNodeSelector", "msg", "peer deconfigured, closing BGP session")
 			if err := p.session.Close(); err != nil {
-				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.cfg.Addr, "msg", "failed to shut down BGP session")
+				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.id, "msg", "failed to shut down BGP session")
 			}
 			p.session = nil
 		} else if p.session == nil && shouldRun {
 			// Session doesn't exist, but should be running. Create
 			// it.
-			level.Info(l).Log("event", "peerAdded", "peer", p.cfg.Addr, "msg", "peer configured, starting BGP session")
+			level.Info(l).Log("event", "peerAdded", "peer", p.id, "msg", "peer configured, starting BGP session")
 			var routerID net.IP
 			if p.cfg.RouterID != nil {
 				routerID = p.cfg.RouterID
 			}
 
+			peerAddr := "" // we need because otherwise the value will "<nil>"
+			if p.cfg.Addr != nil {
+				peerAddr = p.cfg.Addr.String()
+			}
 			sessionParams := bgp.SessionParameters{
-				PeerAddress:     net.JoinHostPort(p.cfg.Addr.String(), strconv.Itoa(int(p.cfg.Port))),
+				PeerAddress:     peerAddr,
+				PeerPort:        p.cfg.Port,
+				PeerInterface:   p.cfg.Iface,
 				SourceAddress:   p.cfg.SrcAddr,
 				MyASN:           p.cfg.MyASN,
 				RouterID:        routerID,
@@ -259,7 +271,7 @@ func (c *bgpController) syncPeers(l log.Logger) error {
 			s, err := c.sessionManager.NewSession(c.logger, sessionParams)
 
 			if err != nil {
-				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.cfg.Addr, "msg", "failed to create BGP session")
+				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.id, "msg", "failed to create BGP session")
 				errs++
 			} else {
 				p.session = s
diff --git a/website/content/apis/_index.md b/website/content/apis/_index.md
index 6ea3adc4..91ae9eb6 100644
--- a/website/content/apis/_index.md
+++ b/website/content/apis/_index.md
@@ -288,6 +288,7 @@ _Appears in:_
 | `peerASN` _integer_ | AS number to expect from the remote end of the session.<br />ASN and DynamicASN are mutually exclusive and one of them must be specified. |
 | `dynamicASN` _[DynamicASNMode](#dynamicasnmode)_ | DynamicASN detects the AS number to use for the remote end of the session<br />without explicitly setting it via the ASN field. Limited to:<br />internal - if the neighbor's ASN is different than MyASN connection is denied.<br />external - if the neighbor's ASN is the same as MyASN the connection is denied.<br />ASN and DynamicASN are mutually exclusive and one of them must be specified. |
 | `peerAddress` _string_ | Address to dial when establishing the session. |
+| `interface` _string_ | Interface is the node interface over which the unnumbered BGP peering will<br />be established. No API validation takes place as that string value<br />represents an interface name on the host and if user provides an invalid<br />value, only the actual BGP session will not be established.<br />Address and Interface are mutually exclusive and one of them must be specified. |
 | `sourceAddress` _string_ | Source address to use when establishing the session. |
 | `peerPort` _integer_ | Port to dial when establishing the session. |
 | `holdTime` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#duration-v1-meta)_ | Requested BGP hold time, per RFC4271. |


Test patch:
diff --git a/e2etest/bgptests/dump.go b/e2etest/bgptests/dump.go
index 68d51599..c85cdb6c 100644
--- a/e2etest/bgptests/dump.go
+++ b/e2etest/bgptests/dump.go
@@ -12,11 +12,12 @@ import (
 	"github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"go.universe.tf/e2etest/pkg/frr"
+	frrcontainer "go.universe.tf/e2etest/pkg/frr/container"
 	"go.universe.tf/e2etest/pkg/metallb"
 	clientset "k8s.io/client-go/kubernetes"
 )
 
-func dumpBGPInfo(basePath, testName string, cs clientset.Interface, namespace string) {
+func dumpBGPInfo(basePath, testName string, cs clientset.Interface, namespace string, extraContainers ...*frrcontainer.FRR) {
 	testPath := path.Join(basePath, strings.ReplaceAll(testName, " ", "-"))
 	err := os.Mkdir(testPath, 0755)
 	if err != nil && !errors.Is(err, os.ErrExist) {
@@ -24,7 +25,7 @@ func dumpBGPInfo(basePath, testName string, cs clientset.Interface, namespace st
 		return
 	}
 
-	for _, c := range FRRContainers {
+	for _, c := range append(FRRContainers, extraContainers...) {
 		dump, err := frr.RawDump(c, "/etc/frr/bgpd.conf", "/tmp/frr.log", "/etc/frr/daemons")
 		if err != nil {
 			ginkgo.GinkgoWriter.Printf("External frr dump for container %s failed %v", c.Name, err)
diff --git a/e2etest/bgptests/unnummbered.go b/e2etest/bgptests/unnummbered.go
new file mode 100644
index 00000000..0717fffe
--- /dev/null
+++ b/e2etest/bgptests/unnummbered.go
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package bgptests
+
+import (
+	"context"
+	"fmt"
+	"net/netip"
+	"time"
+
+	"go.universe.tf/e2etest/pkg/config"
+	"go.universe.tf/e2etest/pkg/container"
+	"go.universe.tf/e2etest/pkg/executor"
+	"go.universe.tf/e2etest/pkg/frr"
+	frrconfig "go.universe.tf/e2etest/pkg/frr/config"
+	frrcontainer "go.universe.tf/e2etest/pkg/frr/container"
+	jigservice "go.universe.tf/e2etest/pkg/jigservice"
+	"go.universe.tf/e2etest/pkg/netdev"
+	testservice "go.universe.tf/e2etest/pkg/service"
+
+	"go.universe.tf/e2etest/pkg/k8s"
+	"go.universe.tf/e2etest/pkg/k8sclient"
+	"go.universe.tf/e2etest/pkg/metallb"
+	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
+	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientset "k8s.io/client-go/kubernetes"
+)
+
+var FRRImage string
+var _ = ginkgo.Describe("FRRK8S-MODE Unnumbered BGP", func() {
+	var (
+		testNamespace         string
+		nodeWithP2PConnection corev1.Node
+		remoteP2PContainer    *frrcontainer.FRR
+
+		cs clientset.Interface
+	)
+
+	ginkgo.BeforeEach(func() {
+		ginkgo.By("Clearing any previous configuration")
+		err := ConfigUpdater.Clean()
+		Expect(err).NotTo(HaveOccurred())
+
+		cs = k8sclient.New()
+		allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		nodeWithP2PConnection = allNodes.Items[0]
+
+		err = k8s.DeleteNamespace(k8sclient.New(), "unnumbered-bgp")
+		Expect(err).NotTo(HaveOccurred())
+		testNamespace, err = k8s.CreateTestNamespace(k8sclient.New(), "unnumbered-bgp")
+		Expect(err).NotTo(HaveOccurred())
+		ginkgo.DeferCleanup(func() {
+			err := k8s.DeleteNamespace(k8sclient.New(), testNamespace)
+			Expect(err).NotTo(HaveOccurred())
+		})
+	})
+
+	ginkgo.AfterEach(func() {
+		if ginkgo.CurrentSpecReport().Failed() {
+			k8s.DumpInfo(Reporter, ginkgo.CurrentSpecReport().LeafNodeText)
+			dumpBGPInfo(ReportPath, ginkgo.CurrentSpecReport().LeafNodeText, cs, testNamespace, remoteP2PContainer)
+		}
+
+		err := frrcontainer.Delete([]*frrcontainer.FRR{remoteP2PContainer})
+		Expect(err).NotTo(HaveOccurred())
+	})
+
+	ginkgo.DescribeTable("Session is established and route is advertised", func(prefixSendFromLocal []string, p2pInterface string, tweakService func(svc *corev1.Service)) {
+		rc := frrconfig.RouterConfigUnnumbered{
+			ASNLocal:  metalLBASN,
+			ASNRemote: metalLBASN,
+			Hostname:  "tor1",
+			Interface: p2pInterface,
+			RouterID:  "1.1.1.1",
+		}
+
+		var err error
+		ginkgo.By(fmt.Sprintf("creating p2p %s:%s -- %s:remote-p2p-container", nodeWithP2PConnection.Name, p2pInterface, p2pInterface))
+		remoteP2PContainer, err = frrcontainer.CreateP2PPeerFor(nodeWithP2PConnection.Name, p2pInterface, FRRImage)
+		Expect(err).NotTo(HaveOccurred())
+		ginkgo.By(fmt.Sprintf("updating frrconfig to %s", remoteP2PContainer.Name))
+
+		c, err := rc.Config()
+		Expect(err).NotTo(HaveOccurred())
+		err = remoteP2PContainer.UpdateBGPConfigFile(c)
+		Expect(err).NotTo(HaveOccurred())
+
+		resources := config.Resources{
+			Peers: []metallbv1beta2.BGPPeer{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "tor",
+						Namespace: metallb.Namespace,
+					},
+					Spec: metallbv1beta2.BGPPeerSpec{
+						Interface:  p2pInterface,
+						ASN:        rc.ASNRemote,
+						MyASN:      rc.ASNLocal,
+						BFDProfile: "simple",
+					},
+				},
+			},
+			BFDProfiles: []metallbv1beta1.BFDProfile{{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "simple",
+				},
+			},
+			},
+			Pools: []metallbv1beta1.IPAddressPool{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "bgp-test",
+					},
+					Spec: metallbv1beta1.IPAddressPoolSpec{
+						Addresses: prefixSendFromLocal,
+					},
+				},
+			},
+			BGPAdvs: []metallbv1beta1.BGPAdvertisement{
+				{ObjectMeta: metav1.ObjectMeta{Name: "empty"}},
+			},
+		}
+
+		err = ConfigUpdater.Update(resources)
+		Expect(err).NotTo(HaveOccurred(), "apply the CR in k8s api failed")
+
+		nodeP2PContainer := executor.ForContainer(nodeWithP2PConnection.Name)
+		nodeLLA, err := netdev.LinkLocalAddressForDevice(nodeP2PContainer, p2pInterface)
+		Expect(err).NotTo(HaveOccurred())
+		ginkgo.By("validating the node and p2p container peered")
+		validateUnnumberedBGPPeering(remoteP2PContainer, nodeLLA)
+
+		svc, _ := testservice.CreateWithBackend(cs, testNamespace, "unnumbered-lb", tweakService)
+		ginkgo.By("checking the service gets an ip assigned")
+		for _, i := range svc.Status.LoadBalancer.Ingress {
+			ingressIP := jigservice.GetIngressPoint(&i)
+			err = config.ValidateIPInRange(resources.Pools, ingressIP)
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		ginkgo.By(fmt.Sprintf("validating the p2p peer %s received the routes from node", remoteP2PContainer.Name))
+		validatePeerRoutesViaDevice(remoteP2PContainer, p2pInterface, nodeLLA, prefixSendFromLocal)
+
+	},
+		ginkgo.Entry("IPV4", []string{"5.5.5.5/32"}, "net10", func(_ *corev1.Service) {}),
+		ginkgo.Entry("IPV6", []string{"5555::1/128"}, "net20", func(_ *corev1.Service) {}),
+		ginkgo.Entry("DUALSTACK", []string{"5.5.5.5/32", "5555::1/128"}, "net30",
+			func(svc *corev1.Service) { testservice.DualStack(svc) }),
+	)
+})
+
+func validateUnnumberedBGPPeering(peer *frrcontainer.FRR, nodeLLA string) {
+	ginkgo.By(fmt.Sprintf("validating BGP peering to %s", peer.Name))
+	Eventually(func() error {
+		neighbors, err := frr.NeighborsInfo(peer)
+		if err != nil {
+			return err
+		}
+		for _, n := range neighbors {
+			if n.BGPNeighborAddr == nodeLLA && n.Connected && n.BFDInfo.Status == "Up" {
+				return nil
+			}
+		}
+		return fmt.Errorf("no connected neighbor was found with %s", nodeLLA)
+	}, 2*time.Minute, 10*time.Second).ShouldNot(HaveOccurred(),
+		"timed out waiting to validate nodes peered with the frr instance")
+}
+
+// validatePeerRoutesViaDevice validates that the peer has BGP routes to the
+// specified prefixes with the expected next-hop address on the specified device.
+func validatePeerRoutesViaDevice(peer executor.Executor, dev, nextHop string, prefixes ...[]string) {
+	ginkgo.By(fmt.Sprintf("validating prefix %s to %s dev %s", prefixes, nextHop, dev))
+	Eventually(func() error {
+		nextHopAddr := netip.MustParseAddr(nextHop)
+		want := make(map[netip.Prefix]map[netip.Addr]struct{})
+		for _, prf := range prefixes {
+			for _, p := range prf {
+				want[netip.MustParsePrefix(p)] = map[netip.Addr]struct{}{nextHopAddr: {}}
+			}
+		}
+
+		got, err := container.BGPRoutes(peer, dev)
+		if err != nil {
+			return err
+		}
+		if !cmp.Equal(want, got) {
+			return fmt.Errorf("want %v\n got %v\n diff %v", want, got, cmp.Diff(want, got))
+		}
+		return nil
+	}, 30*time.Second, 5*time.Second).ShouldNot(HaveOccurred(), fmt.Sprintf("peer should have the routes %s", prefixes))
+}
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index 7fa8367b..5dae4350 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -133,6 +133,7 @@ var _ = ginkgo.BeforeSuite(func() {
 		bgptests.FRRContainers, err = bgptests.HostContainerSetup(frrImage, hostBGPMode)
 		Expect(err).NotTo(HaveOccurred())
 	default:
+		bgptests.FRRImage = frrImage
 		bgptests.FRRContainers, err = bgptests.KindnetContainersSetup(cs, frrImage)
 		Expect(err).NotTo(HaveOccurred())
 		if withVRF {
diff --git a/e2etest/go.work.sum b/e2etest/go.work.sum
index 3fed727b..f85097d6 100644
--- a/e2etest/go.work.sum
+++ b/e2etest/go.work.sum
@@ -648,6 +648,7 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -680,6 +681,7 @@ github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6
 github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
 github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
 github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -697,6 +699,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
 github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
@@ -716,7 +719,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
@@ -780,6 +782,7 @@ github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHz
 github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
 github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
 github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -797,6 +800,7 @@ github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLO
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -804,6 +808,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+github.com/metallb/frr-k8s v0.0.0-20241217094531-80d4b7a5591b h1:3+sSwasbGnVDjF84TRsxlCjbgko9tkDbw/kaj+NKPD4=
+github.com/metallb/frr-k8s v0.0.0-20241217094531-80d4b7a5591b/go.mod h1:gcOVS4IiQ7r6SRKc2HBgYll3Ku9SHEatxaqgNiuC/Tg=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
 github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
@@ -841,6 +847,7 @@ github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xl
 github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
 github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
 github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
 github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
 github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
@@ -850,6 +857,7 @@ github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P
 github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
 github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk=
 github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
 github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
 github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
 github.com/open-policy-agent/cert-controller v0.10.1/go.mod h1:4uRbBLY5DsPOog+a9pqk3JLxuuhrWsbUedQW65HcLTI=
@@ -893,6 +901,8 @@ github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr
 github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
 github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
 github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
 github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
 github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
 github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
@@ -904,6 +914,7 @@ github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO
 github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
 github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
 github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
 github.com/prometheus/exporter-toolkit v0.7.1 h1:c6RXaK8xBVercEeUQ4tRNL8UGWzDHfvj9dseo1FcK1Y=
 github.com/prometheus/exporter-toolkit v0.7.1/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -976,6 +987,7 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@@ -1182,6 +1194,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1210,6 +1223,7 @@ golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1250,9 +1264,11 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
 golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1267,6 +1283,9 @@ golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQ
 golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
 golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
 golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1343,6 +1362,7 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
@@ -1350,6 +1370,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -1362,13 +1383,16 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -1554,7 +1578,9 @@ google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -1659,6 +1685,7 @@ k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/s
 k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo=
 k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
+k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
 k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
@@ -1679,6 +1706,7 @@ k8s.io/kube-aggregator v0.29.3/go.mod h1:xGJqV/SJJ1fbwTGfQLAZfwgqX1EMoaqfotDTkDr
 k8s.io/kube-controller-manager v0.29.3/go.mod h1:Tf+BQG52nu8f2Ra9he9MsNFJ2sK7U3TSVvmTB+OVgiw=
 k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
 k8s.io/kube-proxy v0.29.3/go.mod h1:VyAG5i+d+crJ0WM6Ik/4wSq+VttAFDfD4n3z+0hEQl8=
 k8s.io/kube-scheduler v0.29.3/go.mod h1:1NLHViSwFFddWHH4U9UGD57clINAtje/PEs6PjOYQZg=
 k8s.io/kubectl v0.29.3 h1:RuwyyIU42MAISRIePaa8Q7A3U74Q9P4MoJbDFz9o3us=
@@ -1701,6 +1729,8 @@ k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
@@ -1713,8 +1743,11 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
 sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s=
 sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3/go.mod h1:/d88dHCvoy7d0AKFT0yytezSGZKjsZBVs9YTkBHSGFk=
 sigs.k8s.io/kustomize/kustomize/v5 v5.4.2/go.mod h1:5ypfJVYlPb2MKKeoGknVLxvHemDlQT+szI4+KOhnD6k=
 sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
 sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/e2etest/pkg/container/routes.go b/e2etest/pkg/container/routes.go
index 8c2ebfd5..45561c9f 100644
--- a/e2etest/pkg/container/routes.go
+++ b/e2etest/pkg/container/routes.go
@@ -134,30 +134,80 @@ func WireContainers(containerA, containerB, dev string) error {
 	return nil
 }
 
+type Nexthop struct {
+	Gateway string `json:"gateway"`
+	Dev     string `json:"dev"`
+}
+
+type IPRoute struct {
+	Dst      string    `json:"dst"`
+	Nexthops []Nexthop `json:"nexthops,omitempty"`
+	Gateway  string    `json:"gateway,omitempty"`
+	Via      *struct {
+		Family string `json:"family"`
+		Host   string `json:"host"`
+	} `json:"via,omitempty"`
+	Dev string `json:"dev"`
+}
+
+func parseIPRouteOutput(r IPRoute, dev string) (netip.Prefix, map[netip.Addr]struct{}, error) {
+	dst, err := netip.ParsePrefix(r.Dst)
+	if err != nil {
+		// `ip route` command does not print the prefix when /32 or /128
+		addr, err := netip.ParseAddr(r.Dst)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid prefix %s: %w", r.Dst, err)
+		}
+		dst, err = netip.ParsePrefix(r.Dst + "/32")
+		if addr.Is6() {
+			dst, err = netip.ParsePrefix(r.Dst + "/128")
+		}
+	}
+
+	nextHops := make(map[netip.Addr]struct{})
+	// this is for ipv4 single next-hop
+	if r.Via != nil {
+		addr, err := netip.ParseAddr(r.Via.Host)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid next-hop %s: %w", r.Via.Host, err)
+		}
+		if dev == "" || r.Dev == dev {
+			nextHops[addr] = struct{}{}
+		}
+	}
+
+	// this is for ipv4 multiple next-hops
+	for _, nh := range r.Nexthops {
+		addr, err := netip.ParseAddr(nh.Gateway)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid next-hop %s: %w", nh.Gateway, err)
+		}
+
+		if dev == "" || nh.Dev == dev {
+			nextHops[addr] = struct{}{}
+		}
+	}
+
+	// this is for ipv6
+	if r.Gateway != "" {
+		addr, err := netip.ParseAddr(r.Gateway)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid next-hop %s: %w", r.Gateway, err)
+		}
+		if dev == "" || r.Dev == dev {
+			nextHops[addr] = struct{}{}
+		}
+	}
+
+	return dst, nextHops, nil
+}
+
 // BGPRoutes executes `ip route show proto bgp` in the executor and returns all
 // routes filtered by device name e.g. net0. If device name is the empty string
 // no filtering takes place. The return is map[destination CIDR]-> set[nextHops].
 func BGPRoutes(exc executor.Executor, dev string) (map[netip.Prefix]map[netip.Addr]struct{}, error) {
 	ret := make(map[netip.Prefix]map[netip.Addr]struct{})
 
-	type Nexthop struct {
-		Gateway string   `json:"gateway"`
-		Dev     string   `json:"dev"`
-		Weight  int      `json:"weight"`
-		Flags   []string `json:"flags"`
-	}
-
-	type IPRoute struct {
-		Dst      string    `json:"dst"`
-		Nexthops []Nexthop `json:"nexthops,omitempty"`
-		Gateway  string    `json:"gateway,omitempty"`
-		Via      *struct {
-			Family string `json:"family"`
-			Host   string `json:"host"`
-		} `json:"via,omitempty"`
-		Dev string `json:"dev"`
-	}
-
 	for _, proto := range []string{"-4", "-6"} {
 		out, err := exc.Exec("ip", proto, "--json", "route", "show", "proto", "bgp")
 		if err != nil {
@@ -171,49 +221,15 @@ func BGPRoutes(exc executor.Executor, dev string) (map[netip.Prefix]map[netip.Ad
 		}
 
 		for _, r := range routes {
-			dst, err := netip.ParsePrefix(r.Dst)
+			dst, nextHops, err := parseIPRouteOutput(r, dev)
 			if err != nil {
-				return nil, fmt.Errorf("invalid prefix %s: %w", r.Dst, err)
-			}
-
-			nextHops := make(map[netip.Addr]struct{})
-			// this is for ipv4 single next-hop
-			if r.Via != nil {
-				addr, err := netip.ParseAddr(r.Via.Host)
-				if err != nil {
-					return nil, fmt.Errorf("invalid next-hop %s: %w", r.Via.Host, err)
-				}
-				if dev == "" || r.Dev == dev {
-					nextHops[addr] = struct{}{}
-				}
-			}
-
-			// this is for ipv4 multiple next-hops
-			for _, nh := range r.Nexthops {
-				addr, err := netip.ParseAddr(nh.Gateway)
-				if err != nil {
-					return nil, fmt.Errorf("invalid next-hop %s: %w", nh.Gateway, err)
-				}
-
-				if dev == "" || nh.Dev == dev {
-					nextHops[addr] = struct{}{}
-				}
-			}
-
-			// this is for ipv6
-			if r.Gateway != "" {
-				addr, err := netip.ParseAddr(r.Gateway)
-				if err != nil {
-					return nil, fmt.Errorf("invalid next-hop %s: %w", r.Gateway, err)
-				}
-				if dev == "" || r.Dev == dev {
-					nextHops[addr] = struct{}{}
-				}
+				return nil, err
 			}
 
 			if len(nextHops) == 0 {
 				continue
 			}
+
 			ret[dst] = nextHops
 		}
 	}
diff --git a/internal/bgp/frr/frr_bfd_test.go b/internal/bgp/frr/frr_bfd_test.go
index e7cb6a0f..b2bc0779 100644
--- a/internal/bgp/frr/frr_bfd_test.go
+++ b/internal/bgp/frr/frr_bfd_test.go
@@ -124,7 +124,8 @@ func TestBFDWithSession(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index 0bcdfd4a..0d0320c4 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -119,7 +119,8 @@ func TestSingleEBGPSessionMultiHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -146,7 +147,8 @@ func TestSingleEBGPSessionOneHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "127.0.0.2:179",
+			PeerAddress:   "127.0.0.2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -174,7 +176,8 @@ func TestSingleIPv6EBGPSessionOneHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[127:0:0::2]:179",
+			PeerAddress:   "127:0:0::2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -202,7 +205,8 @@ func TestSingleIBGPSession(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -230,7 +234,8 @@ func TestSingleIPv6IBGPSession(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -259,7 +264,8 @@ func TestSingleSessionClose(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -287,7 +293,8 @@ func TestSingleSessionWithGracefulRestart(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           102,
 			RouterID:        net.ParseIP("10.1.1.254"),
@@ -311,7 +318,8 @@ func TestTwoSessions(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -330,7 +338,8 @@ func TestTwoSessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -360,7 +369,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -377,7 +387,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:4:4::255]:179",
+			PeerAddress:   "10:4:4::255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:3:3::254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -405,7 +416,8 @@ func TestIPv4AndIPv6SessionsDisableMP(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -424,7 +436,8 @@ func TestIPv4AndIPv6SessionsDisableMP(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -452,7 +465,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -470,7 +484,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -498,7 +513,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -515,7 +531,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -543,7 +560,8 @@ func TestSingleAdvertisement(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -591,7 +609,8 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      nil,
@@ -625,34 +644,6 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	testCheckConfigFile(t)
 }
 
-func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
-	testSetup(t)
-
-	l := log.NewNopLogger()
-	sessionManager := mockNewSessionManager(l, logging.LevelInfo)
-	defer close(sessionManager.reloadConfig)
-	session, err := sessionManager.NewSession(l,
-		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254",
-			SourceAddress: net.ParseIP("10.1.1.254"),
-			MyASN:         100,
-			RouterID:      net.ParseIP("10.1.1.254"),
-			PeerASN:       200,
-			HoldTime:      ptr.To(time.Second),
-			KeepAliveTime: ptr.To(time.Second),
-			Password:      "password",
-			CurrentNode:   "hostname",
-			EBGPMultiHop:  true,
-			SessionName:   "test-peer"})
-
-	if err == nil {
-		session.Close()
-		t.Fatalf("Should not be able to create session")
-	}
-
-	// Not checking the file since this test won't create it
-}
-
 func TestSingleAdvertisementStop(t *testing.T) {
 	testSetup(t)
 
@@ -661,7 +652,8 @@ func TestSingleAdvertisementStop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -708,7 +700,8 @@ func TestSingleAdvertisementChange(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -763,7 +756,8 @@ func TestTwoAdvertisements(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -816,7 +810,8 @@ func TestTwoAdvertisementsDuplicate(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -863,7 +858,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 		func() {
 			sessionsParameters := []bgp.SessionParameters{
 				{
-					PeerAddress:   "10.2.2.254:179",
+					PeerAddress:   "10.2.2.254",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -876,7 +872,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					EBGPMultiHop:  true,
 					SessionName:   "test-peer"},
 				{
-					PeerAddress:   "10.2.2.255:179",
+					PeerAddress:   "10.2.2.255",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -965,7 +962,8 @@ func TestSingleSessionExtras(t *testing.T) {
 	}
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "127.0.0.2:179",
+			PeerAddress:   "127.0.0.2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -1046,7 +1044,8 @@ func TestLargeCommunities(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -1095,7 +1094,8 @@ func TestManyAdvertisementsSameCommunity(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -1143,7 +1143,8 @@ func TestSingleSessionWithNoTimers(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       102,
 			PeerASN:     100,
 			SessionName: "test-peer"})
@@ -1164,7 +1165,8 @@ func TestSingleSessionWithZeroTimers(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         102,
 			HoldTime:      ptr.To(0 * time.Second),
 			KeepAliveTime: ptr.To(0 * time.Second),
@@ -1366,7 +1368,8 @@ func TestSingleSessionWithInternalASN(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           102,
 			RouterID:        net.ParseIP("10.1.1.254"),
@@ -1390,7 +1393,8 @@ func TestSingleSessionWithExternalASN(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           102,
 			RouterID:        net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frr/frr_vrf_test.go b/internal/bgp/frr/frr_vrf_test.go
index c21a1e1f..3d1dc7f9 100644
--- a/internal/bgp/frr/frr_vrf_test.go
+++ b/internal/bgp/frr/frr_vrf_test.go
@@ -22,7 +22,8 @@ func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -50,7 +51,8 @@ func TestSingleVRFIBGPSession(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -79,7 +81,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -97,7 +100,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -126,7 +130,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -144,7 +149,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -173,7 +179,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -189,7 +196,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -216,7 +224,8 @@ func TestSingleAdvertisementVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -264,7 +273,8 @@ func TestSingleAdvertisementChangeVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -320,7 +330,8 @@ func TestTwoAdvertisementsVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -374,7 +385,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -392,7 +404,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.255:179",
+			PeerAddress:   "10.2.2.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frrk8s/frrk8s_bfd_test.go b/internal/bgp/frrk8s/frrk8s_bfd_test.go
index 075e0f92..f98092cf 100644
--- a/internal/bgp/frrk8s/frrk8s_bfd_test.go
+++ b/internal/bgp/frrk8s/frrk8s_bfd_test.go
@@ -112,7 +112,8 @@ func TestBFDWithSession(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frrk8s/frrk8s_test.go b/internal/bgp/frrk8s/frrk8s_test.go
index a7cafdd4..9c044939 100644
--- a/internal/bgp/frrk8s/frrk8s_test.go
+++ b/internal/bgp/frrk8s/frrk8s_test.go
@@ -121,7 +121,8 @@ func TestSingleEBGPSessionGracefullRestart(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           100,
 			RouterID:        net.ParseIP("10.1.1.254"),
@@ -146,7 +147,8 @@ func TestSingleEBGPSessionMultiHop(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -170,7 +172,8 @@ func TestSingleEBGPSessionOneHop(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "127.0.0.2:179",
+			PeerAddress:   "127.0.0.2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -195,7 +198,8 @@ func TestSingleIPv6EBGPSessionOneHop(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[127:0:0::2]:179",
+			PeerAddress:   "127:0:0::2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -220,7 +224,8 @@ func TestSingleIBGPSession(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -245,7 +250,8 @@ func TestSingleIPv6IBGPSession(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -270,7 +276,8 @@ func TestSingleSessionClose(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -294,7 +301,8 @@ func TestTwoSessions(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -312,7 +320,8 @@ func TestTwoSessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -341,7 +350,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -358,7 +368,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:4:4::255]:179",
+			PeerAddress:   "10:4:4::255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:3:3::254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -382,7 +393,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -400,7 +412,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -425,7 +438,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -442,7 +456,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -467,7 +482,8 @@ func TestSingleAdvertisement(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -511,7 +527,8 @@ func TestTwoAdvertisementsWithSamePrefix(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -564,7 +581,8 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      nil,
@@ -598,39 +616,14 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	testCheckConfigFile(t)
 }
 
-func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
-	sessionManager := newTestSessionManager(t)
-
-	l := log.NewNopLogger()
-	session, err := sessionManager.NewSession(l,
-		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254",
-			SourceAddress: net.ParseIP("10.1.1.254"),
-			MyASN:         100,
-			RouterID:      net.ParseIP("10.1.1.254"),
-			PeerASN:       200,
-			HoldTime:      ptr.To(time.Second),
-			KeepAliveTime: ptr.To(time.Second),
-			Password:      "password",
-			CurrentNode:   "hostname",
-			EBGPMultiHop:  true,
-			SessionName:   "test-peer"})
-
-	if err == nil {
-		session.Close()
-		t.Fatalf("Should not be able to create session")
-	}
-
-	// Not checking the file since this test won't create it
-}
-
 func TestSingleAdvertisementStop(t *testing.T) {
 	l := log.NewNopLogger()
 	sessionManager := newTestSessionManager(t)
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -675,7 +668,8 @@ func TestSingleAdvertisementChange(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -728,7 +722,8 @@ func TestTwoAdvertisements(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -781,7 +776,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 		func() {
 			sessionsParameters := []bgp.SessionParameters{
 				{
-					PeerAddress:   "10.2.2.254:179",
+					PeerAddress:   "10.2.2.254",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -793,7 +789,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					EBGPMultiHop:  true,
 					SessionName:   "test-peer"},
 				{
-					PeerAddress:   "10.2.2.255:179",
+					PeerAddress:   "10.2.2.255",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -874,7 +871,8 @@ func TestLargeCommunities(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -921,7 +919,8 @@ func TestSingleSessionWithNoTimers(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       102,
 			PeerASN:     100,
 			SessionName: "test-peer"})
@@ -940,7 +939,8 @@ func TestSingleSessionWithZeroTimers(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         102,
 			HoldTime:      ptr.To(0 * time.Second),
 			KeepAliveTime: ptr.To(0 * time.Second),
@@ -961,7 +961,8 @@ func TestSingleSessionWithInternalASN(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       100,
 			RouterID:    net.ParseIP("10.1.1.254"),
 			DynamicASN:  "internal",
@@ -981,7 +982,8 @@ func TestSingleSessionWithExternalASN(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       100,
 			RouterID:    net.ParseIP("10.1.1.254"),
 			DynamicASN:  "external",
@@ -994,3 +996,24 @@ func TestSingleSessionWithExternalASN(t *testing.T) {
 
 	testCheckConfigFile(t)
 }
+
+func TestSingleSessionWithInterface(t *testing.T) {
+	sessionManager := newTestSessionManager(t)
+	l := log.NewNopLogger()
+
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerPort:      179,
+			PeerInterface: "net0",
+			MyASN:         100,
+			RouterID:      net.ParseIP("10.1.1.254"),
+			DynamicASN:    "internal",
+			CurrentNode:   "hostname",
+			SessionName:   "test-peer"})
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
diff --git a/internal/bgp/frrk8s/frrk8s_vrf_test.go b/internal/bgp/frrk8s/frrk8s_vrf_test.go
index 681cbfe5..d89c4aec 100644
--- a/internal/bgp/frrk8s/frrk8s_vrf_test.go
+++ b/internal/bgp/frrk8s/frrk8s_vrf_test.go
@@ -19,7 +19,8 @@ func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -45,7 +46,8 @@ func TestSingleVRFIBGPSession(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -72,7 +74,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -90,7 +93,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -117,7 +121,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -135,7 +140,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -162,7 +168,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -178,7 +185,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -203,7 +211,8 @@ func TestSingleAdvertisementVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -249,7 +258,8 @@ func TestSingleAdvertisementChangeVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -303,7 +313,8 @@ func TestTwoAdvertisementsVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -355,7 +366,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -373,7 +385,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.255:179",
+			PeerAddress:   "10.2.2.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frrk8s/testdata/TestSingleSessionWithInterface.golden b/internal/bgp/frrk8s/testdata/TestSingleSessionWithInterface.golden
new file mode 100644
index 00000000..e7320660
--- /dev/null
+++ b/internal/bgp/frrk8s/testdata/TestSingleSessionWithInterface.golden
@@ -0,0 +1,38 @@
+{
+    "metadata": {
+        "name": "metallb-testnodename",
+        "namespace": "testnamespace",
+        "creationTimestamp": null
+    },
+    "spec": {
+        "bgp": {
+            "routers": [
+                {
+                    "asn": 100,
+                    "id": "10.1.1.254",
+                    "neighbors": [
+                        {
+                            "dynamicASN": "internal",
+                            "interface": "net0",
+                            "port": 179,
+                            "passwordSecret": {},
+                            "toAdvertise": {
+                                "allowed": {}
+                            },
+                            "toReceive": {
+                                "allowed": {}
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        "raw": {},
+        "nodeSelector": {
+            "matchLabels": {
+                "kubernetes.io/hostname": "testnodename"
+            }
+        }
+    },
+    "status": {}
+}
\ No newline at end of file
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index d08ba6a2..39866140 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -3499,6 +3499,72 @@ func TestParse(t *testing.T) {
 				},
 			},
 		},
+		{
+			desc: "unnumbered peer without address with interface ok",
+			crs: ClusterResources{
+
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "peer1",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:      42,
+							DynamicASN: v1beta2.InternalASNMode,
+							Interface:  "net0",
+						},
+					},
+				},
+			},
+			want: &Config{
+				Peers: map[string]*Peer{
+					"peer1": {
+						Name:          "peer1",
+						Iface:         "net0",
+						MyASN:         42,
+						DynamicASN:    "internal",
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						EBGPMultiHop:  false,
+					},
+				},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+		{
+			desc: "unnumbered peer without address without interface nok",
+			crs: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "invalid",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:      42,
+							DynamicASN: "internal",
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "unnumbered peer with address with interface nok",
+			crs: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "invalid",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							Address:    "1.2.3.4",
+							Interface:  "net0",
+							MyASN:      42,
+							DynamicASN: "internal",
+						},
+					},
+				},
+			},
+		},
 	}
 
 	for _, test := range tests {
diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go
index cd4ae44c..5a2a0a1b 100644
--- a/internal/config/validation_test.go
+++ b/internal/config/validation_test.go
@@ -207,6 +207,19 @@ func TestValidate(t *testing.T) {
 			},
 			mustFail: true,
 		},
+		{
+			desc: "interface",
+			config: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						Spec: v1beta2.BGPPeerSpec{
+							Interface: "eth0",
+						},
+					},
+				},
+			},
+			mustFail: true,
+		},
 		{
 			desc: "keepalive time",
 			config: ClusterResources{
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index 1490737d..0c131d98 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -285,7 +285,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				}},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 		{
@@ -313,7 +313,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -343,7 +343,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -377,7 +377,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -420,7 +420,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -493,7 +493,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -509,7 +509,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			},
 			eps: []discovery.EndpointSlice{},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -539,7 +539,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -578,7 +578,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -613,7 +613,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -648,7 +648,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -712,7 +712,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix:    ipnet("10.20.30.1/32"),
 						LocalPref: 100,
@@ -786,7 +786,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix:    ipnet("10.20.30.1/32"),
 						LocalPref: 100,
@@ -859,7 +859,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			},
 
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix:    ipnet("10.20.30.1/32"),
 						LocalPref: 100,
@@ -927,12 +927,12 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -965,12 +965,12 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1004,7 +1004,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1012,7 +1012,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						Prefix: ipnet("10.20.30.5/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1049,7 +1049,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					// Prefixes duplicated because the dedupe happens
 					// inside the real BGP session.
 					{
@@ -1059,7 +1059,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1074,12 +1074,12 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			desc:     "Delete svc",
 			balancer: "test1",
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1132,7 +1132,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1144,7 +1144,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			desc:     "Delete second svc",
 			balancer: "test2",
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.5:0": nil,
+				"1.2.3.5": nil,
 			},
 		},
 	}
@@ -1223,7 +1223,7 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -1245,7 +1245,7 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -1260,8 +1260,8 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1276,7 +1276,7 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -1298,8 +1298,8 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1314,8 +1314,8 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1338,8 +1338,8 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1354,8 +1354,8 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1371,8 +1371,8 @@ func TestNodeSelectors(t *testing.T) {
 				Spec: v1.NodeSpec{Unschedulable: true},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 			wantReturnState: controllers.SyncStateReprocessAll,
 		},
@@ -1389,8 +1389,8 @@ func TestNodeSelectors(t *testing.T) {
 				Spec: v1.NodeSpec{Unschedulable: true},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1406,8 +1406,8 @@ func TestNodeSelectors(t *testing.T) {
 				Spec: v1.NodeSpec{Unschedulable: false},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 			wantReturnState: controllers.SyncStateReprocessAll,
 		},