Repository: metallb/metallb
Pull request #2623 (2024-12-03 02:04:21)

Chore(deps): bump sigs.k8s.io/controller-runtime from 0.19.0 to 0.19.3
Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.19.0 to 0.19.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/kubernetes-sigs/controller-runtime/releases">sigs.k8s.io/controller-runtime's releases</a>.</em></p>
<blockquote>
<h2>v0.19.3</h2>
<h2>What's Changed</h2>
<ul>
<li>🐛 Refactor certificate watcher to use polling, instead of fsnotify by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/3023">kubernetes-sigs/controller-runtime#3023</a></li>
<li>🐛 Use leader elector with client timeout by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/3031">kubernetes-sigs/controller-runtime#3031</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.2...v0.19.3">https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.2...v0.19.3</a></p>
<h2>v0.19.2</h2>
<h2>What's Changed</h2>
<ul>
<li>✨ Add EnableWatchBookmarks option to cache informers by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/3018">kubernetes-sigs/controller-runtime#3018</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.1...v0.19.2">https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.1...v0.19.2</a></p>
<h2>v0.19.1</h2>
<h2>What's Changed</h2>
<ul>
<li>[release-0.19] 🌱 Verify PR titles with shell script by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/2947">kubernetes-sigs/controller-runtime#2947</a></li>
<li>[release-0.19] 🐛  Fakeclient: preserve TypeMeta for PartialObjectMeta resources by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/2951">kubernetes-sigs/controller-runtime#2951</a></li>
<li>[release-0.19] 🌱 pr-verify: use env var for passing the PR title by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/2962">kubernetes-sigs/controller-runtime#2962</a></li>
<li>[release-0.19] 🌱 pr-verify: use env var for passing the PR title by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/2965">kubernetes-sigs/controller-runtime#2965</a></li>
<li>[release-0.19] 🌱 Fix PR verify action by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/2974">kubernetes-sigs/controller-runtime#2974</a></li>
<li>[release-0.19] :bug: Fakeclient: Fix TOCTOU races by <a href="https://github.com/k8s-infra-cherrypick-robot"><code>@​k8s-infra-cherrypick-robot</code></a> in <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/2992">kubernetes-sigs/controller-runtime#2992</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.0...v0.19.1">https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.0...v0.19.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/3e6681045387530b740e4b35a96a6b7fe590718a"><code>3e66810</code></a> Merge pull request <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/issues/3037">#3037</a> from kubernetes-sigs/backport019-watch</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/2085acc2a1dcf54243922ea26066ee07cd64d9ab"><code>2085acc</code></a> add watch deprecated to certwatcher</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/08235301ea88c62760e9263b732e1e9a223dd5b0"><code>0823530</code></a> Merge pull request <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/issues/3031">#3031</a> from k8s-infra-cherrypick-robot/cherry-pick-3028-to-...</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/e727239ea3dea3c649e405af40af172844e7bd4e"><code>e727239</code></a> [release-0.19] 🐛 Refactor certificate watcher to use polling, instead of fsno...</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/2a0ce596d97af3673d20ff8930a60dba89a20ca0"><code>2a0ce59</code></a> :seedling: Make using leader elector with client timeout non-breaking</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/4bc3811a419e7564daea4baaabc1bae9d4c011df"><code>4bc3811</code></a> :bug: Fix RenewDeadline typo in leader election</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/01707429e75ad5588b874cf33cb888f0476a6659"><code>0170742</code></a> warning: Use leader elector with client timeout</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/bfd1cf925238323fd53b372780a3d0a1a750a85c"><code>bfd1cf9</code></a> [release-0.19] ✨ Add EnableWatchBookmarks option to cache informers (<a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/issues/3018">#3018</a>)</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/013f46fbca88cf495a783cdde6dda9ab5c3cd0b9"><code>013f46f</code></a> Merge pull request <a href="https://redirect.github.com/kubernetes-sigs/controller-runtime/issues/2992">#2992</a> from k8s-infra-cherrypick-robot/cherry-pick-2980-to-...</li>
<li><a href="https://github.com/kubernetes-sigs/controller-runtime/commit/44214256ba36506ed2a0cb0910c687bc172cf78a"><code>4421425</code></a> bug: Fakeclient: Fix TOCTOU races</li>
<li>Additional commits viewable in <a href="https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.0...v0.19.3">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=sigs.k8s.io/controller-runtime&package-manager=go_modules&previous-version=0.19.0&new-version=0.19.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index af448539..1a39b203 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -228,8 +228,7 @@ jobs:
           HELM_FLAGS=""
           echo '/etc/frr/core-%e.%p.%h.%t' | sudo tee /proc/sys/kernel/core_pattern
           if [ ${{ matrix.deployment }} = "helm" ]; then export SPEAKER_SELECTOR="app.kubernetes.io/component=speaker" && export CONTROLLER_SELECTOR="app.kubernetes.io/component=controller"; fi
-          # TODO restore to NONE when https://github.com/metallb/metallb/issues/2311 is fixed
-          SKIP="L2ServiceStatus"
+          SKIP="none"
           WITH_VRF="--with-vrf"
           FOCUS=""
           if [ "${{ matrix.bgp-type }}" == "native" ]; then SKIP="$SKIP|FRR|FRR-MODE|FRRK8S-MODE|BFD|VRF|DUALSTACK"; WITH_VRF=""; fi
@@ -339,8 +338,7 @@ jobs:
           spec:
             logLevel: debug
           EOF
-          # TOOD remove skipping L2ServiceStatus once https://github.com/metallb/metallb/issues/2311 is fixed
-          sudo -E env "PATH=$PATH" inv e2etest --bgp-mode frr --skip "IPV6|DUALSTACK|metrics|L2-interface selector|FRRK8S-MODE|L2ServiceStatus" -e /tmp/kind_logs
+          sudo -E env "PATH=$PATH" inv e2etest --bgp-mode frr --skip "IPV6|DUALSTACK|metrics|L2-interface selector|FRRK8S-MODE" -e /tmp/kind_logs
 
       - name: Collect Logs
         if: ${{ failure() }}
diff --git a/CODEOWNERS b/CODEOWNERS
index 3550e099..19cc0f53 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -2,7 +2,7 @@
 # https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
 
 # The MetalLB maintainers team
-*   @fedepaol @russellb @gclawes @oribon
+*   @fedepaol @oribon
 
 # Emeritus Maintainers
 #
@@ -13,3 +13,5 @@
 # - daxmc99
 # - johananl
 # - rata
+# - russellb
+# - gclawes 
diff --git a/README.md b/README.md
index 2b3b9a3a..efb1bb28 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ You can report security issues in the github issue tracker. If you
 prefer private disclosure, please email to all of the maintainers:
 
 - fpaoline@redhat.com
-- rbryant@redhat.com
+- obraunsh@redhat.com
 
 We aim for initial response to vulnerability reports within 48
 hours. The timeline for fixes depends on the complexity of the issue.
