Repository: metallb/metallb
Pull request #2508 (2024-08-30 06:52:28)

Unnumbered BGP Peering design document
 /kind design

**What this PR does / why we need it**:

**Special notes for your reviewer**:

In part of this PR there is
1. A design document about unnumbered BGP peering, discussing implementation and API changes
2. A draft version of how an e2e test can run, more for discussion rather than actually merging that file in that PR.

My plan would be that first we discuss how the raw frr config would look like, then the API in frr-k8s, then in metallb.

Thanks

**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".
4. Follow the instructions for writing a release note from k8s: https://git.k8s.io/community/contributors/guide/release-notes.md
5. If no release note is required, just write "NONE".
-->

```release-note
None
```


karampok: @fedepaol  @oribon I have updated the doc and I think I have addressed all your feedback. Please take a look, thanks

Patch:
diff --git a/api/v1beta2/bgppeer_types.go b/api/v1beta2/bgppeer_types.go
index ef3aec06..836d11be 100644
--- a/api/v1beta2/bgppeer_types.go
+++ b/api/v1beta2/bgppeer_types.go
@@ -29,9 +29,20 @@ type BGPPeerSpec struct {
 	MyASN uint32 `json:"myASN"`
 
 	// AS number to expect from the remote end of the session.
+	// ASN and DynamicASN are mutually exclusive and one of them must be specified.
 	// +kubebuilder:validation:Minimum=0
 	// +kubebuilder:validation:Maximum=4294967295
-	ASN uint32 `json:"peerASN"`
+	// +optional
+	ASN uint32 `json:"peerASN,omitempty"`
+
+	// DynamicASN detects the AS number to use for the remote end of the session
+	// without explicitly setting it via the ASN field. Limited to:
+	// internal - if the neighbor's ASN is different than MyASN connection is denied.
+	// external - if the neighbor's ASN is the same as MyASN the connection is denied.
+	// ASN and DynamicASN are mutually exclusive and one of them must be specified.
+	// +kubebuilder:validation:Enum=internal;external
+	// +optional
+	DynamicASN DynamicASNMode `json:"dynamicASN,omitempty"`
 
 	// Address to dial when establishing the session.
 	Address string `json:"peerAddress"`
@@ -144,3 +155,10 @@ type BGPPeerList struct {
 func init() {
 	SchemeBuilder.Register(&BGPPeer{}, &BGPPeerList{})
 }
+
+type DynamicASNMode string
+
+const (
+	InternalASNMode DynamicASNMode = "internal"
+	ExternalASNMode DynamicASNMode = "external"
+)
diff --git a/charts/metallb/README.md b/charts/metallb/README.md
index add286f4..6a7f039e 100644
--- a/charts/metallb/README.md
+++ b/charts/metallb/README.md
@@ -79,17 +79,17 @@ Kubernetes: `>= 1.19.0-0`
 | prometheus.podMonitor.relabelings | list | `[]` |  |
 | prometheus.prometheusRule.additionalLabels | object | `{}` |  |
 | prometheus.prometheusRule.addressPoolExhausted.enabled | bool | `true` |  |
-| prometheus.prometheusRule.addressPoolExhausted.labels.severity | string | `"alert"` |  |
+| prometheus.prometheusRule.addressPoolExhausted.labels.severity | string | `"critical"` |  |
 | prometheus.prometheusRule.addressPoolUsage.enabled | bool | `true` |  |
 | prometheus.prometheusRule.addressPoolUsage.thresholds[0].labels.severity | string | `"warning"` |  |
 | prometheus.prometheusRule.addressPoolUsage.thresholds[0].percent | int | `75` |  |
 | prometheus.prometheusRule.addressPoolUsage.thresholds[1].labels.severity | string | `"warning"` |  |
 | prometheus.prometheusRule.addressPoolUsage.thresholds[1].percent | int | `85` |  |
-| prometheus.prometheusRule.addressPoolUsage.thresholds[2].labels.severity | string | `"alert"` |  |
+| prometheus.prometheusRule.addressPoolUsage.thresholds[2].labels.severity | string | `"critical"` |  |
 | prometheus.prometheusRule.addressPoolUsage.thresholds[2].percent | int | `95` |  |
 | prometheus.prometheusRule.annotations | object | `{}` |  |
 | prometheus.prometheusRule.bgpSessionDown.enabled | bool | `true` |  |
-| prometheus.prometheusRule.bgpSessionDown.labels.severity | string | `"alert"` |  |
+| prometheus.prometheusRule.bgpSessionDown.labels.severity | string | `"critical"` |  |
 | prometheus.prometheusRule.configNotLoaded.enabled | bool | `true` |  |
 | prometheus.prometheusRule.configNotLoaded.labels.severity | string | `"warning"` |  |
 | prometheus.prometheusRule.enabled | bool | `false` |  |
diff --git a/charts/metallb/charts/crds/templates/crds.yaml b/charts/metallb/charts/crds/templates/crds.yaml
index 50ad4ff8..998620dc 100644
--- a/charts/metallb/charts/crds/templates/crds.yaml
+++ b/charts/metallb/charts/crds/templates/crds.yaml
@@ -528,6 +528,17 @@ spec:
                   default: false
                   description: To set if we want to disable MP BGP that will separate IPv4 and IPv6 route exchanges into distinct BGP sessions.
                   type: boolean
+                dynamicASN:
+                  description: |-
+                    DynamicASN detects the AS number to use for the remote end of the session
+                    without explicitly setting it via the ASN field. Limited to:
+                    internal - if the neighbor's ASN is different than MyASN connection is denied.
+                    external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                    ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                  enum:
+                    - internal
+                    - external
+                  type: string
                 ebgpMultiHop:
                   description: To set if the BGPPeer is multi-hops away. Needed for FRR mode only.
                   type: boolean
@@ -624,7 +635,9 @@ spec:
                   type: object
                   x-kubernetes-map-type: atomic
                 peerASN:
-                  description: AS number to expect from the remote end of the session.
+                  description: |-
+                    AS number to expect from the remote end of the session.
+                    ASN and DynamicASN are mutually exclusive and one of them must be specified.
                   format: int32
                   maximum: 4294967295
                   minimum: 0
@@ -651,7 +664,6 @@ spec:
                   type: string
               required:
                 - myASN
-                - peerASN
                 - peerAddress
               type: object
             status:
diff --git a/charts/metallb/templates/prometheusrules.yaml b/charts/metallb/templates/prometheusrules.yaml
index e811ef13..64e44c60 100644
--- a/charts/metallb/templates/prometheusrules.yaml
+++ b/charts/metallb/templates/prometheusrules.yaml
@@ -19,8 +19,8 @@ spec:
     {{- if .Values.prometheus.prometheusRule.staleConfig.enabled }}
     - alert: MetalLBStaleConfig
       annotations:
-        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
-          }} has a stale config for > 1 minute'`}}
+        summary: {{`'Stale config on {{ $labels.pod }}'`}}
+        description: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod }} has a stale config for > 1 minute'`}}
       expr: metallb_k8s_client_config_stale_bool{job=~"{{ template "metallb.fullname" . }}.*"} == 1
       for: 1m
       {{- with .Values.prometheus.prometheusRule.staleConfig.labels }}
@@ -31,8 +31,8 @@ spec:
     {{- if .Values.prometheus.prometheusRule.configNotLoaded.enabled }}
     - alert: MetalLBConfigNotLoaded
       annotations:
-        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
-          }} has not loaded for > 1 minute'`}}
+        summary: {{`'Config on {{ $labels.pod }} has not been loaded'`}}
+        description: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod }} has not loaded for > 1 minute'`}}
       expr: metallb_k8s_client_config_loaded_bool{job=~"{{ template "metallb.fullname" . }}.*"} == 0
       for: 1m
       {{- with .Values.prometheus.prometheusRule.configNotLoaded.labels }}
@@ -43,8 +43,8 @@ spec:
     {{- if .Values.prometheus.prometheusRule.addressPoolExhausted.enabled }}
     - alert: MetalLBAddressPoolExhausted
       annotations:
-        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
-          }} has exhausted address pool {{ $labels.pool }} for > 1 minute'`}}
+        summary: {{`'Exhausted address pool on {{ $labels.pod }}'`}}
+        description: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod }} has exhausted address pool {{ $labels.pool }} for > 1 minute'`}}
       expr: metallb_allocator_addresses_in_use_total >= on(pool) metallb_allocator_addresses_total
       for: 1m
       {{- with .Values.prometheus.prometheusRule.addressPoolExhausted.labels }}
@@ -57,8 +57,8 @@ spec:
     {{- range .Values.prometheus.prometheusRule.addressPoolUsage.thresholds }}
     - alert: MetalLBAddressPoolUsage{{ .percent }}Percent
       annotations:
-        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
-          }} has address pool {{ $labels.pool }} past `}}{{ .percent }}{{`% usage for > 1 minute'`}}
+        summary: {{`'Exhausted address pool on {{ $labels.pod }}'`}}
+        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod }} has address pool {{ $labels.pool }} past `}}{{ .percent }}{{`% usage for > 1 minute'`}}
       expr: ( metallb_allocator_addresses_in_use_total / on(pool) metallb_allocator_addresses_total ) * 100 > {{ .percent }}
       {{- with .labels }}
       labels:
@@ -69,8 +69,8 @@ spec:
     {{- if .Values.prometheus.prometheusRule.bgpSessionDown.enabled }}
     - alert: MetalLBBGPSessionDown
       annotations:
-        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
-          }} has BGP session {{ $labels.peer }} down for > 1 minute'`}}
+        summary:  {{`'BGP session down on {{ $labels.pod }}'`}}
+        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod }} has BGP session {{ $labels.peer }} down for > 1 minute'`}}
       expr: metallb_bgp_session_up{job=~"{{ template "metallb.fullname" . }}.*"} == 0
       for: 1m
       {{- with .Values.prometheus.prometheusRule.bgpSessionDown.labels }}
diff --git a/charts/metallb/values.yaml b/charts/metallb/values.yaml
index bcb5d88a..50d26bca 100644
--- a/charts/metallb/values.yaml
+++ b/charts/metallb/values.yaml
@@ -165,7 +165,7 @@ prometheus:
     addressPoolExhausted:
       enabled: true
       labels:
-        severity: alert
+        severity: critical
 
     addressPoolUsage:
       enabled: true
@@ -178,13 +178,13 @@ prometheus:
             severity: warning
         - percent: 95
           labels:
-            severity: alert
+            severity: critical
 
     # MetalLBBGPSessionDown
     bgpSessionDown:
       enabled: true
       labels:
-        severity: alert
+        severity: critical
 
     extraAlerts: []
 
diff --git a/config/crd/bases/metallb.io_bgppeers.yaml b/config/crd/bases/metallb.io_bgppeers.yaml
index a913afbf..6db20d5c 100644
--- a/config/crd/bases/metallb.io_bgppeers.yaml
+++ b/config/crd/bases/metallb.io_bgppeers.yaml
@@ -196,6 +196,17 @@ spec:
                 description: To set if we want to disable MP BGP that will separate
                   IPv4 and IPv6 route exchanges into distinct BGP sessions.
                 type: boolean
+              dynamicASN:
+                description: |-
+                  DynamicASN detects the AS number to use for the remote end of the session
+                  without explicitly setting it via the ASN field. Limited to:
+                  internal - if the neighbor's ASN is different than MyASN connection is denied.
+                  external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                enum:
+                - internal
+                - external
+                type: string
               ebgpMultiHop:
                 description: To set if the BGPPeer is multi-hops away. Needed for
                   FRR mode only.
@@ -298,7 +309,9 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               peerASN:
-                description: AS number to expect from the remote end of the session.
+                description: |-
+                  AS number to expect from the remote end of the session.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
                 format: int32
                 maximum: 4294967295
                 minimum: 0
@@ -325,7 +338,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerASN
             - peerAddress
             type: object
           status:
diff --git a/config/manifests/metallb-frr-k8s-prometheus.yaml b/config/manifests/metallb-frr-k8s-prometheus.yaml
index 32559818..6e293483 100644
--- a/config/manifests/metallb-frr-k8s-prometheus.yaml
+++ b/config/manifests/metallb-frr-k8s-prometheus.yaml
@@ -554,6 +554,17 @@ spec:
                 description: To set if we want to disable MP BGP that will separate
                   IPv4 and IPv6 route exchanges into distinct BGP sessions.
                 type: boolean
+              dynamicASN:
+                description: |-
+                  DynamicASN detects the AS number to use for the remote end of the session
+                  without explicitly setting it via the ASN field. Limited to:
+                  internal - if the neighbor's ASN is different than MyASN connection is denied.
+                  external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                enum:
+                - internal
+                - external
+                type: string
               ebgpMultiHop:
                 description: To set if the BGPPeer is multi-hops away. Needed for
                   FRR mode only.
@@ -656,7 +667,9 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               peerASN:
-                description: AS number to expect from the remote end of the session.
+                description: |-
+                  AS number to expect from the remote end of the session.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
                 format: int32
                 maximum: 4294967295
                 minimum: 0
@@ -683,7 +696,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerASN
             - peerAddress
             type: object
           status:
diff --git a/config/manifests/metallb-frr-k8s.yaml b/config/manifests/metallb-frr-k8s.yaml
index f30a7d80..0c278d1b 100644
--- a/config/manifests/metallb-frr-k8s.yaml
+++ b/config/manifests/metallb-frr-k8s.yaml
@@ -554,6 +554,17 @@ spec:
                 description: To set if we want to disable MP BGP that will separate
                   IPv4 and IPv6 route exchanges into distinct BGP sessions.
                 type: boolean
+              dynamicASN:
+                description: |-
+                  DynamicASN detects the AS number to use for the remote end of the session
+                  without explicitly setting it via the ASN field. Limited to:
+                  internal - if the neighbor's ASN is different than MyASN connection is denied.
+                  external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                enum:
+                - internal
+                - external
+                type: string
               ebgpMultiHop:
                 description: To set if the BGPPeer is multi-hops away. Needed for
                   FRR mode only.