diff --git a/api/v1beta2/bgppeer_types.go b/api/v1beta2/bgppeer_types.go
index 836d11be..3269439d 100644
--- a/api/v1beta2/bgppeer_types.go
+++ b/api/v1beta2/bgppeer_types.go
@@ -96,10 +96,10 @@ type BGPPeerSpec struct {
 	// +optional
 	BFDProfile string `json:"bfdProfile,omitempty"`
 
-	// EnableGracefulRestart allows BGP peer to continue to forward data packets along
-	// known routes while the routing protocol information is being restored.
-	// This field is immutable because it requires restart of the BGP session
-	// Supported for FRR mode only.
+	// EnableGracefulRestart allows BGP peer to continue to forward data packets
+	// along known routes while the routing protocol information is being
+	// restored. This field is immutable because it requires restart of the BGP
+	// session. Supported for FRR mode only.
 	// +optional
 	// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="EnableGracefulRestart cannot be changed after creation"
 	EnableGracefulRestart bool `json:"enableGracefulRestart,omitempty"`
diff --git a/charts/metallb/charts/crds/templates/crds.yaml b/charts/metallb/charts/crds/templates/crds.yaml
index 998620dc..8f241477 100644
--- a/charts/metallb/charts/crds/templates/crds.yaml
+++ b/charts/metallb/charts/crds/templates/crds.yaml
@@ -544,10 +544,10 @@ spec:
                   type: boolean
                 enableGracefulRestart:
                   description: |-
-                    EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                    known routes while the routing protocol information is being restored.
-                    This field is immutable because it requires restart of the BGP session
-                    Supported for FRR mode only.
+                    EnableGracefulRestart allows BGP peer to continue to forward data packets
+                    along known routes while the routing protocol information is being
+                    restored. This field is immutable because it requires restart of the BGP
+                    session. Supported for FRR mode only.
                   type: boolean
                   x-kubernetes-validations:
                     - message: EnableGracefulRestart cannot be changed after creation
diff --git a/config/crd/bases/metallb.io_bgppeers.yaml b/config/crd/bases/metallb.io_bgppeers.yaml
index 6db20d5c..b01512d1 100644
--- a/config/crd/bases/metallb.io_bgppeers.yaml
+++ b/config/crd/bases/metallb.io_bgppeers.yaml
@@ -213,10 +213,10 @@ spec:
                 type: boolean
               enableGracefulRestart:
                 description: |-
-                  EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                  known routes while the routing protocol information is being restored.
-                  This field is immutable because it requires restart of the BGP session
-                  Supported for FRR mode only.
+                  EnableGracefulRestart allows BGP peer to continue to forward data packets
+                  along known routes while the routing protocol information is being
+                  restored. This field is immutable because it requires restart of the BGP
+                  session. Supported for FRR mode only.
                 type: boolean
                 x-kubernetes-validations:
                 - message: EnableGracefulRestart cannot be changed after creation
diff --git a/config/manifests/metallb-frr-k8s-prometheus.yaml b/config/manifests/metallb-frr-k8s-prometheus.yaml
index 0420225e..ff732b08 100644
--- a/config/manifests/metallb-frr-k8s-prometheus.yaml
+++ b/config/manifests/metallb-frr-k8s-prometheus.yaml
@@ -571,10 +571,10 @@ spec:
                 type: boolean
               enableGracefulRestart:
                 description: |-
-                  EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                  known routes while the routing protocol information is being restored.
-                  This field is immutable because it requires restart of the BGP session
-                  Supported for FRR mode only.
+                  EnableGracefulRestart allows BGP peer to continue to forward data packets
+                  along known routes while the routing protocol information is being
+                  restored. This field is immutable because it requires restart of the BGP
+                  session. Supported for FRR mode only.
                 type: boolean
                 x-kubernetes-validations:
                 - message: EnableGracefulRestart cannot be changed after creation
diff --git a/config/manifests/metallb-frr-k8s.yaml b/config/manifests/metallb-frr-k8s.yaml
index ebe2fed9..75720a34 100644
--- a/config/manifests/metallb-frr-k8s.yaml
+++ b/config/manifests/metallb-frr-k8s.yaml
@@ -571,10 +571,10 @@ spec:
                 type: boolean
               enableGracefulRestart:
                 description: |-
-                  EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                  known routes while the routing protocol information is being restored.
-                  This field is immutable because it requires restart of the BGP session
-                  Supported for FRR mode only.
+                  EnableGracefulRestart allows BGP peer to continue to forward data packets
+                  along known routes while the routing protocol information is being
+                  restored. This field is immutable because it requires restart of the BGP
+                  session. Supported for FRR mode only.
                 type: boolean
                 x-kubernetes-validations:
                 - message: EnableGracefulRestart cannot be changed after creation
diff --git a/config/manifests/metallb-frr-prometheus.yaml b/config/manifests/metallb-frr-prometheus.yaml
index 0fa3b966..36821896 100644
--- a/config/manifests/metallb-frr-prometheus.yaml
+++ b/config/manifests/metallb-frr-prometheus.yaml
@@ -571,10 +571,10 @@ spec:
                 type: boolean
               enableGracefulRestart:
                 description: |-
-                  EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                  known routes while the routing protocol information is being restored.
-                  This field is immutable because it requires restart of the BGP session
-                  Supported for FRR mode only.
+                  EnableGracefulRestart allows BGP peer to continue to forward data packets
+                  along known routes while the routing protocol information is being
+                  restored. This field is immutable because it requires restart of the BGP
+                  session. Supported for FRR mode only.
                 type: boolean
                 x-kubernetes-validations:
                 - message: EnableGracefulRestart cannot be changed after creation
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index 78019979..4025e427 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -571,10 +571,10 @@ spec:
                 type: boolean
               enableGracefulRestart:
                 description: |-
-                  EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                  known routes while the routing protocol information is being restored.
-                  This field is immutable because it requires restart of the BGP session
-                  Supported for FRR mode only.
+                  EnableGracefulRestart allows BGP peer to continue to forward data packets
+                  along known routes while the routing protocol information is being
+                  restored. This field is immutable because it requires restart of the BGP
+                  session. Supported for FRR mode only.
                 type: boolean
                 x-kubernetes-validations:
                 - message: EnableGracefulRestart cannot be changed after creation
diff --git a/config/manifests/metallb-native-prometheus.yaml b/config/manifests/metallb-native-prometheus.yaml
index db2a250f..164a6b93 100644
--- a/config/manifests/metallb-native-prometheus.yaml
+++ b/config/manifests/metallb-native-prometheus.yaml
@@ -571,10 +571,10 @@ spec:
                 type: boolean
               enableGracefulRestart:
                 description: |-
-                  EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                  known routes while the routing protocol information is being restored.
-                  This field is immutable because it requires restart of the BGP session
-                  Supported for FRR mode only.
+                  EnableGracefulRestart allows BGP peer to continue to forward data packets
+                  along known routes while the routing protocol information is being
+                  restored. This field is immutable because it requires restart of the BGP
+                  session. Supported for FRR mode only.
                 type: boolean
                 x-kubernetes-validations:
                 - message: EnableGracefulRestart cannot be changed after creation
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index 060e108f..a6f5018b 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -571,10 +571,10 @@ spec:
                 type: boolean
               enableGracefulRestart:
                 description: |-
-                  EnableGracefulRestart allows BGP peer to continue to forward data packets along
-                  known routes while the routing protocol information is being restored.
-                  This field is immutable because it requires restart of the BGP session
-                  Supported for FRR mode only.
+                  EnableGracefulRestart allows BGP peer to continue to forward data packets
+                  along known routes while the routing protocol information is being
+                  restored. This field is immutable because it requires restart of the BGP
+                  session. Supported for FRR mode only.
                 type: boolean
                 x-kubernetes-validations:
                 - message: EnableGracefulRestart cannot be changed after creation
diff --git a/design/gracefulrestart-bgp.md b/design/gracefulrestart-bgp.md
new file mode 100644
index 00000000..c14f90a9
--- /dev/null
+++ b/design/gracefulrestart-bgp.md
@@ -0,0 +1,194 @@
+# BGP Graceful Restart
+
+This is not a design document but extra documenation for therefore
+Graceful Restart feature.
+
+### Introduction
+
+BGP Graceful Restart (GR) functionality
+[(RFC-4724)](https://datatracker.ietf.org/doc/html/rfc4724) defines the mechanism
+that allows the BGP routers to continue to forward data packets along known
+routes while the routing protocol information is being restored.  GR can be
+applied when the control plane is independent from the forwarding plane and
+therefore a restart of the control plane can happen without affecting
+forwarding. This is the case for a most Kubernetes clusters where the control
+plane is a host-networked process (FRR) and the forwarding plane
+is the primary network CNI (Calico, Cilium, OVNK etc). This feature is
+implemented in MetalLB to minimize network disruptions during planned pod
+restarts that take place due to upgrades.
+
+### Configuring GR
+
+GR can be applied per BGP neighbor by setting the field `enableGracefulRestart`
+to true.
+
+```yaml
+apiVersion: metallb.io/v1beta2
+kind: BGPPeer
+metadata:
+  name: example
+  namespace: metallb-system
+spec:
+  myASN: 64512
+  peerASN: 64512
+  peerAddress: 172.30.0.3
+  enableGracefulRestart: true
+```
+
+### GR is Immutable Field
+
+GR is a capability that can only be applied in the OPEN message during BGP
+handshake. This is defined the BGP protocol and cannot be changed. MetalLB does
+not have a user facing mechanism to reset BGP session neither is resetting
+internally the peering. Therefore the option was either allow the configuration
+to pass through and warn the user to `reset BGP` peering externally (e.g. by executing a
+BGP command the external router) or to make it immutable and therefore
+user must delete/create peers. The latter option was preferred.
+
+{{% notice info %}}
+BGP GR requires both ends to be well configured, therefore is recommended
+to verify in the external peer that BGP has GR enabled
+
+```
+$show bgp neighbor <peer>
+...
+    Graceful Restart Capability: advertised and received
+      Remote Restart timer is 120 seconds
+      Address families by peer:
+        IPv4 Unicast(preserved)
+  Graceful restart information:
+    End-of-RIB send: IPv4 Unicast
+    End-of-RIB received: IPv4 Unicast
+    Local GR Mode: Helper*
+
+    Remote GR Mode: Restart
+...
+```
+{{% /notice %}}
+
+### GR Internal Parameters
+
+GR has a number of internal parameters, the `traditional` defaults of FRR are kept
+with one exception.
+
+* [`bgp graceful-restart restart-time`](https://docs.frrouting.org/en/latest/bgp.html#clicmd-bgp-graceful-restart-restart-time-0-4095) is 120 seconds (by FRR).
+* [`no bgp graceful-restart notifications`](https://docs.frrouting.org/en/latest/bgp.html#clicmd-bgp-graceful-restart-notification) no GR support for BGP NOTIFICATION messages (by FRR)
+* [`no bgp hard-administrative-reset`](https://docs.frrouting.org/en/latest/bgp.html#clicmd-bgp-hard-administrative-reset) (by FRR).
+* [`bgp long-lived-graceful-restart stale-time 0`](https://docs.frrouting.org/en/latest/bgp.html#clicmd-bgp-long-lived-graceful-restart-stale-time-1-16777215) BGP long-lived graceful restart (LLGR/[RFC-9494](https://datatracker.ietf.org/doc/rfc9494/)) is disabled (by FRR).
+* [`bgp graceful-restart preserve-fw-state`]( https://docs.frrouting.org/en/latest/bgp.html#bgp-gr-preserve-forwarding-state) Forwarding State (F) bit is set (by MetalLB).
+
+### When GR is Triggered
+
+In context of [Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination), 
+when the pod that runs the FRR process stops, it means that the API asks
+kubelet to stop a pod and kubelet does that by sending a SIGTERM to the `pid 0`
+of the pod. When FRR process receives the signal, it closes the TCP connection,
+and that event triggers GR timer to the external peer. That is the desired
+behavior when our Daemonset pod stops for an update, but it might be less
+desirable if user reduces the set of node the pod can run or user removes
+MetalLB instance when using operator. Nevertheless impact should be low because
+dataplane continues to work. No other case has been identified where GR is triggered.
+
+{{% notice info %}}
+When no TCP packet is lost, the external peer should start the GR timer, nevertheless
+that needs to validated/tested in the specific vendor device. Example logs from FRR
+
+```
+$ cat frr.log
+...
+2024/11/18 10:01:50.140 BGP: [NJ2F2-2W769] 172.18.0.2 [Event] BGP connection closed fd 23
+2024/11/18 10:01:50.140 BGP: [NTX3S-9Q8YV] 172.18.0.2 [Event] BGP error 5 on fd 23
+2024/11/18 10:01:50.140 BGP: [ZWCSR-M7FG9] 172.18.0.2 [FSM] TCP_connection_closed (Established->Clearing), fd 23
+2024/11/18 10:01:50.140 BGP: [RPZW2-39GTY] 172.18.0.2(frr-k8s-control-plane) graceful restart timer started for 120 sec
+2024/11/18 10:01:50.140 BGP: [TK2B6-ZF4MR] 172.18.0.2(frr-k8s-control-plane) graceful restart stalepath timer started for 360 sec
+...
+```
+{{% /notice %}}
+
+
+### GR and Cordoned Node
+
+GR is orthogonal to any Kubernetes node admin [operation](https://kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/)
+and the expected behavior does not change. When admin drains/cordons a node from the cluster, the node will be
+labeled as Unschedulable. When that happens all routes towards that node will
+be removed as normal procedure\*. The BGP peering will remain because Daemonsets
+pods continue to run on Unschedulable nodes (`--ignore-daemonsets`). The BGP
+peer will not have anymore routes to the node, so UDP packets will be
+redirected to other nodes, existing TCP connections will break\*\*, but no new
+traffic will be blackholed. After all that, the node might be hard rebooted or removed,
+then the Daemonset pod will be removed, BGP will be properly terminated
+(TCP connection close), GR will start on the peer but no routes exists to be staled.
+
+\* If service is of traffic policy "Local", then the routes will be removed
+faster because any pod that is endpoint to the service will be removed from the node.
+
+\*\* TCP connection could remain established if the service is of traffic
+policy "Cluster" and if Kubernetes primary CNI plugin implements consistent-hashing
+[[Cilium]](https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/#maglev-consistent-hashing).
+
+**Note:** when BGP peers are removed or added, rebalancing might take place due to
+the load balancing hashing mechanism of the external peer, e.g. client A
+that had a connection to node X can switch to node Y just because node Z was
+removed from the cluster.
+
+
+### GR and BFD
+
+According to the [RFC-5881/BFD Shares Fate with the Control
+Plane](https://datatracker.ietf.org/doc/html/rfc5882#section-4.3.2), 
+
+> If BFD shares fate with the control plane on either system (the "C"
+   bit is clear in either direction), a BFD session failure cannot be
+   disentangled from other events taking place in the control plane. In
+   many cases, the BFD session will fail as a side effect of the restart
+   taking place. As such, it would be best to avoid aborting any
+   Graceful Restart taking place, if possible (since otherwise BFD and
+   Graceful Restart cannot coexist).
+
+and therefore GR and BFD can work together, and the helper router should
+ignore the BFD messages during GR timer (during the green box bellow).
+
+
+{{<mermaid align="center">}}
+sequenceDiagram
+participant kubelet
+participant A as K8S FRR
+participant B as External Peer
+A-->>B:  Sends Graceful Restart Capability in BGP OPEN message
+
+kubelet->>A: SIGTERM
+A->>+B: TCP Close
+rect rgb(192,255,193)
+Note right of B: graceful restart timer started for 120 sec
+Note right of B: BFD events are ignored
+B-->>B:  neigh went from Established to Clearing
+Note right of B: Routes are stale
+A->>A: Restarting 
+A->>+B: BGP Peering
+B-->>B:   A went from OpenConfirm to Established
+end
+Note over A,B: BGP Established
+Note right of B: graceful restart timer stops
+Note right of B: BFD events are NOT ignored
+A->>+B: Update/End-Of Rib
+Note right of B: Routes are NOT stale
+{{< /mermaid >}}
+
+{{% notice warning %}}
+Whether BFD and GR can be used together is implementation specific.
+It is up to vendor's recommendation and needs to be tested. For example Juniper
+suggests not to combine them
+[link](https://www.juniper.net/documentation/us/en/software/junos/bgp/topics/topic-map/bfd-for-bgp-session.html).
+
+One consideration to be taken into account is that doing GR/BFD between routers
+that are placed in the middle of a large BGP network is different than doing GR/BFD between server
+and ToR/DCGW routers.
+{{% /notice %}}
+
+### Know Issue
+
+There are two known issues which have been fixed upstream but not being used yet by MetalLB.
+
+- There is warning message `Graceful restart configuration changed, reset this peer to take effect` that appears
+in reloader which has been fixed upstream [issue](https://github.com/FRRouting/frr/issues/15880)
+- BFD removes routes momentarily [issue](https://github.com/FRRouting/frr/issues/17337)
diff --git a/go.mod b/go.mod
index e421789c..046cc6b1 100644
--- a/go.mod
+++ b/go.mod
@@ -14,21 +14,21 @@ require (
 	github.com/mdlayher/ndp v0.0.0-20200602162440-17ab9e3e5567
 	github.com/metallb/frr-k8s v0.0.16
 	github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721
-	github.com/onsi/ginkgo/v2 v2.20.2
-	github.com/onsi/gomega v1.36.0
+	github.com/onsi/ginkgo/v2 v2.22.0
+	github.com/onsi/gomega v1.36.1
 	github.com/open-policy-agent/cert-controller v0.10.2-0.20240531181455-2649f121ab97
 	github.com/ory/dockertest/v3 v3.10.0
 	github.com/prometheus/client_golang v1.19.1
 	go.uber.org/zap v1.27.0
 	golang.org/x/sys v0.26.0
-	k8s.io/api v0.31.3
+	k8s.io/api v0.31.4
 	k8s.io/apiextensions-apiserver v0.31.1
-	k8s.io/apimachinery v0.31.3
-	k8s.io/client-go v0.31.1
+	k8s.io/apimachinery v0.31.4
+	k8s.io/client-go v0.31.4
 	k8s.io/component-base v0.31.1
 	k8s.io/klog v1.0.0
 	k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
-	sigs.k8s.io/controller-runtime v0.19.0
+	sigs.k8s.io/controller-runtime v0.19.3
 	sigs.k8s.io/yaml v1.4.0
 )
 
@@ -48,7 +48,6 @@ require (
 	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
@@ -63,7 +62,7 @@ require (
 	github.com/google/btree v1.0.1 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect
+	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
@@ -105,14 +104,14 @@ require (
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
-	golang.org/x/mod v0.20.0 // indirect
+	golang.org/x/mod v0.21.0 // indirect
 	golang.org/x/net v0.30.0 // indirect
 	golang.org/x/oauth2 v0.21.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
 	golang.org/x/term v0.25.0 // indirect
 	golang.org/x/text v0.19.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
-	golang.org/x/tools v0.24.0 // indirect
+	golang.org/x/tools v0.26.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/protobuf v1.35.1 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
diff --git a/go.sum b/go.sum
index 399fbbd2..fbab3ccb 100644
--- a/go.sum
+++ b/go.sum
@@ -45,8 +45,6 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv
 github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -98,8 +96,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA=
-github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -183,10 +181,10 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
-github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
-github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y=
-github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
+github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
+github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
 github.com/open-policy-agent/cert-controller v0.10.2-0.20240531181455-2649f121ab97 h1:otk1yQtHleQq5VIy5C+rAYnErOT/WrvDyplqPhYDqb8=
 github.com/open-policy-agent/cert-controller v0.10.2-0.20240531181455-2649f121ab97/go.mod h1:ZoyStAh4D97A4+k4BUj/Q/eyqyeIR4gl/NuVGAWUhvQ=
 github.com/open-policy-agent/frameworks/constraint v0.0.0-20230822235116-f0b62fe1e4c4 h1:5dum5SLEz+95JDLkMls7Z7IDPjvSq3UhJSFe4f5einQ=
@@ -277,8 +275,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
-golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -332,8 +330,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
-golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -363,14 +361,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
 gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
-k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8=
-k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE=
+k8s.io/api v0.31.4 h1:I2QNzitPVsPeLQvexMEsj945QumYraqv9m74isPDKhM=
+k8s.io/api v0.31.4/go.mod h1:d+7vgXLvmcdT1BCo79VEgJxHHryww3V5np2OYTr6jdw=
 k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
 k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
-k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4=
-k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
-k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
-k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
+k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM=
+k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
+k8s.io/client-go v0.31.4 h1:t4QEXt4jgHIkKKlx06+W3+1JOwAFU/2OPiOo7H92eRQ=
+k8s.io/client-go v0.31.4/go.mod h1:kvuMro4sFYIa8sulL5Gi5GFqUPvfH2O/dXuKstbaaeg=
 k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
 k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
 k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
@@ -383,8 +381,8 @@ k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5
 k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc=
 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
-sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
+sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw=
+sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
diff --git a/internal/config/validation.go b/internal/config/validation.go
index 0b9e6409..2d1759e1 100644
--- a/internal/config/validation.go
+++ b/internal/config/validation.go
@@ -7,9 +7,12 @@ import (
 
 	"errors"
 
+	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
 	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
 	"go.universe.tf/metallb/internal/bgp/community"
 	"go.universe.tf/metallb/internal/ipfamily"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 )
 
 type Validate func(ClusterResources) error
@@ -69,7 +72,17 @@ func findIPv6BGPAdvertisement(c ClusterResources) error {
 	if len(c.BGPAdvs) == 0 {
 		return nil
 	}
+
+	bgpSelectors, err := poolSelectorsForBGP(c)
+	if err != nil {
+		return err
+	}
+
 	for _, p := range c.Pools {
+		if !bgpSelectors.matchesPool(p) {
+			continue
+		}
+
 		for _, cidr := range p.Spec.Addresses {
 			nets, err := ParseCIDR(cidr)
 			if err != nil {
@@ -203,3 +216,49 @@ func hasBFDEcho(peer *Peer, bfdProfiles map[string]*BFDProfile) bool {
 func peerAddressKey(peer metallbv1beta2.BGPPeerSpec) string {
 	return fmt.Sprintf("%s-%s", peer.Address, peer.VRFName)
 }
+
+type poolSelector struct {
+	byName   map[string]struct{}
+	byLabels []labels.Selector
+}
+
+func (s poolSelector) matchesPool(p metallbv1beta1.IPAddressPool) bool {
+	if len(s.byLabels) == 0 && len(s.byName) == 0 {
+		return true
+	}
+
+	if _, ok := s.byName[p.Name]; ok {
+		return true
+	}
+	for _, l := range s.byLabels {
+		if l.Matches(labels.Set(p.Labels)) {
+			return true
+		}
+	}
+	return false
+}
+
+func poolSelectorsForBGP(c ClusterResources) (poolSelector, error) {
+	selectedPools := make(map[string]struct{})
+	poolsSelectors := []labels.Selector{}
+	for _, adv := range c.BGPAdvs {
+		if len(adv.Spec.IPAddressPools) == 0 &&
+			len(adv.Spec.IPAddressPoolSelectors) == 0 {
+			return poolSelector{}, nil // no selectors, let's catch em all!
+		}
+		for _, p := range adv.Spec.IPAddressPools {
+			selectedPools[p] = struct{}{}
+		}
+		for _, selector := range adv.Spec.IPAddressPoolSelectors {
+			l, err := metav1.LabelSelectorAsSelector(&selector)
+			if err != nil {
+				return poolSelector{}, fmt.Errorf("invalid label selector %v", selector)
+			}
+			poolsSelectors = append(poolsSelectors, l)
+		}
+	}
+	return poolSelector{
+		byName:   selectedPools,
+		byLabels: poolsSelectors,
+	}, nil
+}
diff --git a/netlify.toml b/netlify.toml
index 1e3d84eb..40206245 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -5,3 +5,9 @@
 
 [build.environment]
   HUGO_VERSION = "0.124.1"
+
+[[redirects]]
+  from = "https://metallb.universe.tf"
+  to = "https://metallb.io"
+  status = 301
+  force = true
diff --git a/tasks.py b/tasks.py
index 50c7e4c1..3602a81a 100644
--- a/tasks.py
+++ b/tasks.py
@@ -30,6 +30,7 @@ controller_gen_version = "v0.16.3"
 build_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "build")
 kubectl_path = os.path.join(build_path, "kubectl")
 kind_path = os.path.join(build_path, "kind")
+ginkgo_path = os.path.join(build_path, "bin", "ginkgo")
 controller_gen_path = os.path.join(build_path, "bin", "controller-gen")
 kubectl_version = "v1.31.0"
 kind_version = "v0.24.0"
@@ -1228,6 +1229,7 @@ def e2etest(
     """Run E2E tests against development cluster."""
     fetch_kubectl()
     fetch_kind()
+    fetch_ginkgo()
 
     if skip_docker:
         opt_skip_docker = "--skip-docker"
@@ -1313,8 +1315,9 @@ def e2etest(
         external_frr_image = "--frr-image=" + (external_frr_image)
     testrun = run(
         "cd `git rev-parse --show-toplevel`/e2etest &&"
-        "KUBECONFIG={} ginkgo {} --junit-report={} --timeout=3h {} {} -- --kubeconfig={} --service-pod-port={} -ipv4-service-range={} -ipv6-service-range={} {} --report-path {} {} -node-nics {} -local-nics {} {} -bgp-mode={} -with-vrf={} {} --host-bgp-mode={} --kubectl={} --frr-k8s-namespace={}".format(
+        "KUBECONFIG={} {} {} --junit-report={} --timeout=3h {} {} -- --kubeconfig={} --service-pod-port={} -ipv4-service-range={} -ipv6-service-range={} {} --report-path {} {} -node-nics {} -local-nics {} {} -bgp-mode={} -with-vrf={} {} --host-bgp-mode={} --kubectl={} --frr-k8s-namespace={}".format(
             kubeconfig,
+            ginkgo_path,
             ginkgo_params,
             junit_report,
             ginkgo_focus,
@@ -1336,7 +1339,8 @@ def e2etest(
             kubectl_path,
             frr_k8s_namespace,
         ),
-        warn="True",
+        warn=True,
+        echo=True,
     )
 
     if export != None:
@@ -1561,6 +1565,31 @@ def fetch_controller_gen():
     )
 
 
+@cache
+def fetch_ginkgo():
+    ginkgo_version = run(
+        "go list -m -f '{{.Version}}' github.com/onsi/ginkgo/v2",
+        warn=True,
+        hide="both",
+    ).stdout
+
+    print(ginkgo_version)
+    fetch_command = (
+        f"GOBIN={build_path}/bin/ GOPATH={build_path} go install github.com/onsi/ginkgo/v2/ginkgo@"
+        + ginkgo_version
+    )
+    print(fetch_command)
+    print(ginkgo_path)
+    get_version_command = f"{ginkgo_path} version"
+    fetch_dependency(
+        ginkgo_path,
+        ginkgo_version,
+        fetch_command,
+        get_version_command,
+        "Ginkgo Version ",
+    )
+
+
 def fetch_dependency(
     path: str,
     version: str,
diff --git a/website/content/apis/_index.md b/website/content/apis/_index.md
index e9998eb0..74cc8a3e 100644
--- a/website/content/apis/_index.md
+++ b/website/content/apis/_index.md
@@ -296,7 +296,7 @@ _Appears in:_
 | `password` _string_ | Authentication password for routers enforcing TCP MD5 authenticated sessions |
 | `passwordSecret` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#secretreference-v1-core)_ | passwordSecret is name of the authentication secret for BGP Peer.<br />the secret must be of type "kubernetes.io/basic-auth", and created in the<br />same namespace as the MetalLB deployment. The password is stored in the<br />secret as the key "password". |
 | `bfdProfile` _string_ | The name of the BFD Profile to be used for the BFD session associated to the BGP session. If not set, the BFD session won't be set up. |
-| `enableGracefulRestart` _boolean_ | EnableGracefulRestart allows BGP peer to continue to forward data packets along<br />known routes while the routing protocol information is being restored.<br />This field is immutable because it requires restart of the BGP session<br />Supported for FRR mode only. |
+| `enableGracefulRestart` _boolean_ | EnableGracefulRestart allows BGP peer to continue to forward data packets<br />along known routes while the routing protocol information is being<br />restored. This field is immutable because it requires restart of the BGP<br />session. Supported for FRR mode only. |
 | `ebgpMultiHop` _boolean_ | To set if the BGPPeer is multi-hops away. Needed for FRR mode only. |
 | `vrf` _string_ | To set if we want to peer with the BGPPeer using an interface belonging to<br />a host vrf |
 | `disableMP` _boolean_ | To set if we want to disable MP BGP that will separate IPv4 and IPv6 route exchanges into distinct BGP sessions. |
diff --git a/website/content/concepts/_index.md b/website/content/concepts/_index.md
index 5964fb19..a4810ab8 100644
--- a/website/content/concepts/_index.md
+++ b/website/content/concepts/_index.md
@@ -40,6 +40,11 @@ providing cluster services to your LAN.
 Or, you could do both! MetalLB lets you define as many address pools
 as you want, and doesn't care what "kind" of addresses you give it.
 
+When one or more IPs are assigned to a service, MetalLB tries to keep
+them assigned to that service. In case of removal of those IPs from the pools
+(deletion or edit of the IPAddressPool those IPs are taken from),
+MetalLB will assign another set of IPs to the service (when available). 
+
 ## External announcement
 
 After MetalLB has assigned an external IP address to a service, it
diff --git a/website/content/configuration/_advanced_bgp_configuration.md b/website/content/configuration/_advanced_bgp_configuration.md
index 54fb58f2..da16047d 100644
--- a/website/content/configuration/_advanced_bgp_configuration.md
+++ b/website/content/configuration/_advanced_bgp_configuration.md
@@ -412,3 +412,34 @@ spec:
           allowed:
             mode: all
 ```
+### Graceful Restart
+
+BGP Graceful Restart (GR) functionality [(RFC-4724)](https://datatracker.ietf.org/doc/html/rfc4724) defines the mechanism
+that allows the BGP routers to continue to forward data packets along known
+routes while the routing protocol information is being restored. GR can be
+applied when the control plane is independent from the forwarding plane, which
+is the case for the most Kubernetes clusters. This feature was added to
+minimize network disruptions during upgrades.
+
+GR can be applied per BGP neighbor by setting the field `enableGracefulRestart`
+to true, note that this field is immutable. For example
+
+```yaml
+apiVersion: metallb.io/v1beta2
+kind: BGPPeer
+metadata:
+  name: example
+  namespace: metallb-system
+spec:
+  myASN: 64512
+  peerASN: 64512
+  peerAddress: 172.30.0.3
+  enableGracefulRestart: true
+```
+
+#### GR With BFD
+
+According to the [RFC-5881/BFD Shares Fate with the Control
+Plane](https://datatracker.ietf.org/doc/html/rfc5882#section-4.3.2), BFD and
+Graceful Restart can work together, but is implementation specific. It is up to
+vendor's recommendation and needs to be tested.
diff --git a/website/content/configuration/_advanced_ipaddresspool_configuration.md b/website/content/configuration/_advanced_ipaddresspool_configuration.md
index 3ed10ff4..2d8372a9 100644
--- a/website/content/configuration/_advanced_ipaddresspool_configuration.md
+++ b/website/content/configuration/_advanced_ipaddresspool_configuration.md
@@ -129,3 +129,59 @@ In order to re-assign a new IP to the services, there are two options:
 This behaviour is subject to change making MetalLB observe any state requested by the user, regardless
 of the fact that it may cause service disruptions or not.
 {{% /notice %}}
+
+### PreferDualStack IP Family Policy
+
+MetalLB supports `PreferDualStack` ip policy, which allows services to prefer
+dual-stack IP allocations while gracefully falling back to single-stack if
+dual-stack is unavailable.
+
+Consider this service
+
+```yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-app
+spec:
+  type: LoadBalancer
+  ipFamilyPolicy: PreferDualStack
+  ipFamilies:
+  - ipv6
+  - ipv4
+```
+
+Suppose that the advertisement is as followed:
+
+```yaml
+apiVersion: metallb.io/v1beta1
+kind: BGPAdvertisement
+metadata:
+  name: external
+  namespace: metallb-system
+spec:
+  ipAddressPools:
+  - ipv4-pool
+  - ipv6-pool
+  - dualstack-pool
+```
+
+The above Service will be assigned 1 ipv4 and 1 ipv6 from the dualstack-pool.
+
+In case the `dualstack-pool` is not added to `ipAddressPools`, svc `my-app` will
+be assigned an ipv6 from the pool `ipv6-pool`, as ipv6 is the first ip family
+listed in the service's `spec.ipFamilies` field.
+
+In case the field `spec.ipFamilies` is empty, ipv4 will be preferred.
+
+Metallb's behaviours in svc with `PreferDualStack` family policy, in case of pool
+change:
+
+- If an svc was assigned with an ip from a single-stack pool, and the said pool is
+extended to be dualstack, the originally assigned ip will stay assigned, and
+the svc will be assigned an additional ip from the newly available stack.
+- If an svc was assigned with ips from a dual-stack pool, and the said pool
+becomes single-stack, the assigned ips are considered malfunctioning, both of
+them are cleared from the svc. Metallb will then perform a re-assign.
+- When an svc was assigned with an ip from a single-stack pool, and a dual-stack
+pool becomes available, the svc will keep its current pool.
diff --git a/website/content/configuration/_advanced_l2_configuration.md b/website/content/configuration/_advanced_l2_configuration.md
index 426a19b8..b17c3645 100644
--- a/website/content/configuration/_advanced_l2_configuration.md
+++ b/website/content/configuration/_advanced_l2_configuration.md
@@ -131,10 +131,11 @@ metadata:
   name: example-advertisement10
   namespace: metallb-system
 spec:
-  ipaddresspools:
+  ipAddressPools:
   - pool1
-  nodeSelector:
-  - kubernetes.io/hostname: hostB
+  nodeSelectors:
+  - matchLabels:
+      kubernetes.io/hostname: hostB
   interfaces:
   - ens18
 ```
diff --git a/website/content/prometheus-metrics/_index.md b/website/content/prometheus-metrics/_index.md
index 990a9d9a..24ec34b8 100644
--- a/website/content/prometheus-metrics/_index.md
+++ b/website/content/prometheus-metrics/_index.md
@@ -31,6 +31,15 @@ metallb_bgp_updates_total{peer="172.30.0.2:0"} 1
 metallb_bgp_updates_total{peer="172.30.0.3:0"} 1
 ```
 
+when using BGP with a vrf, an additional label with the name of the vrf is added:
+
+```bash
+# HELP metallb_bgp_updates_total Number of BGP UPDATE messages sent
+# TYPE metallb_bgp_updates_total counter
+metallb_bgp_updates_total{peer="172.23.0.5:0"} 1
+metallb_bgp_updates_total{peer="172.23.0.5:0" vrf="red"} 1
+```
+
 | Name                                 | Description                                                      |
 | ------------------------------------ | ---------------------------------------------------------------- |
 | metallb_bgp_session_up               | BGP session state (1 is up, 0 is down)                           |
diff --git a/website/content/troubleshooting/_index.md b/website/content/troubleshooting/_index.md
index 563e15da..582553ba 100644
--- a/website/content/troubleshooting/_index.md
+++ b/website/content/troubleshooting/_index.md
@@ -246,6 +246,29 @@ in the BGPAdvertisement / L2Advertisement) and to limit the service's endpoints
 By doing this, the path of the traffic will be clear and it will be easy to understand if the issue
 is caused only by a subset of the nodes or when the pod lives on a different node, for example.
 
+#### MetalLB and asymmetric return path
+
+[Reverse path filtering](https://tldp.org/HOWTO/Adv-Routing-HOWTO/lartc.kernel.rpf.html) is a linux protection
+mechanism which causes packets that should not be coming from an interface to be dropped (i.e., there is no route to the ip of
+the client sending those packets through that interface).
+
+This is common with BGP (and less so with L2 mode) because MetalLB will advertise routes to the service but the nodes
+won't learn how to reach the client:
+
+- The client from a different subnet tries to reach the service exposed by MetalLB
+- The packets reach the node on an interface different from the default gateway
+- The node does not have a route back to the client through that interface
+
+Depending on the value of the `rp_filter` associated to the interface, the packets are dropped.
+
+There are several ways to solve this issue:
+
+- Add static routes to your node
+- Use the [frr-k8s variant](https://metallb.io/concepts/bgp/index.html#frr-k8s-mode) to let your fabric send those
+routes to the nodes
+- Use source based routing (or something that leverages that) to steer the reply traffic towards the right interface.
+This is the approach used by the [metallb node route agent](https://github.com/travisghansen/metallb-node-route-agent) (note that the project is not affiliated to metallb).
+
 ## Collecting information for a bug report
 
 If after following the suggestions of this guide, a MetalLB bug is the primary suspect, you need to file a


Test patch:
diff --git a/e2etest/bgptests/bgp.go b/e2etest/bgptests/bgp.go
index 6994754e..62ecac62 100644
--- a/e2etest/bgptests/bgp.go
+++ b/e2etest/bgptests/bgp.go
@@ -22,6 +22,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net"
 	"strings"
 	"time"
 
@@ -681,9 +682,9 @@ var _ = ginkgo.Describe("BGP", func() {
 					Expect(neighbors).To(HaveLen(len(previousNeighbors[c.Name])))
 
 					for _, n := range neighbors {
-						previousDropped := previousNeighbors[c.Name][n.IP.String()].ConnectionsDropped
+						previousDropped := previousNeighbors[c.Name][n.ID].ConnectionsDropped
 						if n.ConnectionsDropped > previousDropped {
-							return fmt.Errorf("increased connections dropped from %s to %s, previous: %d current %d", c.Name, n.IP.String(), previousDropped, n.ConnectionsDropped)
+							return fmt.Errorf("increased connections dropped from %s to %s, previous: %d current %d", c.Name, n.ID, previousDropped, n.ConnectionsDropped)
 						}
 					}
 				}
@@ -1540,10 +1541,10 @@ var _ = ginkgo.Describe("BGP", func() {
 							return fmt.Errorf("expected connect time to be %d, got %d", int(connectTime.Seconds()), neighbor.ConfiguredConnectTime)
 						}
 
-						neighborFamily := ipfamily.ForAddress(neighbor.IP)
+						neighborFamily := ipfamily.ForAddress(net.ParseIP(neighbor.ID))
 						for _, family := range neighbor.AddressFamilies {
 							if !strings.Contains(family, string(neighborFamily)) {
-								return fmt.Errorf("expected %s neigbour to contain only %s families but contains %s", neighbor.IP, neighborFamily, family)
+								return fmt.Errorf("expected %s neigbour to contain only %s families but contains %s", neighbor.ID, neighborFamily, family)
 							}
 						}
 					}
diff --git a/e2etest/go.work.sum b/e2etest/go.work.sum
index 2cbc2e3f..f7faaff1 100644
--- a/e2etest/go.work.sum
+++ b/e2etest/go.work.sum
@@ -798,8 +798,6 @@ 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.16 h1:tlNIDW5wtBlKE7wUgXBs9GklyTCTFtdOGz8fvEl40vU=
-github.com/metallb/frr-k8s v0.0.16/go.mod h1:TjrGoAf+v00hYGlI8jUdyDxY5udMAOs2GWwrvLWnA4E=
 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=
@@ -837,6 +835,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.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=
 github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
@@ -845,6 +844,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.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
 github.com/open-policy-agent/cert-controller v0.10.1/go.mod h1:4uRbBLY5DsPOog+a9pqk3JLxuuhrWsbUedQW65HcLTI=
 github.com/open-policy-agent/opa v0.54.0 h1:mGEsK+R5ZTMV8fzzbNzmYDGbTmY30wmRCIHmtm2VqWs=
 github.com/open-policy-agent/opa v0.54.0/go.mod h1:d8I8jWygKGi4+T4H07qrbeCdH1ITLsEfT0M+bsvxWw0=
@@ -1152,6 +1152,7 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
@@ -1232,6 +1233,7 @@ 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.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 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=
@@ -1339,6 +1341,7 @@ 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.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 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=
diff --git a/e2etest/l2tests/assignment.go b/e2etest/l2tests/assignment.go
index eadffc7a..6c88c5a9 100644
--- a/e2etest/l2tests/assignment.go
+++ b/e2etest/l2tests/assignment.go
@@ -977,6 +977,7 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 				Expect(err).NotTo(HaveOccurred())
 				return len(svc1.Status.LoadBalancer.Ingress)
 			}, 5*time.Minute, 1*time.Second).Should(Equal(2))
+			originallyAssignedIps := getServiceIps(cs, svc1.Namespace, svc1.Name)
 			ginkgo.By("Validate LoadBalancer IPs are allocated from pool1")
 			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{pool1}, jigservice.GetIngressPoint(
 				&svc1.Status.LoadBalancer.Ingress[0]))
@@ -984,7 +985,13 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{pool1}, jigservice.GetIngressPoint(
 				&svc1.Status.LoadBalancer.Ingress[1]))
 			Expect(err).NotTo(HaveOccurred())
-			ginkgo.By("Updating current pool to exclude ipv4 address, and add a dualstack pool")
+			ginkgo.By("Updating current pool to have an additional dualstack pool")
+			resources = config.Resources{
+				Pools: []metallbv1beta1.IPAddressPool{pool1, poolDual},
+			}
+			err = ConfigUpdater.Update(resources)
+			Expect(err).NotTo(HaveOccurred())
+			ginkgo.By("Updating current pool to exclude ipv4 address")
 			pool1.Spec.Addresses = []string{v6PoolAddresses}
 			resources = config.Resources{
 				Pools: []metallbv1beta1.IPAddressPool{pool1, poolDual},
@@ -992,17 +999,19 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 			err = ConfigUpdater.Update(resources)
 			Expect(err).NotTo(HaveOccurred())
 			ginkgo.By("Validate that the service still has 2 IPs assigned")
-			Eventually(func() int {
-				svc1, err = cs.CoreV1().Services(svc1.Namespace).Get(context.Background(), svc1.Name, metav1.GetOptions{})
-				Expect(err).NotTo(HaveOccurred())
-				return len(svc1.Status.LoadBalancer.Ingress)
-			}, 5*time.Minute, 1*time.Second).Should(Equal(2))
+			Eventually(func() []string {
+				newIps := getServiceIps(cs, svc1.Namespace, svc1.Name)
+				return newIps
+			}, 5*time.Minute, 1*time.Second).Should(And(
+				HaveLen(2),
+				Not(Equal(originallyAssignedIps)),
+			))
 			ginkgo.By("Validate LoadBalancer IPs are allocated from poolDual")
-			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{poolDual}, jigservice.GetIngressPoint(
-				&svc1.Status.LoadBalancer.Ingress[0]))
+			svcIPs := getServiceIps(cs, svc1.Namespace, svc1.Name)
+			Expect(svcIPs).To(HaveLen(2))
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{poolDual}, svcIPs[0])
 			Expect(err).NotTo(HaveOccurred())
-			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{poolDual}, jigservice.GetIngressPoint(
-				&svc1.Status.LoadBalancer.Ingress[1]))
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{poolDual}, svcIPs[1])
 			Expect(err).NotTo(HaveOccurred())
 		})
 	})
@@ -1018,3 +1027,16 @@ func updateServiceIPFamilyPolicy(cs clientset.Interface, namespace, serviceName
 	_, err = cs.CoreV1().Services(namespace).Update(context.TODO(), svc, metav1.UpdateOptions{})
 	return err
 }
+
+// getServiceIps returns the ips assigned to the given svc as a slice of strings.
+func getServiceIps(cs clientset.Interface, namespace, serviceName string) []string {
+	svc, err := cs.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{})
+	ips := []string{}
+	if err != nil {
+		return ips
+	}
+	for _, ingress := range svc.Status.LoadBalancer.Ingress {
+		ips = append(ips, ingress.IP)
+	}
+	return ips
+}
diff --git a/e2etest/pkg/container/routes.go b/e2etest/pkg/container/routes.go
index 775b773d..ef38a9e9 100644
--- a/e2etest/pkg/container/routes.go
+++ b/e2etest/pkg/container/routes.go
@@ -3,7 +3,10 @@
 package container
 
 import (
+	"encoding/json"
 	"fmt"
+	"net/netip"
+	"strings"
 
 	"go.universe.tf/e2etest/pkg/executor"
 	"go.universe.tf/e2etest/pkg/netdev"
@@ -93,3 +96,126 @@ func SetupVRFForNetwork(containerName, vrfNetwork, vrfName, vrfRoutingTable stri
 
 	return nil
 }
+
+// WireContainers creates a point-to-point link between two containers.
+// The second argument will be the name of the interface which will be
+// common to both containers e.g. net0.
+func WireContainers(containerA, containerB, dev string) error {
+	netNSCntA, err := executor.Host.Exec(executor.ContainerRuntime,
+		"inspect", "-f", "{{ .NetworkSettings.SandboxKey }}", containerA)
+	if err != nil {
+		return fmt.Errorf("%s - %w", netNSCntA, err)
+	}
+	netNSCntA = strings.TrimSpace(netNSCntA)
+
+	netNSCntB, err := executor.Host.Exec(executor.ContainerRuntime,
+		"inspect", "-f", "{{ .NetworkSettings.SandboxKey }}", containerB)
+	if err != nil {
+		return fmt.Errorf("%s - %w", netNSCntB, err)
+	}
+	netNSCntB = strings.TrimSpace(netNSCntB)
+
+	if out, err := executor.Host.Exec("sudo", "ip", "link", "add", dev, "netns",
+		netNSCntA, "type", "veth", "peer", "name", dev); err != nil {
+		return fmt.Errorf("%s - %w", out, err)
+	}
+
+	if out, err := executor.Host.Exec("sudo", "ip", "link", "set", "dev", dev,
+		"netns", netNSCntB); err != nil {
+		return fmt.Errorf("%s - %w", out, err)
+	}
+
+	for _, c := range []executor.Executor{executor.ForContainer(containerA), executor.ForContainer(containerB)} {
+		if out, err := c.Exec("ip", "link", "set", "dev", dev, "up"); err != nil {
+			return fmt.Errorf("%s - %w", out, err)
+		}
+	}
+
+	return nil
+}
+
+// BGPRoutes executes `ip route show proto bgp` in the executor and returns all
+// routes filtered by device e.g. net0.  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 {
+			return nil, fmt.Errorf("%s - %w", out, err)
+		}
+
+		var routes []IPRoute
+		err = json.Unmarshal([]byte(out), &routes)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse JSON output: %w", err)
+		}
+
+		for _, r := range routes {
+			dst, err := netip.ParsePrefix(r.Dst)
+			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 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 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 r.Dev == dev {
+					nextHops[addr] = struct{}{}
+				}
+			}
+
+			if len(nextHops) == 0 {
+				continue
+			}
+			ret[dst] = nextHops
+		}
+	}
+
+	return ret, nil
+}
diff --git a/e2etest/pkg/frr/bgp.go b/e2etest/pkg/frr/bgp.go
index 8bc574b0..ff1365ad 100644
--- a/e2etest/pkg/frr/bgp.go
+++ b/e2etest/pkg/frr/bgp.go
@@ -47,7 +47,7 @@ func NeighborsInfo(exec executor.Executor) (NeighborsMap, error) {
 	}
 	res := map[string]*Neighbor{}
 	for _, n := range neighbors {
-		res[n.IP.String()] = n
+		res[n.ID] = n
 	}
 	return res, nil
 }
@@ -152,9 +152,15 @@ func NeighborConnected(neighborJSON string) (bool, error) {
 // To be used for debugging in order to print the status of the frr instance.
 func RawDump(exec executor.Executor, filesToDump ...string) (string, error) {
 	allerrs := errors.New("")
+	res := "####### Show version\n"
+	out, err := exec.Exec("vtysh", "-c", "show version")
+	if err != nil {
+		allerrs = errors.Join(allerrs, fmt.Errorf("\nFailed exec show version: %v", err))
+	}
+	res += out
 
-	res := "####### Show running config\n"
-	out, err := exec.Exec("vtysh", "-c", "show running-config")
+	res += "####### Show running config\n"
+	out, err = exec.Exec("vtysh", "-c", "show running-config")
 	if err != nil {
 		allerrs = errors.Join(allerrs, fmt.Errorf("\nFailed exec show bgp neighbor: %v", err))
 	}
@@ -177,6 +183,20 @@ func RawDump(exec executor.Executor, filesToDump ...string) (string, error) {
 	}
 	res += out
 
+	res += "####### Routes\n"
+	out, err = exec.Exec("vtysh", "-c", "show bgp ipv4 json")
+	if err != nil {
+		allerrs = errors.Join(allerrs, fmt.Errorf("\nFailed exec show bgp ipv4: %v", err))
+	}
+	res += out
+
+	res += "####### Routes\n"
+	out, err = exec.Exec("vtysh", "-c", "show bgp ipv6 json")
+	if err != nil {
+		allerrs = errors.Join(allerrs, fmt.Errorf("\nFailed exec show bgp ipv6: %v", err))
+	}
+	res += out
+
 	res += "####### BFD Peers\n"
 	out, err = exec.Exec("vtysh", "-c", "show bfd peer")
 	if err != nil {
@@ -198,6 +218,13 @@ func RawDump(exec executor.Executor, filesToDump ...string) (string, error) {
 		}
 	}
 
+	res += "####### Network setup for host\n"
+	out, err = exec.Exec("bash", "-c", "ip -6 route; ip -4 route")
+	if err != nil {
+		allerrs = errors.Join(allerrs, fmt.Errorf("\nFailed exec to print network setup: %v", err))
+	}
+	res += out
+
 	if allerrs.Error() == "" {
 		allerrs = nil
 	}
diff --git a/e2etest/pkg/frr/config/config.go b/e2etest/pkg/frr/config/config.go
index e899f434..5ec2256f 100644
--- a/e2etest/pkg/frr/config/config.go
+++ b/e2etest/pkg/frr/config/config.go
@@ -38,9 +38,11 @@ debug bgp nht
 debug bgp updates in
 debug bgp updates out
 debug bgp zebra
+debug bgp bfd
 debug bfd peer
 debug bfd zebra
 debug bfd network
+debug bfd distributed
 !
 ip nht resolve-via-default
 ipv6 nht resolve-via-default
@@ -63,7 +65,7 @@ router bgp {{$ROUTERASN}}
   {{ if .Password -}}
   neighbor {{.Addr}} password {{.Password}}
   {{- end }}
-{{- if .BFDEnabled }} 
+{{- if .BFDEnabled }}
   neighbor {{.Addr}} bfd
 {{- end -}}
 {{- end }}
diff --git a/e2etest/pkg/frr/config/unnumbered.go b/e2etest/pkg/frr/config/unnumbered.go
new file mode 100644
index 00000000..f4315c91
--- /dev/null
+++ b/e2etest/pkg/frr/config/unnumbered.go
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package config
+
+import (
+	"bytes"
+	"fmt"
+	"text/template"
+)
+
+const unnumberedPeerFRRConfigTempl = `
+frr defaults datacenter
+hostname {{.Hostname}}
+log file /tmp/frr.log debugging
+log timestamp precision 3
+interface {{.Interface}}
+ no ipv6 nd suppress-ra
+ ipv6 nd ra-lifetime 0
+exit
+router bgp {{.ASNLocal}}
+ bgp router-id {{.RouterID}}
+ no bgp network import-check
+ no bgp ebgp-requires-policy
+ neighbor MTLB peer-group
+ neighbor MTLB passive
+ neighbor MTLB remote-as {{.ASNRemote}}
+ neighbor MTLB description LEAF-MTLB
+ neighbor MTLB bfd
+ neighbor {{.Interface}} interface peer-group MTLB
+ neighbor {{.Interface}} description k8s-node
+ address-family ipv4 unicast
+{{- range .ToAdvertiseV4 }}
+  network {{.}}
+{{- end }}
+  neighbor MTLB activate
+ exit-address-family
+address-family ipv6 unicast
+{{- range .ToAdvertiseV6 }}
+  network {{.}}
+{{- end }}
+  neighbor MTLB activate
+ exit-address-family
+exit`
+
+type RouterConfigUnnumbered struct {
+	ASNLocal      uint32
+	ASNRemote     uint32
+	Hostname      string
+	Interface     string
+	RouterID      string
+	ToAdvertiseV4 []string
+	ToAdvertiseV6 []string
+}
+
+func (rc RouterConfigUnnumbered) Config() (string, error) {
+	t, err := template.New("UnnumberedBGPTemplate").Parse(unnumberedPeerFRRConfigTempl)
+	if err != nil {
+		return "", fmt.Errorf("Failed to create template %w", err)
+	}
+	var b bytes.Buffer
+	err = t.Execute(&b, rc)
+	if err != nil {
+		return "", fmt.Errorf("failed to update template %w", err)
+	}
+
+	return b.String(), nil
+}
diff --git a/e2etest/pkg/frr/container/container.go b/e2etest/pkg/frr/container/container.go
index ce02b457..a13dc6b5 100644
--- a/e2etest/pkg/frr/container/container.go
+++ b/e2etest/pkg/frr/container/container.go
@@ -13,11 +13,13 @@ import (
 
 	"errors"
 
+	"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"
 	"go.universe.tf/e2etest/pkg/frr/consts"
 	"go.universe.tf/e2etest/pkg/ipfamily"
+	"go.universe.tf/e2etest/pkg/netdev"
 	"golang.org/x/sync/errgroup"
 	"k8s.io/apimachinery/pkg/util/wait"
 	clientset "k8s.io/client-go/kubernetes"
@@ -28,6 +30,7 @@ const (
 	frrConfigDir = "config/frr"
 	// Host network name.
 	hostNetwork = "host"
+	noNetwork   = "none"
 	// FRR container mount destination path.
 	frrMountPath = "/etc/frr"
 )
@@ -69,6 +72,10 @@ func Create(configs map[string]Config) ([]*FRR, error) {
 				"bfdd":     true,
 			}
 			c, err := start(conf)
+			if err != nil {
+				return err
+			}
+
 			if c != nil {
 				err = wait.PollImmediate(time.Second, 5*time.Minute, func() (bool, error) {
 					daemons, err := frr.Daemons(c)
@@ -198,7 +205,12 @@ func configureContainer(cfg Config, configDir string) (*FRR, error) {
 		Network:        cfg.Network,
 	}
 
-	if cfg.Network == hostNetwork {
+	switch cfg.Network {
+	case noNetwork:
+		// In Unnumbered scenario this will be replaced with LLA address of the nic
+		frr.Ipv4 = ""
+		frr.Ipv6 = ""
+	case hostNetwork:
 		if net.ParseIP(cfg.HostIPv4) == nil {
 			return nil, errors.New("Invalid hostIPv4")
 		}
@@ -208,16 +220,15 @@ func configureContainer(cfg Config, configDir string) (*FRR, error) {
 
 		frr.Ipv4 = cfg.HostIPv4
 		frr.Ipv6 = cfg.HostIPv6
-	} else {
-		err := frr.updateIPS()
-		if err != nil {
+
+		frr.RouterConfig.RouterID = frr.Ipv4
+	default:
+		if err := frr.updateIPS(); err != nil {
 			return frr, err
 		}
+		frr.RouterConfig.RouterID = frr.Ipv4
 	}
 
-	// setting routerid after calculating ips
-	frr.RouterConfig.RouterID = frr.Ipv4
-
 	err := frr.updateVolumePermissions()
 	if err != nil {
 		return frr, err
@@ -365,3 +376,36 @@ func containerIsRunning(containerName string) error {
 
 	return nil
 }
+
+func CreateP2PPeerFor(nodeContainer, dev, frrImage string) (*FRR, error) {
+	c := Config{
+		Name:    fmt.Sprintf("unnumbered-p2p-%s-%s", nodeContainer, dev),
+		Image:   frrImage,
+		Network: noNetwork,
+		Router: frrconfig.RouterConfig{
+			BGPPort: 179,
+		},
+	}
+
+	peers, err := Create(map[string]Config{"peer": c})
+	if err != nil {
+		return nil, fmt.Errorf("create container failed - %w", err)
+	}
+
+	peer := peers[0]
+
+	if err := container.WireContainers(peer.Name, nodeContainer, dev); err != nil {
+		return nil, fmt.Errorf("wire the peer failed - %w", err)
+	}
+	lla, err := netdev.LinkLocalAddressForDevice(peer, dev)
+	if err != nil {
+		return nil, err
+	}
+	// Note .Ipv4 to have LLA IPv6 is valid https://datatracker.ietf.org/doc/html/rfc8950
+	//	 $ ip route get 200.100.100.1
+	//       200.100.100.1 via inet6 fe80::2c5f:eff:fec4:cf7b dev net0
+	peer.Ipv4 = lla
+	peer.Ipv6 = lla
+
+	return peer, nil
+}
diff --git a/e2etest/pkg/frr/parse.go b/e2etest/pkg/frr/parse.go
index f97b271d..dd074c7c 100644
--- a/e2etest/pkg/frr/parse.go
+++ b/e2etest/pkg/frr/parse.go
@@ -13,7 +13,9 @@ import (
 )
 
 type Neighbor struct {
-	IP                      net.IP
+	// ID is the key that vtysh returns for the neighbor,
+	// it can be either IP or interface name if unnumbered.
+	ID                      string
 	VRF                     string
 	Connected               bool
 	LocalAS                 string
@@ -22,12 +24,14 @@ type Neighbor struct {
 	Port                    int
 	RemoteRouterID          string
 	GRInfo                  GracefulRestartInfo
+	BFDInfo                 PeerBFDInfo
 	MsgStats                MessageStats
 	ConfiguredHoldTime      int
 	ConfiguredKeepAliveTime int
 	ConfiguredConnectTime   int
 	AddressFamilies         []string
 	ConnectionsDropped      int
+	BGPNeighborAddr         string
 }
 
 type Route struct {
@@ -41,6 +45,7 @@ type Route struct {
 const bgpConnected = "Established"
 
 type FRRNeighbor struct {
+	BGPNeighborAddr              string              `json:"bgpNeighborAddr"`
 	RemoteAs                     int                 `json:"remoteAs"`
 	LocalAs                      int                 `json:"localAs"`
 	RemoteRouterID               string              `json:"remoteRouterId"`
@@ -49,6 +54,7 @@ type FRRNeighbor struct {
 	PortForeign                  int                 `json:"portForeign"`
 	MsgStats                     MessageStats        `json:"messageStats"`
 	GRInfo                       GracefulRestartInfo `json:"gracefulRestartInfo"`
+	PeerBFDInfo                  PeerBFDInfo         `json:"peerBfdInfo"`
 	VRFName                      string              `json:"vrf"`
 	ConfiguredHoldTimeMSecs      int                 `json:"bgpTimerConfiguredHoldTimeMsecs"`
 	ConfiguredKeepAliveTimeMSecs int                 `json:"bgpTimerConfiguredKeepAliveIntervalMsecs"`
@@ -59,6 +65,14 @@ type FRRNeighbor struct {
 	ConnectionsDropped int `json:"connectionsDropped"`
 }
 
+type PeerBFDInfo struct {
+	Type             string `json:"type"`
+	DetectMultiplier int    `json:"detectMultiplier"`
+	RxMinInterval    int    `json:"rxMinInterval"`
+	TxMinInterval    int    `json:"txMinInterval"`
+	Status           string `json:"status"`
+	LastUpdate       string `json:"lastUpdate"`
+}
 type GracefulRestartInfo struct {
 	EndOfRibSend struct {
 		Ipv4Unicast bool `json:"ipv4Unicast"`
@@ -110,6 +124,7 @@ type FRRRoute struct {
 	PeerID    string `json:"peerId"`
 	LocalPref uint32 `json:"locPrf"`
 	Origin    string `json:"origin"`
+	PathFrom  string `json:"pathFrom"`
 	Nexthops  []struct {
 		IP    string `json:"ip"`
 		Scope string `json:"scope"`
@@ -156,10 +171,6 @@ func ParseNeighbour(vtyshRes string) (*Neighbor, error) {
 		return nil, errors.New("no peers were returned")
 	}
 	for k, n := range res {
-		ip := net.ParseIP(k)
-		if ip == nil {
-			return nil, fmt.Errorf("failed to parse %s as ip", ip)
-		}
 		connected := true
 		if n.BgpState != bgpConnected {
 			connected = false
@@ -169,7 +180,7 @@ func ParseNeighbour(vtyshRes string) (*Neighbor, error) {
 			prefixSent += s.SentPrefixCounter
 		}
 		return &Neighbor{
-			IP:                      ip,
+			ID:                      k,
 			Connected:               connected,
 			LocalAS:                 strconv.Itoa(n.LocalAs),
 			RemoteAS:                strconv.Itoa(n.RemoteAs),
@@ -180,6 +191,7 @@ func ParseNeighbour(vtyshRes string) (*Neighbor, error) {
 			ConfiguredKeepAliveTime: n.ConfiguredKeepAliveTimeMSecs,
 			ConfiguredHoldTime:      n.ConfiguredHoldTimeMSecs,
 			ConnectionsDropped:      n.ConnectionsDropped,
+			BGPNeighborAddr:         n.BGPNeighborAddr,
 		}, nil
 	}
 	return nil, errors.New("no peers were returned")
@@ -196,10 +208,6 @@ func ParseNeighbours(vtyshRes string) ([]*Neighbor, error) {
 
 	res := make([]*Neighbor, 0)
 	for k, n := range toParse {
-		ip := net.ParseIP(k)
-		if ip == nil {
-			return nil, fmt.Errorf("failed to parse %s as ip", ip)
-		}
 		connected := true
 		if n.BgpState != bgpConnected {
 			connected = false
@@ -211,7 +219,7 @@ func ParseNeighbours(vtyshRes string) ([]*Neighbor, error) {
 			addressFamilies = append(addressFamilies, family)
 		}
 		res = append(res, &Neighbor{
-			IP:                      ip,
+			ID:                      k,
 			Connected:               connected,
 			LocalAS:                 strconv.Itoa(n.LocalAs),
 			RemoteAS:                strconv.Itoa(n.RemoteAs),
@@ -220,11 +228,13 @@ func ParseNeighbours(vtyshRes string) ([]*Neighbor, error) {
 			RemoteRouterID:          n.RemoteRouterID,
 			MsgStats:                n.MsgStats,
 			GRInfo:                  n.GRInfo,
+			BFDInfo:                 n.PeerBFDInfo,
 			ConfiguredKeepAliveTime: n.ConfiguredKeepAliveTimeMSecs,
 			ConfiguredHoldTime:      n.ConfiguredHoldTimeMSecs,
 			ConfiguredConnectTime:   n.ConnectRetryTimer,
 			AddressFamilies:         addressFamilies,
 			ConnectionsDropped:      n.ConnectionsDropped,
+			BGPNeighborAddr:         n.BGPNeighborAddr,
 		})
 	}
 	return res, nil
diff --git a/e2etest/pkg/frr/parse_test.go b/e2etest/pkg/frr/parse_test.go
index 128608ab..9804477f 100644
--- a/e2etest/pkg/frr/parse_test.go
+++ b/e2etest/pkg/frr/parse_test.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"net"
 	"sort"
+	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -152,8 +153,8 @@ func TestNeighbour(t *testing.T) {
 			if err != nil {
 				t.Fatal("Failed to parse ", err)
 			}
-			if !n.IP.Equal(net.ParseIP(tt.neighborIP)) {
-				t.Fatal("Expected neighbour ip", tt.neighborIP, "got", n.IP.String())
+			if n.ID != tt.neighborIP {
+				t.Fatal("Expected neighbour ip", tt.neighborIP, "got", n.ID)
 			}
 			if n.RemoteAS != tt.remoteAS {
 				t.Fatal("Expected remote as", tt.remoteAS, "got", n.RemoteAS)
@@ -509,16 +510,16 @@ func TestNeighbours(t *testing.T) {
 		t.Fatalf("Expected 4 neighbours, got %d", len(nn))
 	}
 	sort.Slice(nn, func(i, j int) bool {
-		return (bytes.Compare(nn[i].IP, nn[j].IP) < 0)
+		return (strings.Compare(nn[i].ID, nn[j].ID) < 0)
 	})
 
-	if !nn[0].IP.Equal(net.ParseIP("172.18.0.2")) {
+	if nn[0].ID != "172.18.0.2" {
 		t.Fatal("neighbour ip not matching")
 	}
-	if !nn[1].IP.Equal(net.ParseIP("172.18.0.3")) {
+	if nn[1].ID != "172.18.0.3" {
 		t.Fatal("neighbour ip not matching")
 	}
-	if !nn[2].IP.Equal(net.ParseIP("172.18.0.4")) {
+	if nn[2].ID != "172.18.0.4" {
 		t.Fatal("neighbour ip not matching")
 	}
 
diff --git a/e2etest/pkg/frr/validation.go b/e2etest/pkg/frr/validation.go
index 5203cb8c..46c5d0e9 100644
--- a/e2etest/pkg/frr/validation.go
+++ b/e2etest/pkg/frr/validation.go
@@ -26,13 +26,13 @@ func NeighborsMatchNodes(nodes []v1.Node, neighbors NeighborsMap, ipFamily ipfam
 		nodesIPs[ip] = struct{}{}
 	}
 	for _, n := range neighbors {
-		if _, ok := nodesIPs[n.IP.String()]; !ok { // skipping neighbors that are not nodes
+		if _, ok := nodesIPs[n.ID]; !ok { // skipping neighbors that are not nodes
 			continue
 		}
 		if !n.Connected {
-			return fmt.Errorf("node %s BGP session not established", n.IP.String())
+			return fmt.Errorf("node %s BGP session not established", n.ID)
 		}
-		delete(nodesIPs, n.IP.String())
+		delete(nodesIPs, n.ID)
 	}
 	if len(nodesIPs) != 0 { // some leftover, meaning more nodes than neighbors
 		return fmt.Errorf("vrfName=%s ,nodeIPS=%v vs nextHops=%v\n", vrfName, ips, neighbors)
diff --git a/e2etest/pkg/netdev/netdev.go b/e2etest/pkg/netdev/netdev.go
index f18f7f51..cb648bd1 100644
--- a/e2etest/pkg/netdev/netdev.go
+++ b/e2etest/pkg/netdev/netdev.go
@@ -166,6 +166,27 @@ func AddressesForDevice(exec executor.Executor, dev string) (*Addresses, error)
 	return &res, nil
 }
 
+func LinkLocalAddressForDevice(exec executor.Executor, dev string) (string, error) {
+	jsonIPOutput, err := exec.Exec("ip", "-j", "-6", "addr", "show", "dev", dev, "scope", "link")
+	if err != nil {
+		return "", fmt.Errorf("failed to retrieve interface address %s, %w :%s", dev, err, jsonIPOutput)
+	}
+
+	var intf []interfaceAddress
+	err = json.Unmarshal([]byte(jsonIPOutput), &intf)
+	if err != nil {
+		return "", err
+	}
+	if len(intf) != 1 {
+		return "", fmt.Errorf("expected one single interface for %s, got %d", dev, len(intf))
+	}
+	for _, addr := range intf[0].AddrInfo {
+		return addr.Local, nil
+	}
+
+	return "", nil
+}
+
 func findInterfaceWithAddresses(jsonIPOutput string, ipv4Address, ipv6Address string) (string, error) {
 	var interfaces []interfaceAddress
 	err := json.Unmarshal([]byte(jsonIPOutput), &interfaces)
diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go
index f6b21d43..cd4ae44c 100644
--- a/internal/config/validation_test.go
+++ b/internal/config/validation_test.go
@@ -70,6 +70,104 @@ func TestValidate(t *testing.T) {
 			},
 			mustFail: true,
 		},
+		{
+			desc: "v6 address but pool not selected",
+			config: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "foo",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{"2001:db8::/64"},
+						},
+					},
+				},
+				BGPAdvs: []v1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "foo",
+						},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							IPAddressPools: []string{"bar"},
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "v6 address and selected",
+			config: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "foo",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{"2001:db8::/64"},
+						},
+					},
+				},
+				BGPAdvs: []v1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "bar",
+						},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							IPAddressPools: []string{"foo1"},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "bar",
+						},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							IPAddressPools: []string{"foo"},
+						},
+					},
+				},
+			},
+			mustFail: true,
+		},
+		{
+			desc: "v6 address and selected by labels",
+			config: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name:   "foo",
+							Labels: map[string]string{"key": "value"},
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{"2001:db8::/64"},
+						},
+					},
+				},
+				BGPAdvs: []v1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "bar",
+						},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							IPAddressPools: []string{"foo1"},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "bar",
+						},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							IPAddressPoolSelectors: []v1.LabelSelector{
+								{
+									MatchLabels: map[string]string{"key": "value"},
+								},
+							},
+						},
+					},
+				},
+			},
+			mustFail: true,
+		},
 		{
 			desc: "enable BGP GracefulRestart",
 			config: ClusterResources{