@@ -656,7 +667,9 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               peerASN:
-                description: AS number to expect from the remote end of the session.
+                description: |-
+                  AS number to expect from the remote end of the session.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
                 format: int32
                 maximum: 4294967295
                 minimum: 0
@@ -683,7 +696,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerASN
             - peerAddress
             type: object
           status:
diff --git a/config/manifests/metallb-frr-prometheus.yaml b/config/manifests/metallb-frr-prometheus.yaml
index 882a58e5..0fa3b966 100644
--- a/config/manifests/metallb-frr-prometheus.yaml
+++ b/config/manifests/metallb-frr-prometheus.yaml
@@ -554,6 +554,17 @@ spec:
                 description: To set if we want to disable MP BGP that will separate
                   IPv4 and IPv6 route exchanges into distinct BGP sessions.
                 type: boolean
+              dynamicASN:
+                description: |-
+                  DynamicASN detects the AS number to use for the remote end of the session
+                  without explicitly setting it via the ASN field. Limited to:
+                  internal - if the neighbor's ASN is different than MyASN connection is denied.
+                  external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                enum:
+                - internal
+                - external
+                type: string
               ebgpMultiHop:
                 description: To set if the BGPPeer is multi-hops away. Needed for
                   FRR mode only.
@@ -656,7 +667,9 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               peerASN:
-                description: AS number to expect from the remote end of the session.
+                description: |-
+                  AS number to expect from the remote end of the session.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
                 format: int32
                 maximum: 4294967295
                 minimum: 0
@@ -683,7 +696,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerASN
             - peerAddress
             type: object
           status:
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index e88a0705..78019979 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -554,6 +554,17 @@ spec:
                 description: To set if we want to disable MP BGP that will separate
                   IPv4 and IPv6 route exchanges into distinct BGP sessions.
                 type: boolean
+              dynamicASN:
+                description: |-
+                  DynamicASN detects the AS number to use for the remote end of the session
+                  without explicitly setting it via the ASN field. Limited to:
+                  internal - if the neighbor's ASN is different than MyASN connection is denied.
+                  external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                enum:
+                - internal
+                - external
+                type: string
               ebgpMultiHop:
                 description: To set if the BGPPeer is multi-hops away. Needed for
                   FRR mode only.
@@ -656,7 +667,9 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               peerASN:
-                description: AS number to expect from the remote end of the session.
+                description: |-
+                  AS number to expect from the remote end of the session.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
                 format: int32
                 maximum: 4294967295
                 minimum: 0
@@ -683,7 +696,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerASN
             - peerAddress
             type: object
           status:
diff --git a/config/manifests/metallb-native-prometheus.yaml b/config/manifests/metallb-native-prometheus.yaml
index 33204fe5..db2a250f 100644
--- a/config/manifests/metallb-native-prometheus.yaml
+++ b/config/manifests/metallb-native-prometheus.yaml
@@ -554,6 +554,17 @@ spec:
                 description: To set if we want to disable MP BGP that will separate
                   IPv4 and IPv6 route exchanges into distinct BGP sessions.
                 type: boolean
+              dynamicASN:
+                description: |-
+                  DynamicASN detects the AS number to use for the remote end of the session
+                  without explicitly setting it via the ASN field. Limited to:
+                  internal - if the neighbor's ASN is different than MyASN connection is denied.
+                  external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                enum:
+                - internal
+                - external
+                type: string
               ebgpMultiHop:
                 description: To set if the BGPPeer is multi-hops away. Needed for
                   FRR mode only.
@@ -656,7 +667,9 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               peerASN:
-                description: AS number to expect from the remote end of the session.
+                description: |-
+                  AS number to expect from the remote end of the session.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
                 format: int32
                 maximum: 4294967295
                 minimum: 0
@@ -683,7 +696,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerASN
             - peerAddress
             type: object
           status:
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index 23904772..060e108f 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -554,6 +554,17 @@ spec:
                 description: To set if we want to disable MP BGP that will separate
                   IPv4 and IPv6 route exchanges into distinct BGP sessions.
                 type: boolean
+              dynamicASN:
+                description: |-
+                  DynamicASN detects the AS number to use for the remote end of the session
+                  without explicitly setting it via the ASN field. Limited to:
+                  internal - if the neighbor's ASN is different than MyASN connection is denied.
+                  external - if the neighbor's ASN is the same as MyASN the connection is denied.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
+                enum:
+                - internal
+                - external
+                type: string
               ebgpMultiHop:
                 description: To set if the BGPPeer is multi-hops away. Needed for
                   FRR mode only.
@@ -656,7 +667,9 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               peerASN:
-                description: AS number to expect from the remote end of the session.
+                description: |-
+                  AS number to expect from the remote end of the session.
+                  ASN and DynamicASN are mutually exclusive and one of them must be specified.
                 format: int32
                 maximum: 4294967295
                 minimum: 0
@@ -683,7 +696,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerASN
             - peerAddress
             type: object
           status:
diff --git a/design/unnumbered-bgp.md b/design/unnumbered-bgp.md
new file mode 100644
index 00000000..9b7678c9
--- /dev/null
+++ b/design/unnumbered-bgp.md
@@ -0,0 +1,531 @@
+# MetalLB support for BGP Unnumbered
+
+## Summary
+
+BGP Unnumbered allows routers to peer with each other when direct connected
+without the need for IPs (the BGP configuration uses interface names). Once
+peering takes places, exchange IPv4 or IPv6 prefixes can take place. That
+feature can be used by MetalLB to simplify configuration both on the network
+fabric and on the cluster side.
+
+## Motivation
+
+Using BGP unnumbered peering, which dynamically discovers IPV6 neighbors,
+reduces the burden of the network administrator to configue all
+interfaces on the network fabric or on the cluster nodes to have IPv4
+addressing just for the BGP peering. By using BGP unnumbered, cluster administrator
+avoids to specify the address of each neihbor. Unnumbered BGP utilizes IPv6
+link local address to automatically decide which peer to connect to.
+
+## Limitation/Requirements
+
+- To use unnumbered BGP feature, there must be a link (point-to-point, directly
+  connected) between the interfaces two BGP speakers are using
+- IPv6 must be enabled on that interfaces, and with LLA (Link Local IPv6 Address) enabled.
+  Note: the use of IPv6 LLA does not require a full IPv6 deployment
+- IPv6 RAs (Router Advertisments) must be enabled (it should be safe when p2p and no IP address on the interface).
+
+## Goals
+
+- Make the BGP peering from Server to the ToR (TopOfRack) easier to configure
+  and have the same configuration for all servers (assuming the interfaces' names
+  are the same across the cluster nodes)
+- Remove the task of network admins to setup up addressing on those interfaces
+  if it is needed only for BGP peering
+
+## Non Goals
+
+- Make it work over no point-to-point connection
+- Define/Discuss dual ToR architecture, on top of bond interfaces or other HA strategies
+- [Dynamic BGP Peering](https://ipwithease.com/dynamic-bgp-peering/) is a
+  feature different than unnumbered BGP. Both feature reduce the configuration
+  needed for the BGP peering. The main difference is that Dynamic BGP peering
+  still needs IP addresses and does not require point-to-point links.
+
+## User Stories
+
+### Story 1
+
+As cluster administrator, I want to configure metallb without configuring IP addresses.
+
+### Story 2
+
+As network administrator, I want to avoid configuring IP addresses for
+point-to-point connection just for the BPG peering reason.
+
+### Story 3
+
+As network administrator, I want to reduce the size of the configuration in the ToR router.
+
+## The API Changes
+
+A new field will be introduced, `Interface string` which if defined then Unnumber
+BGP takes place. In that case, the address must NOT have value. The API doc will
+look like:
+
+```
+Interface, if defined, instructs Metallb to setup Unnumber BGP peering on the interface,
+which means that the peering configuration only includes the interface and does
+not include IP address. Address and Interface are mutually exclusive and one of
+them must be specified.
+```
+
+#### FRR-K8S
+
+```go
+type Neighbor struct {
+  Address string `json:"address"`
+  Interface string `json:"interface"`
+}
+```
+
+##### MetalLB
+
+```go
+type BGPPeerSpec struct {
+  Address string `json:"peerAddress"`
+  Interface string `json:"interface"`
+}
+```
+
+Example CR
+
+```yaml
+apiVersion: metallb.io/v1beta2
+kind: BGPPeer
+metadata:
+  name: peer-unnumber
+  namespace: metallb-system
+spec:
+  myASN: 64512
+  ASN: 645000
+  interface: net0
+```
+
+#### Rejected Alternative - Re-Use Address field
+
+We can re-use `Address string` to either be IP or interface, if not valid IP we
+indicate Unnumbered BGP peering. This looks the minimal API, but it will not
+have clean error path. E.g. if the user input is not a valid IP, the
+implementation will shadow all the existing errors and will fail in the
+runtime.
+
+#### Rejected Alternative - Add new Unnumber bool field
+
+We can introduce a new field `Unnumber bool`, which if true the we indicate
+Unnumber BGP Peering. If `true` the Address field holds the interface value.
+Bool option makes the configuration error-free, because the user declares
+explicitly that they want Unnumber and there is no "guessing" what metallb
+does. Unnumber case is disconnected from LLA support case.
+
+### Design details
+
+To make unnumber BGP work, we need to modify the neighborsession
+as defined in the following template [neighborsession.tmpl](internal/bgp/frr/templates/neighborsession.tmpl)
+and make the following changes
+
+1. Every time there is FRR directive that uses the address, to use the
+   interface name
+
+```
+before:  neighbor 192.168.10.10 timers 180 540
+after:  neighbor net0 timers 180 540
+```
+
+2. The `remote-as` directive must be replaced
+
+```
+before: neighbor 192.168.10.10 remote-as 65004
+after: neighbor net0 interface remote-as 65004
+```
+
+3. There is currently a single directive that does not accept interface.
+   Therefore it must be always excluded.
+
+```
+neighbor {{.neighbor.Addr}} disable-connected-check
+```
+
+### Topics
+
+#### IPv6 RAs (Route Advertisments) enabled on both ends of the link
+
+According to the design of unnumber BGP peering, we need to enable RAs. By
+observation it seems that FRR/Zebra component is auto-enabling RAs when the BGP
+unnumber session is configured. This is the [commit](https://github.com/FRRouting/frr/commit/4a04e5f7968d46e446bdb530724da18b90b7f5f8).
+Therefore the Metallb implementation does not require that RAs are explicitly
+enabled in the FRR config unless we have other reasons (e.g. have different period).
+
+#### Do I need an IPv{4,6} to enable dataplane traffic
+
+There is no need for the interface to have an IP in order the BGP
+session to be established. This is the core of unnumber BGP. Nevertheless,
+when doing BGP peering between a non-router component and a route, then an IP
+might be needed for the dataplane traffic to work.
+
+**Note:** the rest of the section is outside the scope of Metallb and is related
+to the CNI that cluster is being using. We discuss it as that might be important.
+
+##### Ingress traffic to LB IP and BGP route learning
+
+When external client (outside peer router) talks to an LB address and there is BGP route learning
+(= local k8s nodes adds routing from peer) **there is no need for an IP**
+
+```
+# packet in/out of k8s node
+net0  In  IP 200.100.100.1.40524 > 5.5.5.5.80: Flags [S], seq 1203290349,
+veth3e626b76 Out IP 200.100.100.1.40524 > 10.244.2.4.80: Flags [S], seq 1203290349,
+veth3e626b76 In  IP 10.244.2.4.80 > 200.100.100.1.40524: Flags [S.], seq 3181148864, ack 1203290350,
+net0  Out IP 5.5.5.5.80 > 200.100.100.1.40524: Flags [S.], seq 3181148864, ack 1203290350,
+
+# k8s node
+kind-worker:/# ip --br add show
+    lo               UNKNOWN        127.0.0.1/8 ::1/128
+    veth2c551257@if2 UP             10.244.2.1/32 fe80::1498:dbff:fe25:6e1b/64
+    eth0@if325       UP             172.20.20.4/24 2001:172:20:20::4/64 fe80::42:acff:fe14:1404/64
+    eth1@if330       UP             172.18.0.3/16 fc00:f853:ccd:e791::3/64 fe80::42:acff:fe12:3/64
+    net0@if3         UP             fe80::dcad:beff:feff:1161/64
+kind-worker:/# ip route
+    default via 172.20.20.1 dev eth0
+    10.244.0.0/24 via 172.20.20.3 dev eth0
+    10.244.1.0/24 via 172.20.20.2 dev eth0
+    10.244.2.3 dev veth2c551257 scope host
+    172.18.0.0/16 dev eth1 proto kernel scope link src 172.18.0.3
+    172.20.20.0/24 dev eth0 proto kernel scope link src 172.20.20.4
+    172.30.0.0/16 via 172.20.20.6 dev eth0
+    200.100.100.0/24 nhid 68 via inet6 fe80::2c5f:eff:fec4:cf7b dev net0 proto bgp metric 20
+kind-worker:/# ip route get 200.100.100.1
+    200.100.100.1 via inet6 fe80::2c5f:eff:fec4:cf7b dev net0 src 172.20.20.4 uid 0
+        cache
+```
+That works as long as the NAT/Masq rule properly sets back the source IP to be the LB
+which depends at the end of the CNI/use or not kube-proxy.
+
+##### Ingress traffic to LB IP and static routes
+
+This does NOT work without IP on the interface. Even if route is configured
+with the interface `ip route add 200.100.100.0/24 dev net0` the traffic breaks
+due to ARP.
+
+```
+net0  In  IP 200.100.100.1.43198 > 5.5.5.5.80: Flags [S], seq 3169002768,
+veth3e626b76 Out IP 200.100.100.1.43198 > 10.244.2.4.80: Flags [S], seq 3169002768,
+veth3e626b76 In  IP 10.244.2.4.80 > 200.100.100.1.43198: Flags [S.], seq 490740861, ack 3169002769,
+net0  Out ARP, Request who-has 200.100.100.1 tell 172.20.20.4, length 28
+net0  Out ARP, Request who-has 200.100.100.1 tell 172.20.20.4, length 28
+```
+
+[RFC 8950](https://datatracker.ietf.org/doc/html/rfc8950) explains why we do not need ARP in previous case.
+
+##### Egress traffic to LB IP and BGP route learning
+
+When pod in a node that has the routing entry due BGP peering
+```
+kind-worker:/# ip route
+    200.100.100.0/24 nhid 68 via inet6 fe80::2c5f:eff:fec4:cf7b dev net0 proto bgp metric 20
+kind-worker:/# ip route get 200.100.100.1
+    200.100.100.1 via inet6 fe80::2c5f:eff:fec4:cf7b dev net0 src 172.20.20.4 uid 0
+kind-worker:/# ip a s eth0
+324: eth0@if325: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
+    link/ether 02:42:ac:14:14:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
+    inet 172.20.20.4/24 brd 172.20.20.255 scope global eth0
+       valid_lft forever preferred_lft forever
+```
+initiates a connection to the outside, the source IP of the packet is one of the other interfaces.
+What is the selection algorithm is unclear and kernel documentation could
+answer. The important information is that if IP address is added in the
+loopback interface, then that IP address is being used. There is not need for
+an IP in the peer interface.
+
+```
+kind-worker:/# ip a s lo
+1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
+    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
+    inet 127.0.0.1/8 scope host lo
+       valid_lft forever preferred_lft forever
+    inet 12.12.12.12/32 scope global lo
+       valid_lft forever preferred_lft forever
+    inet6 ::1/128 scope host
+       valid_lft forever preferred_lft forever
+kind-worker:/# ip route get 200.100.100.1
+200.100.100.1 via inet6 fe80::cc61:c8ff:fefc:2dce dev net0 src 12.12.12.12 uid 0
+    cache
+```
+So the answer is IP address is needed, but configuring IP address in the loopback is enough
+to control it.
+
+##### Egress traffic to LB IP and static routes
+
+The interface needs IP address to setup source IP address and routing.
+
+## Test Plan
+
+### E2E test for {i,E}Unnumbered on primary interface
+
+```mermaid
+graph TD
+    subgraph Rack0
+        direction LR
+        subgraph ToR0
+            direction TB
+            A1[eth0] -.- A{Routing}
+            A -.- A3[eth00]
+            A -.- A4[eth01]
+
+            BGP(BGP)
+
+        end
+
+        %% Top Right Subgraph
+        subgraph Node1
+            direction TB
+            B1 --- B2(cni)
+            B2 --- B3
+            subgraph Pod1[pod]
+              B3[eth0]
+            end
+            BGP-N1(BGP process)
+        end
+
+        %% Bottom Right Subgraph
+        subgraph Node0
+            direction TB
+            C1 --- C2(cni)
+            C2 --- C3
+            subgraph Pod2[pod]
+              C3[eth0]
+            end
+            BGP-N0(BGP process)
+        end
+
+        %% Connections between subgraphs
+        A3 -- point-to-point ------ B1[eth0]
+        A4 -- point-to-point ------ C1[eth0]
+    end
+
+    %% Define styles for subgraphs and nodes
+    classDef node fill:#f0f0f0,stroke:#000000,stroke-width:1px;
+    class Node0 node;
+    class Node1 node;
+```
+
+
+
+There is no straightforward way to test unnumbered in the primary node
+interface. The testing setup (kind creation) creates bridge and connect
+containers. Ideally we need to setup a kind cluster, and replace the bridge
+with a container that runs FRR on each other side of the veth, and at the same
+time to provide switch/upstream. Until we find a way to automate that infra setup
+we will test that scenario manually.
+
+### E2E test for Unnumbered on secondary interface
+
+```mermaid
+graph TD
+    subgraph Rack0
+        direction LR
+        style Rack0 fill:#ffffff,stroke:#000000,stroke-width:2px
+
+        subgraph ToR0
+            style ToR0 fill:#ffffff,stroke:#000000,stroke-width:2px
+            direction TB
+            subgraph Default
+              direction LR
+              A1[net0] -.- A{Routing}
+              A -.- A3[eth00]
+              A -.- A4[eth01]
+            end
+            subgraph RED VRF*
+              direction LR
+              AA1[net00] -.- AA{Routing}
+              AA -.- AA3[eth00.vlanRED]
+              AA -.- AA4[eth01.vlanRED]
+              BGP-T0(BGP configured on RED)
+            end
+
+        end
+
+        subgraph Node0
+            direction TB
+            B1 --- B2(cni)
+            B2 --- B3
+            BB1
+            subgraph Pod1[pod]
+              B3[eth0]
+            end
+            BGP-N0(BGP process)
+        end
+
+        subgraph Node1
+            direction TB
+            C1 --- C2(cni)
+            C2 --- C3
+            CC1
+            subgraph Pod2[pod]
+              C3[eth0]
+            end
+            BGP-N1(BGP process)
+        end
+
+        %% Connections between subgraphs
+        A3 ------- B1[eth0]
+        AA3 -- BGP Peering ----- BB1[net0]
+        A4 ------- C1[eth0]
+        AA4 -- BGP Peering ----- CC1[net0]
+    end
+
+    %% Define styles for subgraphs and nodes
+    classDef node fill:#f0f0f0,stroke:#000000,stroke-width:1px;
+    class Node0 node;
+    class Node1 node;
+```
+
+We should modify the testing infra to include point-to-point links which means
+connect containers using VETH, and not through a bridge. We need NET-ADMIN cap
+(or sudo) when creating the VETH.
+
+#### Option 1 - have a separate test
+
+We could have an isolated test file that creates/deletes peer per that specific tests.
+In that option we do not to take resources from runners outside the runtime of
+the test. This new peering setup needs different configuration (not IPs but interface
+names) and therefore avoid extensive(?) refactoring of the help functions.
+
+#### Option 2 - extend the FRR Containers and have all test run on top
+
+Benefit of that would be that we will cover all the feature/scenarios (e.g. uncordon node).
+Reason not to do that would be add extra time, create many skip rules (e.g. if Multihop skip that Peer),
+extensive(?) refactor of the test functions.
+
+### An example ToR configuration when based on FRR
+
+This how the testing is setup, and how we emulate a ToR Switch/Router
+
+```
+frr defaults datacenter
+hostname tor
+no ipv6 forwarding
+!
+interface eth00
+    ipv6 nd ra-interval 10
+    no ipv6 nd suppress-ra
+exit
+!
+interface eth01
+    ipv6 nd ra-interval 10
+    no ipv6 nd suppress-ra
+exit
+!
+interface lo
+    ip address 200.100.100.1/24 //for testing
+exit
+!
+router bgp 65001
+    bgp router-id 11.11.11.254
+    neighbor MTLB peer-group
+    neighbor MTLB passive
+    neighbor MTLB remote-as external
+    neighbor MTLB description LEAF-MTLB
+    neighbor eth00 interface peer-group MTLB
+    neighbor eth01 interface peer-group MTLB
+    !
+    address-family ipv4 unicast
+        redistribute connected
+        neighbor MTLB activate
+    exit-address-family
+    !
+    address-family ipv6 unicast
+        redistribute connected
+        neighbor MTLB activate
+    exit-address-family
+    exit
+```
+
+### How a peering will look like
+
+```
+> docker exec -it unnumbered-p2p-peer vtysh -c 'show bgp summary'
+
+IPv4 Unicast Summary (VRF default):
+BGP router identifier 11.11.11.254, local AS number 65004 vrf-id 0
+BGP table version 4
+RIB entries 3, using 288 bytes of memory
+Peers 3, using 39 KiB of memory
+Peer groups 1, using 64 bytes of memory
+
+Neighbor                  V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
+kind-control-plane(eth00) 4      65000       510       510        4    0    0 00:24:43            1        2 k8s-node
+kind-worker(eth01)        4      65000       510       510        4    0    0 00:24:43            1        2 k8s-node
+kind-worker2(eth02)       4      65000       510       510        4    0    0 00:24:43            1        2 k8s-node
+
+Total number of neighbors 3
+
+IPv6 Unicast Summary (VRF default):
+BGP router identifier 11.11.11.254, local AS number 65004 vrf-id 0
+BGP table version 3
+RIB entries 1, using 96 bytes of memory
+Peers 3, using 39 KiB of memory
+Peer groups 1, using 64 bytes of memory
+
+Neighbor                  V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
+kind-control-plane(eth00) 4      65000       510       510        3    0    0 00:24:43            1        1 k8s-node
+kind-worker(eth01)        4      65000       510       510        3    0    0 00:24:43            1        1 k8s-node
+kind-worker2(eth02)       4      65000       510       510        3    0    0 00:24:43            1        1 k8s-node
+
+
+> k -n metallb-system exec -it -c frr frr-k8s-daemon-kzpz2 -- vtysh -c 'show bgp summary'
+
+IPv4 Unicast Summary (VRF default):
+BGP router identifier 172.19.0.3, local AS number 65000 vrf-id 0
+BGP table version 2
+RIB entries 3, using 288 bytes of memory
+Peers 1, using 13 KiB of memory
+Peer groups 1, using 64 bytes of memory
+
+Neighbor                  V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
+unnumbered-p2p-peer(net0) 4      65004       512       512        2    0    0 00:25:15            1        2 TOR
+
+Total number of neighbors 1
+
+IPv6 Unicast Summary (VRF default):
+BGP router identifier 172.19.0.3, local AS number 65000 vrf-id 0
+BGP table version 1
+RIB entries 1, using 96 bytes of memory
+Peers 1, using 13 KiB of memory
+Peer groups 1, using 64 bytes of memory
+
+Neighbor                  V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
+unnumbered-p2p-peer(net0) 4      65004       512       512        1    0    0 00:25:15            0        1 TOR
+
+Total number of neighbors 1
+
+> docker exec -it unnumbered-p2p-peer vtysh -c 'show bgp ipv4'
+BGP table version is 3, local router ID is 11.11.11.254, vrf id 0
+Default local pref 100, local AS 65004
+Status codes:  s suppressed, d damped, h history, * valid, > best, = multipath,
+               i internal, r RIB-failure, S Stale, R Removed
+Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
+Origin codes:  i - IGP, e - EGP, ? - incomplete
+RPKI validation codes: V valid, I invalid, N Not found
+
+    Network          Next Hop            Metric LocPrf Weight Path
+ *= 5.5.5.5/32       eth02                    0             0 65000 i
+ *=                  eth01                    0             0 65000 i
+ *>                  eth00                    0             0 65000 i
+ *> 200.100.100.0/24 0.0.0.0(unnumbered-p2p-peer)
+                                             0         32768 ?
+
+Displayed  2 routes and 4 total paths
+> docker exec -it unnumbered-p2p-peer ip route
+5.5.5.5 nhid 21 proto bgp metric 20
+        nexthop via inet6 fe80::dcad:beff:feff:1160 dev eth00 weight 1
+        nexthop via inet6 fe80::dcad:beff:feff:1161 dev eth01 weight 1
+        nexthop via inet6 fe80::dcad:beff:feff:1162 dev eth02 weight 1
+```
+
+## Links
+
+- [BGP in the Data Center](https://www.oreilly.com/library/view/bgp-in-the/9781491983416/)
+- [blog](https://blog.ipspace.net/2022/11/bgp-unnumbered-duct-tape/)
+- [srlinux](https://documentation.nokia.com/srlinux/22-6/SR_Linux_Book_Files/Configuration_Basics_Guide/configb-bgp.html)
+- [cilium e2e](https://docs.cilium.io/en/stable/contributing/development/bgp_cplane/#bgp-cplane-contributin)
diff --git a/internal/bgp/bgp.go b/internal/bgp/bgp.go
index 54ad6a3e..78fb2d9b 100644
--- a/internal/bgp/bgp.go
+++ b/internal/bgp/bgp.go
@@ -68,6 +68,7 @@ type SessionParameters struct {
 	MyASN           uint32
 	RouterID        net.IP
 	PeerASN         uint32
+	DynamicASN      string
 	HoldTime        *time.Duration
 	KeepAliveTime   *time.Duration
 	ConnectTime     *time.Duration
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index 57e8043e..5f328ec2 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -66,7 +66,7 @@ type BFDProfile struct {
 type neighborConfig struct {
 	IPFamily            ipfamily.Family
 	Name                string
-	ASN                 uint32
+	ASN                 string
 	Addr                string
 	SrcAddr             string
 	Port                uint16
@@ -116,8 +116,17 @@ func RouterName(srcAddr string, myASN uint32, vrfName string) string {
 
 // neighborName() defines the format of key of the 'Neighbors' map in the
 // routerConfig struct.
-func NeighborName(peerAddr string, ASN uint32, vrfName string) string {
-	return fmt.Sprintf("%d@%s@%s", ASN, peerAddr, vrfName)
+func NeighborName(peerAddr string, ASN uint32, dynamicASN string, vrfName string) string {
+	asn := asnFor(ASN, dynamicASN)
+	return fmt.Sprintf("%s@%s@%s", asn, peerAddr, vrfName)
+}
+
+func asnFor(ASN uint32, dynamicASN string) string {
+	asn := strconv.FormatUint(uint64(ASN), 10)
+	if dynamicASN != "" {
+		asn = dynamicASN
+	}
+	return asn
 }
 
 // templateConfig uses the template library to template
@@ -153,11 +162,33 @@ func templateConfig(data interface{}) (string, error) {
 			"allowedPrefixList": func(neighbor *neighborConfig) string {
 				return fmt.Sprintf("%s-pl-%s", neighbor.ID(), neighbor.IPFamily)
 			},
-			"mustDisableConnectedCheck": func(ipFamily ipfamily.Family, myASN, asn uint32, eBGPMultiHop bool) bool {
-				// return true only for IPv6 eBGP sessions
-				if ipFamily == "ipv6" && myASN != asn && !eBGPMultiHop {
+			"mustDisableConnectedCheck": func(ipFamily ipfamily.Family, myASN uint32, asn string, eBGPMultiHop bool) bool {
+				// return true only for non-multihop IPv6 eBGP sessions
+
+				if ipFamily != ipfamily.IPv6 {
+					return false
+				}
+
+				if eBGPMultiHop {
+					return false
+				}
+
+				// internal means we expect the session to be iBGP
+				if asn == "internal" {
+					return false
+				}
+
+				// external means we expect the session to be eBGP
+				if asn == "external" {
 					return true
 				}
+
+				// the peer's asn is not dynamic (it is a number),
+				// we check if it is different than ours for eBGP
+				if strconv.FormatUint(uint64(myASN), 10) != asn {
+					return true
+				}
+
 				return false
 			},
 			"dict": func(values ...interface{}) (map[string]interface{}, error) {
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index 11e196f1..492ece0a 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -252,7 +252,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 			routers[routerName] = rout
 		}
 
-		neighborName := NeighborName(s.PeerAddress, s.PeerASN, s.VRFName)
+		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 {
@@ -285,7 +285,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 			neighbor = &neighborConfig{
 				Name:            neighborName,
 				IPFamily:        family,
-				ASN:             s.PeerASN,
+				ASN:             asnFor(s.PeerASN, s.DynamicASN),
 				Addr:            host,
 				Port:            uint16(portUint),
 				HoldTime:        holdTime,
diff --git a/internal/bgp/frrk8s/frrk8s.go b/internal/bgp/frrk8s/frrk8s.go
index 29e8383c..68f8616f 100644
--- a/internal/bgp/frrk8s/frrk8s.go
+++ b/internal/bgp/frrk8s/frrk8s.go
@@ -227,7 +227,7 @@ func (sm *sessionManager) updateConfig() error {
 			routers[routerName] = rout
 		}
 
-		neighborName := frr.NeighborName(s.PeerAddress, s.PeerASN, s.VRFName)
+		neighborName := frr.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 {
@@ -260,6 +260,7 @@ func (sm *sessionManager) updateConfig() error {
 
 			neighbor = frrv1beta1.Neighbor{
 				ASN:                   s.PeerASN,
+				DynamicASN:            frrv1beta1.DynamicASNMode(s.DynamicASN),
 				Address:               host,
 				Port:                  &portUint16,
 				HoldTime:              holdTime,
diff --git a/internal/config/config.go b/internal/config/config.go
index f0be82e2..a7d49bb8 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -96,6 +96,8 @@ type Peer struct {
 	MyASN uint32
 	// AS number to expect from the remote end of the session.
 	ASN uint32
+	// Detect the AS number to use for the remote end of the session.
+	DynamicASN string
 	// Address to dial when establishing the session.
 	Addr net.IP
 	// Source address to use when establishing the session.
@@ -375,8 +377,14 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 	if p.Spec.MyASN == 0 {
 		return nil, errors.New("missing local ASN")
 	}
-	if p.Spec.ASN == 0 {
-		return nil, errors.New("missing peer ASN")
+	if p.Spec.ASN == 0 && p.Spec.DynamicASN == "" {
+		return nil, errors.New("missing peer ASN and dynamicASN")
+	}
+	if p.Spec.ASN != 0 && p.Spec.DynamicASN != "" {
+		return nil, errors.New("both peer ASN and dynamicASN specified")
+	}
+	if p.Spec.DynamicASN != "" && p.Spec.DynamicASN != metallbv1beta2.InternalASNMode && p.Spec.DynamicASN != metallbv1beta2.ExternalASNMode {
+		return nil, fmt.Errorf("invalid dynamicASN %s", p.Spec.DynamicASN)
 	}
 	if p.Spec.ASN == p.Spec.MyASN && p.Spec.EBGPMultiHop {
 		return nil, errors.New("invalid ebgp-multihop parameter set for an ibgp peer")
@@ -445,6 +453,7 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 		Name:                  p.Name,
 		MyASN:                 p.Spec.MyASN,
 		ASN:                   p.Spec.ASN,
+		DynamicASN:            string(p.Spec.DynamicASN),
 		Addr:                  ip,
 		SrcAddr:               src,
 		Port:                  p.Spec.Port,
diff --git a/internal/config/validation.go b/internal/config/validation.go
index 56ed79d6..0b9e6409 100644
--- a/internal/config/validation.go
+++ b/internal/config/validation.go
@@ -48,6 +48,9 @@ func DiscardFRROnly(c ClusterResources) error {
 		if p.Spec.DisableMP {
 			return fmt.Errorf("peer %s has disable MP flag set on native bgp mode", p.Spec.Address)
 		}
+		if p.Spec.DynamicASN != "" {
+			return fmt.Errorf("peer %s has dynamicASN 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 da0cb28b..20cc733b 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -238,6 +238,7 @@ func (c *bgpController) syncPeers(l log.Logger) error {
 				MyASN:           p.cfg.MyASN,
 				RouterID:        routerID,
 				PeerASN:         p.cfg.ASN,
+				DynamicASN:      p.cfg.DynamicASN,
 				HoldTime:        p.cfg.HoldTime,
 				KeepAliveTime:   p.cfg.KeepaliveTime,
 				ConnectTime:     p.cfg.ConnectTime,
diff --git a/website/content/apis/_index.md b/website/content/apis/_index.md
index 958daaa9..e9998eb0 100644
--- a/website/content/apis/_index.md
+++ b/website/content/apis/_index.md
@@ -283,7 +283,8 @@ _Appears in:_
 | Field | Description |
 | --- | --- |
 | `myASN` _integer_ | AS number to use for the local end of the session. |
-| `peerASN` _integer_ | AS number to expect from the remote end of the session. |
+| `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. |
 | `sourceAddress` _string_ | Source address to use when establishing the session. |
 | `peerPort` _integer_ | Port to dial when establishing the session. |
@@ -301,3 +302,14 @@ _Appears in:_
 | `disableMP` _boolean_ | To set if we want to disable MP BGP that will separate IPv4 and IPv6 route exchanges into distinct BGP sessions. |
 
 
+#### DynamicASNMode
+
+_Underlying type:_ _string_
+
+
+
+_Appears in:_
+- [BGPPeerSpec](#bgppeerspec)
+
+
+


Test patch:
diff --git a/e2etest/bgptests/bgp.go b/e2etest/bgptests/bgp.go
index ae6370ee..6994754e 100644
--- a/e2etest/bgptests/bgp.go
+++ b/e2etest/bgptests/bgp.go
@@ -1614,6 +1614,33 @@ var _ = ginkgo.Describe("BGP", func() {
 		ginkgo.Entry("IPV4", ipfamily.IPv4, []string{l2tests.IPV4ServiceRange}),
 		ginkgo.Entry("IPV6", ipfamily.IPv6, []string{l2tests.IPV6ServiceRange}),
 	)
+	ginkgo.DescribeTable("FRR establishes connections with dynamic ASN ", func(pairingIPFamily ipfamily.Family) {
+		resources := config.Resources{
+			Peers: metallb.PeersForContainers(FRRContainers, pairingIPFamily, func(p *metallbv1beta2.BGPPeer) {
+				dynamicASN := metallbv1beta2.InternalASNMode
+				if p.Spec.ASN != p.Spec.MyASN {
+					dynamicASN = metallbv1beta2.ExternalASNMode
+				}
+				p.Spec.ASN = 0
+				p.Spec.DynamicASN = dynamicASN
+			}),
+		}
+
+		for _, c := range FRRContainers {
+			err := frrcontainer.PairWithNodes(cs, c, pairingIPFamily)
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		err := ConfigUpdater.Update(resources)
+		Expect(err).NotTo(HaveOccurred())
+
+		for _, c := range FRRContainers {
+			validateFRRPeeredWithAllNodes(cs, c, pairingIPFamily)
+		}
+	},
+		ginkgo.Entry("IPV4", ipfamily.IPv4),
+		ginkgo.Entry("IPV6", ipfamily.IPv6),
+	)
 })
 
 // substringCount creates a Gomega transform function that
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index a999d4f4..0bcdfd4a 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -1357,3 +1357,51 @@ func TestAddToAdvertisements(t *testing.T) {
 		})
 	}
 }
+
+func TestSingleSessionWithInternalASN(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:179",
+			SourceAddress:   net.ParseIP("10.1.1.254"),
+			MyASN:           102,
+			RouterID:        net.ParseIP("10.1.1.254"),
+			DynamicASN:      "internal",
+			GracefulRestart: true,
+			SessionName:     "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
+
+func TestSingleSessionWithExternalASN(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:179",
+			SourceAddress:   net.ParseIP("10.1.1.254"),
+			MyASN:           102,
+			RouterID:        net.ParseIP("10.1.1.254"),
+			DynamicASN:      "external",
+			GracefulRestart: true,
+			SessionName:     "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
diff --git a/internal/bgp/frr/testdata/TestSingleSessionWithExternalASN.golden b/internal/bgp/frr/testdata/TestSingleSessionWithExternalASN.golden
new file mode 100644
index 00000000..89623d5b
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestSingleSessionWithExternalASN.golden
@@ -0,0 +1,42 @@
+log file /etc/frr/frr.log 
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+route-map 10.2.2.254-in deny 20
+
+
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 deny any
+ipv6 prefix-list 10.2.2.254-pl-ipv4 seq 2 deny any
+
+route-map 10.2.2.254-out permit 1
+  match ip address prefix-list 10.2.2.254-pl-ipv4
+route-map 10.2.2.254-out permit 2
+  match ipv6 address prefix-list 10.2.2.254-pl-ipv4
+
+router bgp 102
+  no bgp ebgp-requires-policy
+  no bgp network import-check
+  no bgp default ipv4-unicast
+  bgp graceful-restart preserve-fw-state
+
+  bgp router-id 10.1.1.254
+  neighbor 10.2.2.254 remote-as external
+  neighbor 10.2.2.254 port 179
+  
+  neighbor 10.2.2.254 update-source 10.1.1.254
+  neighbor 10.2.2.254 graceful-restart
+
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+
diff --git a/internal/bgp/frr/testdata/TestSingleSessionWithInternalASN.golden b/internal/bgp/frr/testdata/TestSingleSessionWithInternalASN.golden
new file mode 100644
index 00000000..308f89e7
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestSingleSessionWithInternalASN.golden
@@ -0,0 +1,42 @@
+log file /etc/frr/frr.log 
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+route-map 10.2.2.254-in deny 20
+
+
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 deny any
+ipv6 prefix-list 10.2.2.254-pl-ipv4 seq 2 deny any
+
+route-map 10.2.2.254-out permit 1
+  match ip address prefix-list 10.2.2.254-pl-ipv4
+route-map 10.2.2.254-out permit 2
+  match ipv6 address prefix-list 10.2.2.254-pl-ipv4
+
+router bgp 102
+  no bgp ebgp-requires-policy
+  no bgp network import-check
+  no bgp default ipv4-unicast
+  bgp graceful-restart preserve-fw-state
+
+  bgp router-id 10.1.1.254
+  neighbor 10.2.2.254 remote-as internal
+  neighbor 10.2.2.254 port 179
+  
+  neighbor 10.2.2.254 update-source 10.1.1.254
+  neighbor 10.2.2.254 graceful-restart
+
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+
diff --git a/internal/bgp/frrk8s/frrk8s_test.go b/internal/bgp/frrk8s/frrk8s_test.go
index 27b99be4..6bd69044 100644
--- a/internal/bgp/frrk8s/frrk8s_test.go
+++ b/internal/bgp/frrk8s/frrk8s_test.go
@@ -901,3 +901,43 @@ func TestSingleSessionWithZeroTimers(t *testing.T) {
 
 	testCheckConfigFile(t)
 }
+
+func TestSingleSessionWithInternalASN(t *testing.T) {
+	sessionManager := newTestSessionManager(t)
+	l := log.NewNopLogger()
+
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress: "10.2.2.254:179",
+			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)
+}
+
+func TestSingleSessionWithExternalASN(t *testing.T) {
+	sessionManager := newTestSessionManager(t)
+	l := log.NewNopLogger()
+
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress: "10.2.2.254:179",
+			MyASN:       100,
+			RouterID:    net.ParseIP("10.1.1.254"),
+			DynamicASN:  "external",
+			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/testdata/TestSingleSessionWithExternalASN.golden b/internal/bgp/frrk8s/testdata/TestSingleSessionWithExternalASN.golden
new file mode 100644
index 00000000..9f8848ef
--- /dev/null
+++ b/internal/bgp/frrk8s/testdata/TestSingleSessionWithExternalASN.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": "external",
+                            "address": "10.2.2.254",
+                            "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/bgp/frrk8s/testdata/TestSingleSessionWithInternalASN.golden b/internal/bgp/frrk8s/testdata/TestSingleSessionWithInternalASN.golden
new file mode 100644
index 00000000..d97f5955
--- /dev/null
+++ b/internal/bgp/frrk8s/testdata/TestSingleSessionWithInternalASN.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",
+                            "address": "10.2.2.254",
+                            "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 c7f3f3c3..d08ba6a2 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -3439,6 +3439,66 @@ func TestParse(t *testing.T) {
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
+		{
+			desc: "peer with dynamic asn",
+			crs: ClusterResources{
+
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "peer1",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:      42,
+							DynamicASN: v1beta2.InternalASNMode,
+							Address:    "1.2.3.4",
+						},
+					},
+				},
+			},
+			want: &Config{
+				Peers: map[string]*Peer{
+					"peer1": {
+						Name:          "peer1",
+						MyASN:         42,
+						DynamicASN:    "internal",
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						EBGPMultiHop:  false,
+					},
+				},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+		{
+			desc: "peer without asn or dynamic asn",
+			crs: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:      42,
+							ASN:        0,
+							DynamicASN: "",
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "peer with both asn and dynamic asn",
+			crs: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:      42,
+							ASN:        42,
+							DynamicASN: v1beta2.InternalASNMode,
+						},
+					},
+				},
+			},
+		},
 	}
 
 	for _, test := range tests {
diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go
index 0502700e..f6b21d43 100644
--- a/internal/config/validation_test.go
+++ b/internal/config/validation_test.go
@@ -96,6 +96,19 @@ func TestValidate(t *testing.T) {
 			},
 			mustFail: true,
 		},
+		{
+			desc: "dynamic ASN",
+			config: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						Spec: v1beta2.BGPPeerSpec{
+							DynamicASN: "internal",
+						},
+					},
+				},
+			},
+			mustFail: true,
+		},
 		{
 			desc: "keepalive time",
 			config: ClusterResources{