Repository: metallb/metallb
Pull request #2669 (2025-01-28 17:35:07)

Chore(deps): bump github.com/golang/glog from 1.0.0 to 1.2.4 in /e2etest
Bumps [github.com/golang/glog](https://github.com/golang/glog) from 1.0.0 to 1.2.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/golang/glog/releases">github.com/golang/glog's releases</a>.</em></p>
<blockquote>
<h2>v1.2.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Fail if log file already exists by <a href="https://github.com/chressie"><code>@​chressie</code></a> in <a href="https://redirect.github.com/golang/glog/pull/74">golang/glog#74</a>:
<ul>
<li>glog: Don't try to create/rotate a given syncBuffer twice in the same second</li>
<li>glog: introduce createInDir function as in internal version</li>
<li>glog: have createInDir fail if the file already exists</li>
</ul>
</li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/golang/glog/compare/v1.2.3...v1.2.4">https://github.com/golang/glog/compare/v1.2.3...v1.2.4</a></p>
<h2>v1.2.3</h2>
<h2>What's Changed</h2>
<ul>
<li>glog: check that stderr is valid before using it by default by <a href="https://github.com/chressie"><code>@​chressie</code></a> in <a href="https://redirect.github.com/golang/glog/pull/72">golang/glog#72</a></li>
<li>glog: fix typo by <a href="https://github.com/chressie"><code>@​chressie</code></a> in <a href="https://redirect.github.com/golang/glog/pull/73">golang/glog#73</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/golang/glog/compare/v1.2.2...v1.2.3">https://github.com/golang/glog/compare/v1.2.2...v1.2.3</a></p>
<h2>v1.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li>glog: avoid calling user.Current() on windows by <a href="https://github.com/bentekkie"><code>@​bentekkie</code></a> in <a href="https://redirect.github.com/golang/glog/pull/69">golang/glog#69</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/golang/glog/compare/v1.2.1...v1.2.2">https://github.com/golang/glog/compare/v1.2.1...v1.2.2</a></p>
<h2>v1.2.1</h2>
<h2>What's Changed</h2>
<ul>
<li>glog: don't hold mutex when sync'ing by <a href="https://github.com/chressie"><code>@​chressie</code></a> in <a href="https://redirect.github.com/golang/glog/pull/68">golang/glog#68</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/golang/glog/compare/v1.2.0...v1.2.1">https://github.com/golang/glog/compare/v1.2.0...v1.2.1</a></p>
<h2>v1.2.0</h2>
<h2>What's Changed</h2>
<ul>
<li>glog: add context variants and logsink tests by <a href="https://github.com/chressie"><code>@​chressie</code></a> in <a href="https://redirect.github.com/golang/glog/pull/66">golang/glog#66</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/golang/glog/compare/v1.1.2...v1.2.0">https://github.com/golang/glog/compare/v1.1.2...v1.2.0</a></p>
<h2>v1.1.2</h2>
<p>Bugfix release.</p>
<h2>What's Changed</h2>
<ul>
<li>glog: populate symlinks -log_link directory by <a href="https://github.com/chressie"><code>@​chressie</code></a> in <a href="https://redirect.github.com/golang/glog/pull/64">golang/glog#64</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/golang/glog/compare/v1.1.1...v1.1.2">https://github.com/golang/glog/compare/v1.1.1...v1.1.2</a></p>
<h2>v1.1.1</h2>
<p>Bugfixes since the larger v1.1.0, which have been addressed.</p>
<h2>v1.1.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/glog/commit/a0e3c40a0ed0cecc58c84e7684d9ce55a54044ee"><code>a0e3c40</code></a> glog: have createInDir fail if the file already exists</li>
<li><a href="https://github.com/golang/glog/commit/7139da234346c23dba05a8c588284c379b9c0bf8"><code>7139da2</code></a> glog: introduce createInDir function as in internal version</li>
<li><a href="https://github.com/golang/glog/commit/dd58629ba6ced28322bcb149ed86016fb1ec9a5b"><code>dd58629</code></a> glog: Don't try to create/rotate a given syncBuffer twice in the same second</li>
<li><a href="https://github.com/golang/glog/commit/04dbec0df8d03d5c187beaf5ce8e6c58e9fc82b5"><code>04dbec0</code></a> glog: fix typo (<a href="https://redirect.github.com/golang/glog/issues/73">#73</a>)</li>
<li><a href="https://github.com/golang/glog/commit/459cf3b8151dcfd8aa971077774eaf0c804119e4"><code>459cf3b</code></a> glog: check that stderr is valid before using it by default (<a href="https://redirect.github.com/golang/glog/issues/72">#72</a>)</li>
<li><a href="https://github.com/golang/glog/commit/97303146a4ffecf364d4300e07fca855d0062c43"><code>9730314</code></a> glog: avoid calling user.Current() on windows (<a href="https://redirect.github.com/golang/glog/issues/69">#69</a>)</li>
<li><a href="https://github.com/golang/glog/commit/861d094757551d2f65d1003f0ec93bc92b254a5a"><code>861d094</code></a> glog: don't hold mutex when sync'ing (<a href="https://redirect.github.com/golang/glog/issues/68">#68</a>)</li>
<li><a href="https://github.com/golang/glog/commit/b256bbe5d3f846c1d1ce73030c10973378e04cf0"><code>b256bbe</code></a> glog: add context variants to most log functions</li>
<li><a href="https://github.com/golang/glog/commit/bcf318487a966b61bca6189643b72648fa132e6e"><code>bcf3184</code></a> glog/internal/logsink: add logsink tests</li>
<li><a href="https://github.com/golang/glog/commit/6b9d7f7df28d318a28dde3adb8a5e9ae9bf1abaf"><code>6b9d7f7</code></a> glog: populate symlinks -log_link directory (<a href="https://redirect.github.com/golang/glog/issues/64">#64</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/golang/glog/compare/v1.0.0...v1.2.4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/golang/glog&package-manager=go_modules&previous-version=1.0.0&new-version=1.2.4)](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)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/metallb/metallb/network/alerts).

</details>


Patch:
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 4d8af036..e195c14f 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -60,7 +60,7 @@ jobs:
         image: [speaker, controller, configmaptocrs]
     steps:
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3.2.0
+        uses: docker/setup-qemu-action@v3.4.0
 
       - name: Install Cosign
         uses: sigstore/cosign-installer@main
diff --git a/api/v1beta1/ipaddresspool_types.go b/api/v1beta1/ipaddresspool_types.go
index f954c725..8e9c5506 100644
--- a/api/v1beta1/ipaddresspool_types.go
+++ b/api/v1beta1/ipaddresspool_types.go
@@ -67,8 +67,17 @@ type ServiceAllocation struct {
 
 // IPAddressPoolStatus defines the observed state of IPAddressPool.
 type IPAddressPoolStatus struct {
-	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
-	// Important: Run "make" to regenerate code after modifying this file
+	// AssignedIPv4 is the number of assigned IPv4 addresses.
+	AssignedIPv4 int64 `json:"assignedIPv4"`
+
+	// AssignedIPv6 is the number of assigned IPv6 addresses.
+	AssignedIPv6 int64 `json:"assignedIPv6"`
+
+	// AvailableIPv4 is the number of available IPv4 addresses.
+	AvailableIPv4 int64 `json:"availableIPv4"`
+
+	// AvailableIPv6 is the number of available IPv6 addresses.
+	AvailableIPv6 int64 `json:"availableIPv6"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta2/bgppeer_types.go b/api/v1beta2/bgppeer_types.go
index 3269439d..d984f5ee 100644
--- a/api/v1beta2/bgppeer_types.go
+++ b/api/v1beta2/bgppeer_types.go
@@ -45,7 +45,16 @@ type BGPPeerSpec struct {
 	DynamicASN DynamicASNMode `json:"dynamicASN,omitempty"`
 
 	// Address to dial when establishing the session.
-	Address string `json:"peerAddress"`
+	// +optional
+	Address string `json:"peerAddress,omitempty"`
+
+	// Interface is the node interface over which the unnumbered BGP peering will
+	// be established. No API validation takes place as that string value
+	// represents an interface name on the host and if user provides an invalid
+	// value, only the actual BGP session will not be established.
+	// Address and Interface are mutually exclusive and one of them must be specified.
+	// +optional
+	Interface string `json:"interface,omitempty"`
 
 	// Source address to use when establishing the session.
 	// +optional
@@ -53,7 +62,7 @@ type BGPPeerSpec struct {
 
 	// Port to dial when establishing the session.
 	// +optional
-	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:validation:Minimum=1
 	// +kubebuilder:validation:Maximum=16384
 	// +kubebuilder:default:=179
 	Port uint16 `json:"peerPort,omitempty"`
diff --git a/charts/metallb/charts/crds/templates/crds.yaml b/charts/metallb/charts/crds/templates/crds.yaml
index 31468045..12bf139f 100644
--- a/charts/metallb/charts/crds/templates/crds.yaml
+++ b/charts/metallb/charts/crds/templates/crds.yaml
@@ -336,7 +336,6 @@ spec:
     strategy: Webhook
     webhook:
       clientConfig:
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           name: metallb-webhook-service
           namespace: {{ .Release.Namespace }}
@@ -555,6 +554,14 @@ spec:
                 holdTime:
                   description: Requested BGP hold time, per RFC4271.
                   type: string
+                interface:
+                  description: |-
+                    Interface is the node interface over which the unnumbered BGP peering will
+                    be established. No API validation takes place as that string value
+                    represents an interface name on the host and if user provides an invalid
+                    value, only the actual BGP session will not be established.
+                    Address and Interface are mutually exclusive and one of them must be specified.
+                  type: string
                 keepaliveTime:
                   description: Requested BGP keepalive time, per RFC4271.
                   type: string
@@ -649,7 +656,7 @@ spec:
                   default: 179
                   description: Port to dial when establishing the session.
                   maximum: 16384
-                  minimum: 0
+                  minimum: 1
                   type: integer
                 routerID:
                   description: BGP router ID to advertise to the peer
@@ -664,7 +671,6 @@ spec:
                   type: string
               required:
                 - myASN
-                - peerAddress
               type: object
             status:
               description: BGPPeerStatus defines the observed state of Peer.
@@ -941,6 +947,28 @@ spec:
               type: object
             status:
               description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+              properties:
+                assignedIPv4:
+                  description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                  format: int64
+                  type: integer
+                assignedIPv6:
+                  description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                  format: int64
+                  type: integer
+                availableIPv4:
+                  description: AvailableIPv4 is the number of available IPv4 addresses.
+                  format: int64
+                  type: integer
+                availableIPv6:
+                  description: AvailableIPv6 is the number of available IPv6 addresses.
+                  format: int64
+                  type: integer
+              required:
+                - assignedIPv4
+                - assignedIPv6
+                - availableIPv4
+                - availableIPv6
               type: object
           required:
             - spec
diff --git a/charts/metallb/templates/rbac.yaml b/charts/metallb/templates/rbac.yaml
index b174cba3..11d2ae04 100644
--- a/charts/metallb/templates/rbac.yaml
+++ b/charts/metallb/templates/rbac.yaml
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["metallb.io"]
   resources: ["ipaddresspools"]
   verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["ipaddresspools/status"]
+  verbs: ["update"]
 - apiGroups: ["metallb.io"]
   resources: ["bgppeers"]
   verbs: ["get", "list"]
diff --git a/config/crd/bases/metallb.io_bgppeers.yaml b/config/crd/bases/metallb.io_bgppeers.yaml
index 1b740778..3fd406d0 100644
--- a/config/crd/bases/metallb.io_bgppeers.yaml
+++ b/config/crd/bases/metallb.io_bgppeers.yaml
@@ -224,6 +224,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -323,7 +331,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -338,7 +346,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
diff --git a/config/crd/bases/metallb.io_ipaddresspools.yaml b/config/crd/bases/metallb.io_ipaddresspools.yaml
index 59c3475c..c3a7d7e4 100644
--- a/config/crd/bases/metallb.io_ipaddresspools.yaml
+++ b/config/crd/bases/metallb.io_ipaddresspools.yaml
@@ -206,6 +206,28 @@ spec:
             type: object
           status:
             description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            properties:
+              assignedIPv4:
+                description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                format: int64
+                type: integer
+              assignedIPv6:
+                description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                format: int64
+                type: integer
+              availableIPv4:
+                description: AvailableIPv4 is the number of available IPv4 addresses.
+                format: int64
+                type: integer
+              availableIPv6:
+                description: AvailableIPv6 is the number of available IPv6 addresses.
+                format: int64
+                type: integer
+            required:
+            - assignedIPv4
+            - assignedIPv6
+            - availableIPv4
+            - availableIPv6
             type: object
         required:
         - spec
diff --git a/config/crd/patches/crd-conversion-patch-bgppeers.yaml b/config/crd/patches/crd-conversion-patch-bgppeers.yaml
index dc9c88ba..d917f6bf 100644
--- a/config/crd/patches/crd-conversion-patch-bgppeers.yaml
+++ b/config/crd/patches/crd-conversion-patch-bgppeers.yaml
@@ -8,9 +8,6 @@ spec:
     webhook:
       conversionReviewVersions: ["v1beta1", "v1beta2"]
       clientConfig:
-        # this is a valid pem format, otherwise the apiserver will reject the deletion of the crds
-        # with "unable to parse bytes as PEM block", The controller will patch it with the right content after it starts
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           namespace: metallb-system
           name: metallb-webhook-service
diff --git a/config/manifests/metallb-frr-k8s-prometheus.yaml b/config/manifests/metallb-frr-k8s-prometheus.yaml
index 9b7c9837..c5c7d2ea 100644
--- a/config/manifests/metallb-frr-k8s-prometheus.yaml
+++ b/config/manifests/metallb-frr-k8s-prometheus.yaml
@@ -356,7 +356,6 @@ spec:
     strategy: Webhook
     webhook:
       clientConfig:
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           name: metallb-webhook-service
           namespace: metallb-system
@@ -582,6 +581,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +688,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +703,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
@@ -1523,6 +1529,28 @@ spec:
             type: object
           status:
             description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            properties:
+              assignedIPv4:
+                description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                format: int64
+                type: integer
+              assignedIPv6:
+                description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                format: int64
+                type: integer
+              availableIPv4:
+                description: AvailableIPv4 is the number of available IPv4 addresses.
+                format: int64
+                type: integer
+              availableIPv6:
+                description: AvailableIPv6 is the number of available IPv6 addresses.
+                format: int64
+                type: integer
+            required:
+            - assignedIPv4
+            - assignedIPv6
+            - availableIPv4
+            - availableIPv6
             type: object
         required:
         - spec
@@ -1983,6 +2011,12 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools/status
+  verbs:
+  - update
 - apiGroups:
   - metallb.io
   resources:
diff --git a/config/manifests/metallb-frr-k8s.yaml b/config/manifests/metallb-frr-k8s.yaml
index 943cc597..4ab2be95 100644
--- a/config/manifests/metallb-frr-k8s.yaml
+++ b/config/manifests/metallb-frr-k8s.yaml
@@ -356,7 +356,6 @@ spec:
     strategy: Webhook
     webhook:
       clientConfig:
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           name: metallb-webhook-service
           namespace: metallb-system
@@ -582,6 +581,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +688,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +703,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
@@ -1523,6 +1529,28 @@ spec:
             type: object
           status:
             description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            properties:
+              assignedIPv4:
+                description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                format: int64
+                type: integer
+              assignedIPv6:
+                description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                format: int64
+                type: integer
+              availableIPv4:
+                description: AvailableIPv4 is the number of available IPv4 addresses.
+                format: int64
+                type: integer
+              availableIPv6:
+                description: AvailableIPv6 is the number of available IPv6 addresses.
+                format: int64
+                type: integer
+            required:
+            - assignedIPv4
+            - assignedIPv6
+            - availableIPv4
+            - availableIPv6
             type: object
         required:
         - spec
@@ -1983,6 +2011,12 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools/status
+  verbs:
+  - update
 - apiGroups:
   - metallb.io
   resources:
diff --git a/config/manifests/metallb-frr-prometheus.yaml b/config/manifests/metallb-frr-prometheus.yaml
index b86b1a63..f294d0cd 100644
--- a/config/manifests/metallb-frr-prometheus.yaml
+++ b/config/manifests/metallb-frr-prometheus.yaml
@@ -356,7 +356,6 @@ spec:
     strategy: Webhook
     webhook:
       clientConfig:
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           name: metallb-webhook-service
           namespace: metallb-system
@@ -582,6 +581,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +688,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +703,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
@@ -979,6 +985,28 @@ spec:
             type: object
           status:
             description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            properties:
+              assignedIPv4:
+                description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                format: int64
+                type: integer
+              assignedIPv6:
+                description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                format: int64
+                type: integer
+              availableIPv4:
+                description: AvailableIPv4 is the number of available IPv4 addresses.
+                format: int64
+                type: integer
+              availableIPv6:
+                description: AvailableIPv6 is the number of available IPv6 addresses.
+                format: int64
+                type: integer
+            required:
+            - assignedIPv4
+            - assignedIPv6
+            - availableIPv4
+            - availableIPv6
             type: object
         required:
         - spec
@@ -1426,6 +1454,12 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools/status
+  verbs:
+  - update
 - apiGroups:
   - metallb.io
   resources:
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index bf457f05..7d1f88c3 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -356,7 +356,6 @@ spec:
     strategy: Webhook
     webhook:
       clientConfig:
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           name: metallb-webhook-service
           namespace: metallb-system
@@ -582,6 +581,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +688,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +703,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
@@ -979,6 +985,28 @@ spec:
             type: object
           status:
             description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            properties:
+              assignedIPv4:
+                description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                format: int64
+                type: integer
+              assignedIPv6:
+                description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                format: int64
+                type: integer
+              availableIPv4:
+                description: AvailableIPv4 is the number of available IPv4 addresses.
+                format: int64
+                type: integer
+              availableIPv6:
+                description: AvailableIPv6 is the number of available IPv6 addresses.
+                format: int64
+                type: integer
+            required:
+            - assignedIPv4
+            - assignedIPv6
+            - availableIPv4
+            - availableIPv6
             type: object
         required:
         - spec
@@ -1426,6 +1454,12 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools/status
+  verbs:
+  - update
 - apiGroups:
   - metallb.io
   resources:
diff --git a/config/manifests/metallb-native-prometheus.yaml b/config/manifests/metallb-native-prometheus.yaml
index 54cee476..5bfd0d62 100644
--- a/config/manifests/metallb-native-prometheus.yaml
+++ b/config/manifests/metallb-native-prometheus.yaml
@@ -356,7 +356,6 @@ spec:
     strategy: Webhook
     webhook:
       clientConfig:
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           name: metallb-webhook-service
           namespace: metallb-system
@@ -582,6 +581,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +688,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +703,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
@@ -979,6 +985,28 @@ spec:
             type: object
           status:
             description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            properties:
+              assignedIPv4:
+                description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                format: int64
+                type: integer
+              assignedIPv6:
+                description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                format: int64
+                type: integer
+              availableIPv4:
+                description: AvailableIPv4 is the number of available IPv4 addresses.
+                format: int64
+                type: integer
+              availableIPv6:
+                description: AvailableIPv6 is the number of available IPv6 addresses.
+                format: int64
+                type: integer
+            required:
+            - assignedIPv4
+            - assignedIPv6
+            - availableIPv4
+            - availableIPv6
             type: object
         required:
         - spec
@@ -1426,6 +1454,12 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools/status
+  verbs:
+  - update
 - apiGroups:
   - metallb.io
   resources:
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index 92768781..6888401a 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -356,7 +356,6 @@ spec:
     strategy: Webhook
     webhook:
       clientConfig:
-        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
         service:
           name: metallb-webhook-service
           namespace: metallb-system
@@ -582,6 +581,14 @@ spec:
               holdTime:
                 description: Requested BGP hold time, per RFC4271.
                 type: string
+              interface:
+                description: |-
+                  Interface is the node interface over which the unnumbered BGP peering will
+                  be established. No API validation takes place as that string value
+                  represents an interface name on the host and if user provides an invalid
+                  value, only the actual BGP session will not be established.
+                  Address and Interface are mutually exclusive and one of them must be specified.
+                type: string
               keepaliveTime:
                 description: Requested BGP keepalive time, per RFC4271.
                 type: string
@@ -681,7 +688,7 @@ spec:
                 default: 179
                 description: Port to dial when establishing the session.
                 maximum: 16384
-                minimum: 0
+                minimum: 1
                 type: integer
               routerID:
                 description: BGP router ID to advertise to the peer
@@ -696,7 +703,6 @@ spec:
                 type: string
             required:
             - myASN
-            - peerAddress
             type: object
           status:
             description: BGPPeerStatus defines the observed state of Peer.
@@ -979,6 +985,28 @@ spec:
             type: object
           status:
             description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            properties:
+              assignedIPv4:
+                description: AssignedIPv4 is the number of assigned IPv4 addresses.
+                format: int64
+                type: integer
+              assignedIPv6:
+                description: AssignedIPv6 is the number of assigned IPv6 addresses.
+                format: int64
+                type: integer
+              availableIPv4:
+                description: AvailableIPv4 is the number of available IPv4 addresses.
+                format: int64
+                type: integer
+              availableIPv6:
+                description: AvailableIPv6 is the number of available IPv6 addresses.
+                format: int64
+                type: integer
+            required:
+            - assignedIPv4
+            - assignedIPv6
+            - availableIPv4
+            - availableIPv6
             type: object
         required:
         - spec
@@ -1426,6 +1454,12 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - metallb.io
+  resources:
+  - ipaddresspools/status
+  verbs:
+  - update
 - apiGroups:
   - metallb.io
   resources:
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 1fc501d1..832474bf 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -281,6 +281,12 @@ rules:
       - get
       - list
       - watch
+  - apiGroups:
+      - metallb.io
+    resources:
+      - ipaddresspools/status
+    verbs:
+      - update
   - apiGroups:
       - metallb.io
     resources:
diff --git a/config/webhook/patches/webhook_conversion.yaml b/config/webhook/patches/webhook_conversion.yaml
deleted file mode 100644
index b1788c87..00000000
--- a/config/webhook/patches/webhook_conversion.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
-  name: bgppeers.metallb.io
-spec:
-  conversion:
-    strategy: Webhook
-    webhook:
-      conversionReviewVersions: ["v1beta1", "v1beta2"]
-      clientConfig:
-        # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
-        # but we're going to set it later using the cert-controller.
-        caBundle: Cg==
-        service:
-          namespace: metallb-system
-          name: metallb-webhook-service
-          path: /convert
diff --git a/controller/main.go b/controller/main.go
index 3cf78ebb..bfcb9588 100644
--- a/controller/main.go
+++ b/controller/main.go
@@ -27,6 +27,7 @@ import (
 	"go.universe.tf/metallb/internal/k8s/controllers"
 	"go.universe.tf/metallb/internal/logging"
 	"go.universe.tf/metallb/internal/version"
+	"sigs.k8s.io/controller-runtime/pkg/event"
 
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
@@ -203,8 +204,11 @@ func main() {
 		*namespace = string(bs)
 	}
 
+	poolStatusChan := make(chan event.GenericEvent)
 	c := &controller{
-		ips: allocator.New(),
+		ips: allocator.New(func(name string) {
+			poolStatusChan <- controllers.NewPoolStatusEvent(*namespace, name)
+		}),
 	}
 
 	bgpType, present := os.LookupEnv("METALLB_BGP_TYPE")
@@ -235,6 +239,8 @@ func main() {
 		CertDir:             *certDir,
 		CertServiceName:     *certServiceName,
 		LoadBalancerClass:   *loadBalancerClass,
+		PoolStatusChan:      poolStatusChan,
+		PoolCountersFetcher: c.ips.CountersForPool,
 	}
 	switch *webhookMode {
 	case "enabled":
diff --git a/go.mod b/go.mod
index e58a8558..8fdd9863 100644
--- a/go.mod
+++ b/go.mod
@@ -12,8 +12,8 @@ require (
 	github.com/mdlayher/ndp v0.0.0-20200602162440-17ab9e3e5567
 	github.com/metallb/frr-k8s v0.0.17
 	github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721
-	github.com/onsi/ginkgo/v2 v2.22.0
-	github.com/onsi/gomega v1.36.1
+	github.com/onsi/ginkgo/v2 v2.22.2
+	github.com/onsi/gomega v1.36.2
 	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.20.5
@@ -23,7 +23,7 @@ require (
 	k8s.io/apiextensions-apiserver v0.32.1
 	k8s.io/apimachinery v0.32.2
 	k8s.io/client-go v0.32.2
-	k8s.io/component-base v0.32.1
+	k8s.io/component-base v0.32.2
 	k8s.io/klog v1.0.0
 	k8s.io/utils v0.0.0-20241210054802-24370beab758
 	sigs.k8s.io/controller-runtime v0.20.1
@@ -60,7 +60,7 @@ require (
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
+	github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // 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
@@ -103,14 +103,14 @@ require (
 	gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/mod v0.21.0 // indirect
+	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/net v0.35.0 // indirect
 	golang.org/x/oauth2 v0.26.0 // indirect
 	golang.org/x/sync v0.11.0 // indirect
 	golang.org/x/term v0.29.0 // indirect
 	golang.org/x/text v0.22.0 // indirect
 	golang.org/x/time v0.10.0 // indirect
-	golang.org/x/tools v0.26.0 // indirect
+	golang.org/x/tools v0.28.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/protobuf v1.36.5 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
diff --git a/go.sum b/go.sum
index 41f35c9c..ff987363 100644
--- a/go.sum
+++ b/go.sum
@@ -96,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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
-github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
+github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/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=
@@ -185,10 +185,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.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/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
+github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
+github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
+github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
 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=
@@ -278,8 +278,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.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=
@@ -333,8 +333,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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
-golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
+golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
+golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
 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=
@@ -371,8 +371,8 @@ k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
 k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
 k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
 k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
-k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk=
-k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w=
+k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU=
+k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0=
 k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
diff --git a/internal/allocator/allocator.go b/internal/allocator/allocator.go
index dfc68b3e..971ff70c 100644
--- a/internal/allocator/allocator.go
+++ b/internal/allocator/allocator.go
@@ -9,6 +9,7 @@ import (
 	"net"
 	"sort"
 	"strings"
+	"sync"
 
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/ipfamily"
@@ -29,6 +30,10 @@ type Allocator struct {
 	poolIPsInUse    map[string]map[string]int  // poolName -> ip.String() -> number of users
 	poolIPV4InUse   map[string]map[string]int  // poolName -> ipv4.String() -> number of users
 	poolIPV6InUse   map[string]map[string]int  // poolName -> ipv6.String() -> number of users
+
+	poolToCounters          map[string]PoolCounters // poolName -> Counters
+	countersMutex           sync.RWMutex
+	countersChangedCallback func(string)
 }
 
 // Port represents one port in use by a service.
@@ -54,30 +59,49 @@ type alloc struct {
 	key
 }
 
+type PoolCounters struct {
+	AssignedIPv4  int64
+	AssignedIPv6  int64
+	AvailableIPv4 int64
+	AvailableIPv6 int64
+}
+
 // New returns an Allocator managing no pools.
-func New() *Allocator {
+func New(countersCallback func(string)) *Allocator {
 	return &Allocator{
 		pools: &config.Pools{ByName: map[string]*config.Pool{}},
 
-		allocated:       map[string]*alloc{},
-		sharingKeyForIP: map[string]*key{},
-		portsInUse:      map[string]map[Port]string{},
-		servicesOnIP:    map[string]map[string]bool{},
-		poolIPsInUse:    map[string]map[string]int{},
-		poolIPV4InUse:   map[string]map[string]int{},
-		poolIPV6InUse:   map[string]map[string]int{},
+		allocated:               map[string]*alloc{},
+		sharingKeyForIP:         map[string]*key{},
+		portsInUse:              map[string]map[Port]string{},
+		servicesOnIP:            map[string]map[string]bool{},
+		poolIPsInUse:            map[string]map[string]int{},
+		poolIPV4InUse:           map[string]map[string]int{},
+		poolIPV6InUse:           map[string]map[string]int{},
+		poolToCounters:          map[string]PoolCounters{},
+		countersMutex:           sync.RWMutex{},
+		countersChangedCallback: countersCallback,
 	}
 }
 
 // SetPools updates the set of address pools that the allocator owns.
 func (a *Allocator) SetPools(pools *config.Pools) {
+	refreshPools := []string{}
+	defer func() {
+		for _, p := range refreshPools {
+			a.countersChangedCallback(p)
+		}
+	}()
+
+	a.countersMutex.Lock()
 	for n := range a.pools.ByName {
 		if pools.ByName[n] == nil {
-			stats.poolCapacity.DeleteLabelValues(n)
-			stats.poolActive.DeleteLabelValues(n)
-			stats.poolAllocated.DeleteLabelValues(n)
+			deleteStatsFor(n)
+			delete(a.poolToCounters, n)
+			refreshPools = append(refreshPools, n)
 		}
 	}
+	a.countersMutex.Unlock()
 
 	a.pools = pools
 
@@ -98,14 +122,9 @@ func (a *Allocator) SetPools(pools *config.Pools) {
 	}
 
 	// Refresh or initiate stats
-	for n, p := range a.pools.ByName {
-		total, ipv4, ipv6 := poolCount(p)
-		stats.poolCapacity.WithLabelValues(n).Set(float64(total))
-		stats.ipv4PoolCapacity.WithLabelValues(n).Set(float64(ipv4))
-		stats.ipv6PoolCapacity.WithLabelValues(n).Set(float64(ipv6))
-		stats.poolActive.WithLabelValues(n).Set(float64(len(a.poolIPsInUse[n])))
-		stats.ipv4PoolActive.WithLabelValues(n).Set(float64(len(a.poolIPV4InUse[n])))
-		stats.ipv6PoolActive.WithLabelValues(n).Set(float64(len(a.poolIPV6InUse[n])))
+	for _, p := range a.pools.ByName {
+		a.updatePoolStats(p)
+		refreshPools = append(refreshPools, p.Name)
 	}
 }
 
@@ -143,13 +162,8 @@ func (a *Allocator) assign(svc string, alloc *alloc) {
 			a.poolIPV4InUse[alloc.pool][ip.String()]++
 		}
 	}
-	total, ipv4, ipv6 := poolCount(a.pools.ByName[alloc.pool])
-	stats.poolCapacity.WithLabelValues(alloc.pool).Set(float64(total))
-	stats.ipv4PoolCapacity.WithLabelValues(alloc.pool).Set(float64(ipv4))
-	stats.ipv6PoolCapacity.WithLabelValues(alloc.pool).Set(float64(ipv6))
-	stats.poolActive.WithLabelValues(alloc.pool).Set(float64(len(a.poolIPsInUse[alloc.pool])))
-	stats.ipv4PoolActive.WithLabelValues(alloc.pool).Set(float64(len(a.poolIPV4InUse[alloc.pool])))
-	stats.ipv6PoolActive.WithLabelValues(alloc.pool).Set(float64(len(a.poolIPV6InUse[alloc.pool])))
+	a.updatePoolStats(a.pools.ByName[alloc.pool])
+	a.countersChangedCallback(alloc.pool)
 }
 
 // Assign assigns the requested ip to svc, if the assignment is
@@ -240,9 +254,14 @@ func (a *Allocator) Unassign(svc string) {
 			delete(a.poolIPV6InUse[al.pool], ip.String())
 		}
 	}
-	stats.poolActive.WithLabelValues(al.pool).Set(float64(len(a.poolIPsInUse[al.pool])))
-	stats.ipv4PoolActive.WithLabelValues(al.pool).Set(float64(len(a.poolIPV4InUse[al.pool])))
-	stats.ipv6PoolActive.WithLabelValues(al.pool).Set(float64(len(a.poolIPV6InUse[al.pool])))
+
+	if _, ok := a.pools.ByName[al.pool]; !ok {
+		deleteStatsFor(al.pool)
+		return
+	}
+
+	a.updatePoolStats(a.pools.ByName[al.pool])
+	a.countersChangedCallback(al.pool)
 }
 
 // getFreeIPsFromPool determines, with best effort, an ipv4 and an ipv6 available from the provided pool.
@@ -546,6 +565,12 @@ func (a *Allocator) PoolForIP(ips []net.IP) *config.Pool {
 	return poolFor(a.pools.ByName, ips)
 }
 
+func (a *Allocator) CountersForPool(name string) PoolCounters {
+	a.countersMutex.RLock()
+	defer a.countersMutex.RUnlock()
+	return a.poolToCounters[name]
+}
+
 func sortPools(pools []*config.Pool) {
 	// A lower value for pool priority equals a higher priority and sort
 	// pools from higher to low priority. when no priority (0) set on
@@ -712,3 +737,21 @@ func ipPolicyForService(svc *v1.Service) v1.IPFamilyPolicy {
 	}
 	return serviceIPFamilyPolicy
 }
+
+func (a *Allocator) updatePoolStats(p *config.Pool) {
+	a.countersMutex.Lock()
+	defer a.countersMutex.Unlock()
+	total, ipv4, ipv6 := poolCount(p)
+	stats.poolCapacity.WithLabelValues(p.Name).Set(float64(total))
+	stats.ipv4PoolCapacity.WithLabelValues(p.Name).Set(float64(ipv4))
+	stats.ipv6PoolCapacity.WithLabelValues(p.Name).Set(float64(ipv6))
+	stats.poolActive.WithLabelValues(p.Name).Set(float64(len(a.poolIPsInUse[p.Name])))
+	stats.ipv4PoolActive.WithLabelValues(p.Name).Set(float64(len(a.poolIPV4InUse[p.Name])))
+	stats.ipv6PoolActive.WithLabelValues(p.Name).Set(float64(len(a.poolIPV6InUse[p.Name])))
+	a.poolToCounters[p.Name] = PoolCounters{
+		AvailableIPv4: ipv4 - int64(len(a.poolIPV4InUse[p.Name])),
+		AvailableIPv6: ipv6 - int64(len(a.poolIPV6InUse[p.Name])),
+		AssignedIPv4:  int64(len(a.poolIPV4InUse[p.Name])),
+		AssignedIPv6:  int64(len(a.poolIPV6InUse[p.Name])),
+	}
+}
diff --git a/internal/allocator/stats.go b/internal/allocator/stats.go
index 3106d9d1..f6928b92 100644
--- a/internal/allocator/stats.go
+++ b/internal/allocator/stats.go
@@ -71,6 +71,16 @@ var stats = struct {
 	}),
 }
 
+func deleteStatsFor(pool string) {
+	stats.poolCapacity.DeleteLabelValues(pool)
+	stats.ipv4PoolCapacity.DeleteLabelValues(pool)
+	stats.ipv6PoolCapacity.DeleteLabelValues(pool)
+	stats.poolActive.DeleteLabelValues(pool)
+	stats.poolAllocated.DeleteLabelValues(pool)
+	stats.ipv4PoolActive.DeleteLabelValues(pool)
+	stats.ipv6PoolActive.DeleteLabelValues(pool)
+}
+
 func init() {
 	prometheus.MustRegister(stats.poolCapacity)
 	prometheus.MustRegister(stats.ipv4PoolCapacity)
diff --git a/internal/bgp/bgp.go b/internal/bgp/bgp.go
index 78fb2d9b..1daf58d9 100644
--- a/internal/bgp/bgp.go
+++ b/internal/bgp/bgp.go
@@ -64,6 +64,8 @@ type Session interface {
 
 type SessionParameters struct {
 	PeerAddress     string
+	PeerPort        uint16
+	PeerInterface   string
 	SourceAddress   net.IP
 	MyASN           uint32
 	RouterID        net.IP
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index 5f328ec2..f932af22 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -68,6 +68,7 @@ type neighborConfig struct {
 	Name                string
 	ASN                 string
 	Addr                string
+	Iface               string
 	SrcAddr             string
 	Port                uint16
 	HoldTime            *int64
@@ -94,10 +95,15 @@ type neighborConfig struct {
 }
 
 func (n *neighborConfig) ID() string {
-	if n.VRFName == "" {
-		return n.Addr
+	id := n.Addr
+	if n.Iface != "" {
+		id = n.Iface
 	}
-	return fmt.Sprintf("%s-%s", n.Addr, n.VRFName)
+	vrf := ""
+	if n.VRFName != "" {
+		vrf = "-" + n.VRFName
+	}
+	return id + vrf
 }
 
 type advertisementConfig struct {
@@ -116,8 +122,11 @@ 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, dynamicASN string, vrfName string) string {
+func NeighborName(peerAddr, iface string, ASN uint32, dynamicASN string, vrfName string) string {
 	asn := asnFor(ASN, dynamicASN)
+	if peerAddr == "" {
+		return fmt.Sprintf("%s@%s@%s", asn, iface, vrfName)
+	}
 	return fmt.Sprintf("%s@%s@%s", asn, peerAddr, vrfName)
 }
 
@@ -162,7 +171,7 @@ 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 uint32, asn string, eBGPMultiHop bool) bool {
+			"mustDisableConnectedCheck": func(ipFamily ipfamily.Family, myASN uint32, asn, iface string, eBGPMultiHop bool) bool {
 				// return true only for non-multihop IPv6 eBGP sessions
 
 				if ipFamily != ipfamily.IPv6 {
@@ -173,6 +182,10 @@ func templateConfig(data interface{}) (string, error) {
 					return false
 				}
 
+				if iface != "" {
+					return true
+				}
+
 				// internal means we expect the session to be iBGP
 				if asn == "internal" {
 					return false
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index 492ece0a..82da02c0 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -47,7 +47,17 @@ var osHostname = os.Hostname
 // sessionName() defines the format of the key of the 'sessions' map in
 // the 'frrState' struct.
 func sessionName(s session) string {
-	baseName := fmt.Sprintf("%d@%s-%d@%s", s.PeerASN, s.PeerAddress, s.MyASN, s.SourceAddress)
+	asn := strconv.FormatUint(uint64(s.PeerASN), 10)
+	if s.DynamicASN != "" {
+		asn = s.DynamicASN
+	}
+
+	peer := s.PeerAddress
+	if s.PeerInterface != "" {
+		peer = s.PeerInterface
+	}
+
+	baseName := fmt.Sprintf("%s@%s-%d@%s", asn, peer, s.MyASN, s.SourceAddress)
 	if s.VRFName == "" {
 		return baseName
 	}
@@ -252,20 +262,14 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 			routers[routerName] = rout
 		}
 
-		neighborName := NeighborName(s.PeerAddress, s.PeerASN, s.DynamicASN, s.VRFName)
+		neighborName := NeighborName(s.PeerAddress, s.PeerInterface, s.PeerASN, s.DynamicASN, s.VRFName)
 		if neighbor, exist = rout.neighbors[neighborName]; !exist {
-			host, port, err := net.SplitHostPort(s.PeerAddress)
-			if err != nil {
-				return nil, err
-			}
+			family := ipfamily.ForAddress(net.ParseIP(s.PeerAddress))
 
-			portUint, err := strconv.ParseUint(port, 10, 16)
-			if err != nil {
-				return nil, err
+			if s.PeerInterface != "" {
+				family = ipfamily.DualStack
 			}
 
-			family := ipfamily.ForAddress(net.ParseIP(host))
-
 			var connectTime int64
 			if s.ConnectTime != nil {
 				connectTime = int64(*s.ConnectTime / time.Second)
@@ -286,8 +290,9 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 				Name:            neighborName,
 				IPFamily:        family,
 				ASN:             asnFor(s.PeerASN, s.DynamicASN),
-				Addr:            host,
-				Port:            uint16(portUint),
+				Addr:            s.PeerAddress,
+				Iface:           s.PeerInterface,
+				Port:            s.PeerPort,
 				HoldTime:        holdTime,
 				KeepaliveTime:   keepaliveTime,
 				ConnectTime:     connectTime,
diff --git a/internal/bgp/frr/templates/neighboripfamily.tmpl b/internal/bgp/frr/templates/neighboripfamily.tmpl
index ac114d00..0e046a93 100644
--- a/internal/bgp/frr/templates/neighboripfamily.tmpl
+++ b/internal/bgp/frr/templates/neighboripfamily.tmpl
@@ -1,17 +1,22 @@
 {{- define "neighborenableipfamily"}}
+{{- $peer := .Addr }}
+{{- /* Unnummber BGP */}}
+{{- if ne .Iface "" }}
+  {{- $peer = .Iface }}
+{{- end }}
 {{/* no bgp default ipv4-unicast prevents peering if no address families are defined. We declare an ipv4 one for the peer to make the pairing happen */}}
 {{- if activateNeighborFor "ipv4" .IPFamily .DisableMP}}
   address-family ipv4 unicast
-    neighbor {{.Addr}} activate
-    neighbor {{.Addr}} route-map {{.ID}}-in in
-    neighbor {{.Addr}} route-map {{.ID}}-out out
+    neighbor {{$peer}} activate
+    neighbor {{$peer}} route-map {{.ID}}-in in
+    neighbor {{$peer}} route-map {{.ID}}-out out
   exit-address-family
 {{- end -}}
 {{if activateNeighborFor "ipv6" .IPFamily .DisableMP}}
   address-family ipv6 unicast
-    neighbor {{.Addr}} activate
-    neighbor {{.Addr}} route-map {{.ID}}-in in
-    neighbor {{.Addr}} route-map {{.ID}}-out out
+    neighbor {{$peer}} activate
+    neighbor {{$peer}} route-map {{.ID}}-in in
+    neighbor {{$peer}} route-map {{.ID}}-out out
   exit-address-family
 {{- end -}}
 {{- end -}}
diff --git a/internal/bgp/frr/templates/neighborsession.tmpl b/internal/bgp/frr/templates/neighborsession.tmpl
index 840311bb..77942c1e 100644
--- a/internal/bgp/frr/templates/neighborsession.tmpl
+++ b/internal/bgp/frr/templates/neighborsession.tmpl
@@ -1,31 +1,40 @@
 {{- define "neighborsession"}}
+  {{- if ne .neighbor.Iface  ""}}
+  neighbor {{.neighbor.Iface }} interface remote-as {{.neighbor.ASN}}
+  {{- else }}
   neighbor {{.neighbor.Addr}} remote-as {{.neighbor.ASN}}
+  {{- end }}
+  {{- /* Unnummber BGP */}}
+  {{- $peer := .neighbor.Addr }}
+  {{- if ne .neighbor.Iface "" }}
+    {{- $peer = .neighbor.Iface }}
+  {{- end }}
   {{- if .neighbor.EBGPMultiHop }}
-  neighbor {{.neighbor.Addr}} ebgp-multihop
+  neighbor {{$peer}} ebgp-multihop
   {{- end }}
   {{ if .neighbor.Port -}}
-  neighbor {{.neighbor.Addr}} port {{.neighbor.Port}}
+  neighbor {{$peer}} port {{.neighbor.Port}}
   {{- end }}
   {{- if and .neighbor.KeepaliveTime .neighbor.HoldTime }}
-  neighbor {{.neighbor.Addr}} timers {{.neighbor.KeepaliveTime}} {{.neighbor.HoldTime}}
+  neighbor {{$peer}} timers {{.neighbor.KeepaliveTime}} {{.neighbor.HoldTime}}
   {{- end }}
   {{- if ne .neighbor.ConnectTime 0}}
-  neighbor {{.neighbor.Addr}} timers connect {{.neighbor.ConnectTime}}
+  neighbor {{$peer}} timers connect {{.neighbor.ConnectTime}}
   {{- end }}
   {{ if .neighbor.Password -}}
-  neighbor {{.neighbor.Addr}} password {{.neighbor.Password}}
+  neighbor {{$peer}} password {{.neighbor.Password}}
   {{- end }}
   {{ if .neighbor.SrcAddr -}}
-  neighbor {{.neighbor.Addr}} update-source {{.neighbor.SrcAddr}}
+  neighbor {{$peer}} update-source {{.neighbor.SrcAddr}}
   {{- end }}
   {{- if .neighbor.GracefulRestart }}
-  neighbor {{.neighbor.Addr}} graceful-restart
+  neighbor {{$peer}} graceful-restart
   {{- end }}
   {{- if ne .neighbor.BFDProfile "" }}
-  neighbor {{.neighbor.Addr}} bfd
-  neighbor {{.neighbor.Addr}} bfd profile {{.neighbor.BFDProfile}}
+  neighbor {{$peer}} bfd
+  neighbor {{$peer}} bfd profile {{.neighbor.BFDProfile}}
   {{- end }}
-  {{- if  mustDisableConnectedCheck .neighbor.IPFamily .routerASN .neighbor.ASN .neighbor.EBGPMultiHop }}
+  {{- if  mustDisableConnectedCheck .neighbor.IPFamily .routerASN .neighbor.ASN .neighbor.Iface .neighbor.EBGPMultiHop }}
   neighbor {{.neighbor.Addr}} disable-connected-check
   {{- end }}
 {{- end -}}
diff --git a/internal/bgp/frrk8s/frrk8s.go b/internal/bgp/frrk8s/frrk8s.go
index b0d9cf1c..9cec413d 100644
--- a/internal/bgp/frrk8s/frrk8s.go
+++ b/internal/bgp/frrk8s/frrk8s.go
@@ -6,7 +6,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"net"
 	"reflect"
 	"sort"
 	"strconv"
@@ -48,13 +47,23 @@ type session struct {
 	logger         log.Logger
 }
 
-// sessionName() defines the format of the key of the 'sessions' map in
-// the 'frrState' struct.
+// sessionName() defines the format of a map key.
 func sessionName(s session) string {
-	baseName := fmt.Sprintf("%d@%s-%d@%s", s.PeerASN, s.PeerAddress, s.MyASN, s.SourceAddress)
+	asn := strconv.FormatUint(uint64(s.PeerASN), 10)
+	if s.DynamicASN != "" {
+		asn = s.DynamicASN
+	}
+
+	peerName := s.PeerAddress
+	if s.PeerInterface != "" {
+		peerName = s.PeerInterface
+	}
+
+	baseName := fmt.Sprintf("%s@%s-%d@%s", asn, peerName, s.MyASN, s.SourceAddress)
 	if s.VRFName == "" {
 		return baseName
 	}
+
 	return baseName + "/" + s.VRFName
 }
 
@@ -227,19 +236,8 @@ func (sm *sessionManager) updateConfig() error {
 			routers[routerName] = rout
 		}
 
-		neighborName := frr.NeighborName(s.PeerAddress, s.PeerASN, s.DynamicASN, s.VRFName)
+		neighborName := sessionName(*s)
 		if neighbor, exist = rout.neighbors[neighborName]; !exist {
-			host, port, err := net.SplitHostPort(s.PeerAddress)
-			if err != nil {
-				return err
-			}
-
-			portUint, err := strconv.ParseUint(port, 10, 16)
-			if err != nil {
-				return err
-			}
-			portUint16 := uint16(portUint)
-
 			if !reflect.DeepEqual(s.PasswordRef, corev1.SecretReference{}) && s.Password != "" {
 				return fmt.Errorf("invalid session with password and secret set: %s", sessionName(*s))
 			}
@@ -259,10 +257,11 @@ func (sm *sessionManager) updateConfig() error {
 			}
 
 			neighbor = frrv1beta1.Neighbor{
+				Address:               s.PeerAddress,
+				Interface:             s.PeerInterface,
 				ASN:                   s.PeerASN,
 				DynamicASN:            frrv1beta1.DynamicASNMode(s.DynamicASN),
-				Address:               host,
-				Port:                  &portUint16,
+				Port:                  &s.PeerPort,
 				HoldTime:              holdTime,
 				KeepaliveTime:         keepaliveTime,
 				ConnectTime:           connectTime,
diff --git a/internal/bgp/native/native.go b/internal/bgp/native/native.go
index eea36036..9413d3a0 100644
--- a/internal/bgp/native/native.go
+++ b/internal/bgp/native/native.go
@@ -44,6 +44,9 @@ type session struct {
 	nextHop        net.IP
 	advertised     map[string]*bgp.Advertisement
 	new            map[string]*bgp.Advertisement
+
+	// peerName identifies this BGP session to be used for metrics
+	peerName string
 }
 
 // The 'Native' implementation does not require a session manager .
@@ -71,6 +74,7 @@ func (sm *sessionManager) NewSession(l log.Logger, args bgp.SessionParameters) (
 		logger:            log.With(l, "peer", args.PeerAddress, "localASN", args.MyASN, "peerASN", args.PeerASN),
 		newHoldTime:       make(chan bool, 1),
 		advertised:        map[string]*bgp.Advertisement{},
+		peerName:          fmt.Sprintf("%s:%d", args.PeerAddress, args.PeerPort),
 	}
 	ret.cond = sync.NewCond(&ret.mu)
 	go ret.sendKeepalives()
@@ -100,7 +104,7 @@ func (sm *sessionManager) SetEventCallback(func(interface{})) {}
 
 // run tries to stay connected to the peer, and pumps route updates to it.
 func (s *session) run() {
-	defer stats.DeleteSession(s.PeerAddress)
+	defer stats.DeleteSession(s.peerName)
 	for {
 		if err := s.connect(); err != nil {
 			if err == errClosed {
@@ -111,7 +115,7 @@ func (s *session) run() {
 			time.Sleep(backoff)
 			continue
 		}
-		stats.SessionUp(s.PeerAddress)
+		stats.SessionUp(s.peerName)
 		s.backoff.Reset()
 
 		level.Info(s.logger).Log("event", "sessionUp", "msg", "BGP session established")
@@ -119,7 +123,7 @@ func (s *session) run() {
 		if !s.sendUpdates() {
 			return
 		}
-		stats.SessionDown(s.PeerAddress)
+		stats.SessionDown(s.peerName)
 		level.Warn(s.logger).Log("event", "sessionDown", "msg", "BGP session down")
 	}
 }
@@ -150,9 +154,9 @@ func (s *session) sendUpdates() bool {
 			level.Error(s.logger).Log("op", "sendUpdate", "ip", c, "error", err, "msg", "failed to send BGP update")
 			return true
 		}
-		stats.UpdateSent(s.PeerAddress)
+		stats.UpdateSent(s.peerName)
 	}
-	stats.AdvertisedPrefixes(s.PeerAddress, len(s.advertised))
+	stats.AdvertisedPrefixes(s.peerName, len(s.advertised))
 
 	for {
 		for s.new == nil && s.conn != nil {
@@ -183,7 +187,7 @@ func (s *session) sendUpdates() bool {
 				level.Error(s.logger).Log("op", "sendUpdate", "prefix", c, "error", err, "msg", "failed to send BGP update")
 				return true
 			}
-			stats.UpdateSent(s.PeerAddress)
+			stats.UpdateSent(s.peerName)
 		}
 
 		wdr := []*net.IPNet{}
@@ -200,10 +204,10 @@ func (s *session) sendUpdates() bool {
 				}
 				return true
 			}
-			stats.UpdateSent(s.PeerAddress)
+			stats.UpdateSent(s.peerName)
 		}
 		s.advertised, s.new = s.new, nil
-		stats.AdvertisedPrefixes(s.PeerAddress, len(s.advertised))
+		stats.AdvertisedPrefixes(s.peerName, len(s.advertised))
 	}
 }
 
@@ -220,7 +224,7 @@ func (s *session) connect() error {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
 	deadline, _ := ctx.Deadline()
-	conn, err := dialMD5(ctx, s.PeerAddress, s.SourceAddress, s.Password)
+	conn, err := dialMD5(ctx, fmt.Sprintf("%s:%d", s.PeerAddress, s.PeerPort), s.SourceAddress, s.Password)
 	if err != nil {
 		return fmt.Errorf("dial %q: %s", s.PeerAddress, err)
 	}
@@ -473,7 +477,8 @@ func (s *session) Set(advs ...*bgp.Advertisement) error {
 	}
 
 	s.new = newAdvs
-	stats.PendingPrefixes(s.PeerAddress, len(s.new))
+
+	stats.PendingPrefixes(s.peerName, len(s.new))
 	s.cond.Broadcast()
 
 	return nil
@@ -485,13 +490,14 @@ func (s *session) abort() {
 	if s.conn != nil {
 		s.conn.Close()
 		s.conn = nil
-		stats.SessionDown(s.PeerAddress)
+		stats.SessionDown(s.peerName)
 	}
 	// Next time we retry the connection, we can just skip straight to
 	// the desired end state.
 	if s.new != nil {
 		s.advertised, s.new = s.new, nil
-		stats.PendingPrefixes(s.PeerAddress, len(s.advertised))
+
+		stats.PendingPrefixes(s.peerName, len(s.advertised))
 	}
 	s.cond.Broadcast()
 }
diff --git a/internal/bgp/native/stats.go b/internal/bgp/native/stats.go
index 001d469f..616ed78d 100644
--- a/internal/bgp/native/stats.go
+++ b/internal/bgp/native/stats.go
@@ -53,13 +53,6 @@ func init() {
 	prometheus.MustRegister(stats.pendingPrefixes)
 }
 
-func (m *metrics) NewSession(addr string) {
-	m.sessionUp.WithLabelValues(addr).Set(0)
-	m.prefixes.WithLabelValues(addr).Set(0)
-	m.pendingPrefixes.WithLabelValues(addr).Set(0)
-	m.updatesSent.WithLabelValues(addr).Add(0) // just creates the metric
-}
-
 func (m *metrics) DeleteSession(addr string) {
 	m.sessionUp.DeleteLabelValues(addr)
 	m.prefixes.DeleteLabelValues(addr)
diff --git a/internal/config/config.go b/internal/config/config.go
index 4efa660a..c6d28be1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -31,6 +31,8 @@ import (
 	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"
+	k8snodes "go.universe.tf/metallb/internal/k8s/nodes"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -100,6 +102,9 @@ type Peer struct {
 	DynamicASN string
 	// Address to dial when establishing the session.
 	Addr net.IP
+	// Iface is the Interface to use for Unnumbered BGP peering.
+	// Addr field must be nil.
+	Iface string
 	// Source address to use when establishing the session.
 	SrcAddr net.IP
 	// Port to dial when establishing the session.
@@ -278,7 +283,7 @@ func peersFor(resources ClusterResources, BFDProfiles map[string]*BFDProfile) (m
 	for _, p := range resources.Peers {
 		peer, err := peerFromCR(p, resources.PasswordSecrets)
 		if err != nil {
-			return nil, errors.Join(err, fmt.Errorf("parsing peer %s", p.Name))
+			return nil, fmt.Errorf("parsing peer %s %w", p.Name, err)
 		}
 		if peer.BFDProfile != "" {
 			if _, ok := BFDProfiles[peer.BFDProfile]; !ok {
@@ -324,6 +329,13 @@ func poolsFor(resources ClusterResources) (*Pools, error) {
 					return nil, fmt.Errorf("CIDR %q in pool %q overlaps with already defined CIDR %q", cidr, p.Name, m)
 				}
 			}
+			// Check pool CIDR is not overlapping with Node IPs
+			nodeIps := k8snodes.NodeIPsForFamily(resources.Nodes, ipfamily.ForCIDR(cidr))
+			for _, nodeIP := range nodeIps {
+				if cidr.Contains(nodeIP) {
+					return nil, fmt.Errorf("pool cidr %q contains nodeIp %q", cidr, nodeIP)
+				}
+			}
 			allCIDRs = append(allCIDRs, cidr)
 		}
 
@@ -389,9 +401,12 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 	if p.Spec.ASN == p.Spec.MyASN && p.Spec.EBGPMultiHop {
 		return nil, errors.New("invalid ebgp-multihop parameter set for an ibgp peer")
 	}
-	ip := net.ParseIP(p.Spec.Address)
-	if ip == nil {
-		return nil, fmt.Errorf("invalid BGPPeer address %q", p.Spec.Address)
+	if p.Spec.Address == "" && p.Spec.Interface == "" {
+		return nil, fmt.Errorf("peer has no Address or Interface specified")
+	}
+
+	if p.Spec.Address != "" && p.Spec.Interface != "" {
+		return nil, fmt.Errorf("peer has both Address and Interface specified")
 	}
 
 	holdTime, keepaliveTime, err := parseTimers(p.Spec.HoldTime, p.Spec.KeepaliveTime)
@@ -399,6 +414,14 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 		return nil, fmt.Errorf("invalid BGPPeer timers: %w", err)
 	}
 
+	var ip net.IP
+	if p.Spec.Address != "" {
+		ip = net.ParseIP(p.Spec.Address)
+		if ip == nil {
+			return nil, fmt.Errorf("invalid BGPPeer address %q", p.Spec.Address)
+		}
+	}
+
 	// Ideally we would set a default RouterID here, instead of having
 	// to do it elsewhere in the code. Unfortunately, we don't know
 	// the node IP here.
@@ -455,6 +478,7 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 		ASN:                   p.Spec.ASN,
 		DynamicASN:            string(p.Spec.DynamicASN),
 		Addr:                  ip,
+		Iface:                 p.Spec.Interface,
 		SrcAddr:               src,
 		Port:                  p.Spec.Port,
 		HoldTime:              holdTime,
diff --git a/internal/config/validation.go b/internal/config/validation.go
index 2d1759e1..66ece4b7 100644
--- a/internal/config/validation.go
+++ b/internal/config/validation.go
@@ -54,6 +54,9 @@ func DiscardFRROnly(c ClusterResources) error {
 		if p.Spec.DynamicASN != "" {
 			return fmt.Errorf("peer %s has dynamicASN set on native bgp mode", p.Spec.Address)
 		}
+		if p.Spec.Interface != "" {
+			return fmt.Errorf("peer %s has interface set on native bgp mode", p.Spec.Address)
+		}
 	}
 	if len(c.BFDProfiles) > 0 {
 		return errors.New("bfd profiles section set")
diff --git a/internal/k8s/controllers/pool_controller.go b/internal/k8s/controllers/pool_controller.go
index d072c00d..d1041163 100644
--- a/internal/k8s/controllers/pool_controller.go
+++ b/internal/k8s/controllers/pool_controller.go
@@ -116,7 +116,7 @@ func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
 func (r *PoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	p := predicate.Funcs{
 		UpdateFunc: func(e event.UpdateEvent) bool {
-			return filterNodeEvent(e) && filterNamespaceEvent(e)
+			return filterNodeEvent(e) && filterNamespaceEvent(e) && filterPoolStatusEvent(e)
 		},
 	}
 	return ctrl.NewControllerManagedBy(mgr).
@@ -126,3 +126,17 @@ func (r *PoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		WithEventFilter(p).
 		Complete(r)
 }
+
+func filterPoolStatusEvent(e event.UpdateEvent) bool {
+	_, ok := e.ObjectOld.(*metallbv1beta1.IPAddressPool)
+	if !ok {
+		return true
+	}
+
+	_, ok = e.ObjectNew.(*metallbv1beta1.IPAddressPool)
+	if !ok {
+		return true
+	}
+
+	return predicate.GenerationChangedPredicate{}.Update(e)
+}
diff --git a/internal/k8s/controllers/pool_status_controller.go b/internal/k8s/controllers/pool_status_controller.go
new file mode 100644
index 00000000..822ebb24
--- /dev/null
+++ b/internal/k8s/controllers/pool_status_controller.go
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package controllers
+
+import (
+	"context"
+	"reflect"
+
+	"github.com/go-kit/log"
+	"github.com/go-kit/log/level"
+	"go.universe.tf/metallb/api/v1beta1"
+	"go.universe.tf/metallb/internal/allocator"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/controller-runtime/pkg/source"
+)
+
+type PoolCountersFetcher func(string) allocator.PoolCounters
+
+type poolStatusEvent struct {
+	metav1.TypeMeta
+	metav1.ObjectMeta
+}
+
+func (evt *poolStatusEvent) DeepCopyObject() runtime.Object {
+	res := new(poolStatusEvent)
+	res.Name = evt.Name
+	res.Namespace = evt.Namespace
+	return res
+}
+
+func NewPoolStatusEvent(namespace, name string) event.GenericEvent {
+	evt := poolStatusEvent{}
+	evt.Name = name
+	evt.Namespace = namespace
+	return event.GenericEvent{Object: &evt}
+}
+
+type PoolStatusReconciler struct {
+	client.Client
+	Logger          log.Logger
+	CountersFetcher PoolCountersFetcher
+	ReconcileChan   <-chan event.GenericEvent
+}
+
+func (r *PoolStatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	level.Info(r.Logger).Log("controller", "PoolStatusReconciler", "start reconcile", req.NamespacedName.String())
+	defer level.Info(r.Logger).Log("controller", "PoolStatusReconciler", "end reconcile", req.NamespacedName.String())
+
+	var pool v1beta1.IPAddressPool
+	err := r.Get(ctx, req.NamespacedName, &pool)
+	if apierrors.IsNotFound(err) {
+		return ctrl.Result{}, nil
+	}
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
+	c := r.CountersFetcher(pool.Name)
+
+	newStatus := v1beta1.IPAddressPoolStatus{
+		AssignedIPv4:  c.AssignedIPv4,
+		AssignedIPv6:  c.AssignedIPv6,
+		AvailableIPv4: c.AvailableIPv4,
+		AvailableIPv6: c.AvailableIPv6,
+	}
+
+	if reflect.DeepEqual(pool.Status, newStatus) {
+		return ctrl.Result{}, nil
+	}
+
+	pool.Status = newStatus
+	err = r.Client.Status().Update(ctx, &pool)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
+	return ctrl.Result{}, nil
+}
+
+func (r *PoolStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	p := predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			return !filterPoolStatusEvent(e)
+		},
+	}
+
+	return ctrl.NewControllerManagedBy(mgr).
+		Named("PoolStatusController").
+		For(&v1beta1.IPAddressPool{}).
+		WithEventFilter(p).
+		WatchesRawSource(source.Channel(r.ReconcileChan, &handler.EnqueueRequestForObject{})).
+		Complete(r)
+}
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index 4a1019f9..34fb99ea 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -124,6 +124,8 @@ type Config struct {
 	Layer2StatusFetcher controllers.L2StatusFetcher
 	BGPStatusChan       <-chan event.GenericEvent
 	BGPPeersFetcher     controllers.PeersForService
+	PoolStatusChan      <-chan event.GenericEvent
+	PoolCountersFetcher controllers.PoolCountersFetcher
 }
 
 // New connects to masterAddr, using kubeconfig to authenticate.
@@ -212,6 +214,16 @@ func New(cfg *Config) (*Client, error) {
 			level.Error(c.logger).Log("error", err, "unable to create controller", "config")
 			return nil, errors.Join(err, errors.New("failed to create config reconciler"))
 		}
+
+		if err = (&controllers.PoolStatusReconciler{
+			Client:          mgr.GetClient(),
+			Logger:          cfg.Logger,
+			CountersFetcher: cfg.PoolCountersFetcher,
+			ReconcileChan:   cfg.PoolStatusChan,
+		}).SetupWithManager(mgr); err != nil {
+			level.Error(c.logger).Log("error", err, "unable to create controller", "config")
+			return nil, errors.Join(err, errors.New("failed to create pool status reconciler"))
+		}
 	}
 
 	if cfg.NodeChanged != nil {
diff --git a/internal/k8s/nodes/nodes.go b/internal/k8s/nodes/nodes.go
index 1c99f460..be7fba2c 100644
--- a/internal/k8s/nodes/nodes.go
+++ b/internal/k8s/nodes/nodes.go
@@ -4,7 +4,9 @@ package nodes
 
 import (
 	"errors"
+	"net"
 
+	"go.universe.tf/metallb/internal/ipfamily"
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -59,3 +61,20 @@ func IsNodeExcludedFromBalancers(n *corev1.Node) bool {
 	}
 	return false
 }
+
+// NodeIPsForFamily returns all input node nodeIP based on ipfamily.
+func NodeIPsForFamily(nodes []corev1.Node, family ipfamily.Family) []net.IP {
+	var nodeIPs []net.IP
+	for _, n := range nodes {
+		for _, a := range n.Status.Addresses {
+			if a.Type == corev1.NodeInternalIP {
+				nodeIP := net.ParseIP(a.Address)
+				if family != ipfamily.DualStack && ipfamily.ForAddress(nodeIP) != family {
+					continue
+				}
+				nodeIPs = append(nodeIPs, nodeIP)
+			}
+		}
+	}
+	return nodeIPs
+}
diff --git a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go
index ec797034..3f30ae95 100644
--- a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go
+++ b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go
@@ -104,8 +104,13 @@ func validateIPAddressPoolCreate(ipAddress *v1beta1.IPAddressPool) error {
 		return err
 	}
 
+	nodes, err := getExistingNodes()
+	if err != nil {
+		return err
+	}
+
 	toValidate := ipAddressListWithUpdate(existingIPAddressPoolList, ipAddress)
-	err = Validator.Validate(toValidate)
+	err = Validator.Validate(toValidate, nodes)
 	if err != nil {
 		level.Error(Logger).Log("webhook", "ipAddress", "action", "create", "name", ipAddress.Name, "namespace", ipAddress.Namespace, "error", err)
 		return err
@@ -122,8 +127,13 @@ func validateIPAddressPoolUpdate(ipAddress *v1beta1.IPAddressPool, _ *v1beta1.IP
 		return err
 	}
 
+	nodes, err := getExistingNodes()
+	if err != nil {
+		return err
+	}
+
 	toValidate := ipAddressListWithUpdate(existingIPAddressPoolList, ipAddress)
-	err = Validator.Validate(toValidate)
+	err = Validator.Validate(toValidate, nodes)
 	if err != nil {
 		level.Error(Logger).Log("webhook", "ipAddress", "action", "update", "name", ipAddress.Name, "namespace", ipAddress.Namespace, "error", err)
 		return err
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 47061ef6..655e1b88 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -19,7 +19,6 @@ import (
 	"net"
 	"reflect"
 	"sort"
-	"strconv"
 	"sync"
 
 	"github.com/go-kit/log"
@@ -59,6 +58,7 @@ const (
 type peer struct {
 	cfg     *config.Peer
 	session bgp.Session
+	id      string
 }
 
 type bgpController struct {
@@ -90,9 +90,15 @@ newPeers:
 				continue newPeers
 			}
 		}
+		id := p.Iface
+		if p.Addr != nil {
+			id = p.Addr.String()
+		}
+
 		// No existing peers match, create a new one.
 		newPeers = append(newPeers, &peer{
 			cfg: p,
+			id:  id,
 		})
 	}
 
@@ -103,14 +109,14 @@ newPeers:
 		if p == nil {
 			continue
 		}
-		level.Info(l).Log("event", "peerRemoved", "peer", p.cfg.Addr, "reason", "removedFromConfig", "msg", "peer deconfigured, closing BGP session")
+		level.Info(l).Log("event", "peerRemoved", "peer", p.id, "reason", "removedFromConfig", "msg", "peer deconfigured, closing BGP session")
 
 		if p.session != nil {
 			if err := p.session.Close(); err != nil {
-				level.Error(l).Log("op", "setConfig", "error", err, "peer", p.cfg.Addr, "msg", "failed to shut down BGP session")
+				level.Error(l).Log("op", "setConfig", "error", err, "peer", p.id, "msg", "failed to shut down BGP session")
 			}
 		}
-		level.Debug(l).Log("event", "peerRemoved", "peer", p.cfg.Addr, "reason", "removedFromConfig", "msg", "peer deconfigured, BGP session closed")
+		level.Debug(l).Log("event", "peerRemoved", "peer", p.id, "reason", "removedFromConfig", "msg", "peer deconfigured, BGP session closed")
 	}
 
 	err := c.syncBFDProfiles(cfg.BFDProfiles)
@@ -222,22 +228,28 @@ func (c *bgpController) syncPeers(l log.Logger) error {
 		// Now, compare current state to intended state, and correct.
 		if p.session != nil && !shouldRun {
 			// Oops, session is running but shouldn't be. Shut it down.
-			level.Info(l).Log("event", "peerRemoved", "peer", p.cfg.Addr, "reason", "filteredByNodeSelector", "msg", "peer deconfigured, closing BGP session")
+			level.Info(l).Log("event", "peerRemoved", "peer", p.id, "reason", "filteredByNodeSelector", "msg", "peer deconfigured, closing BGP session")
 			if err := p.session.Close(); err != nil {
-				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.cfg.Addr, "msg", "failed to shut down BGP session")
+				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.id, "msg", "failed to shut down BGP session")
 			}
 			p.session = nil
 		} else if p.session == nil && shouldRun {
 			// Session doesn't exist, but should be running. Create
 			// it.
-			level.Info(l).Log("event", "peerAdded", "peer", p.cfg.Addr, "msg", "peer configured, starting BGP session")
+			level.Info(l).Log("event", "peerAdded", "peer", p.id, "msg", "peer configured, starting BGP session")
 			var routerID net.IP
 			if p.cfg.RouterID != nil {
 				routerID = p.cfg.RouterID
 			}
 
+			peerAddr := "" // we need because otherwise the value will "<nil>"
+			if p.cfg.Addr != nil {
+				peerAddr = p.cfg.Addr.String()
+			}
 			sessionParams := bgp.SessionParameters{
-				PeerAddress:     net.JoinHostPort(p.cfg.Addr.String(), strconv.Itoa(int(p.cfg.Port))),
+				PeerAddress:     peerAddr,
+				PeerPort:        p.cfg.Port,
+				PeerInterface:   p.cfg.Iface,
 				SourceAddress:   p.cfg.SrcAddr,
 				MyASN:           p.cfg.MyASN,
 				RouterID:        routerID,
@@ -259,7 +271,7 @@ func (c *bgpController) syncPeers(l log.Logger) error {
 			s, err := c.sessionManager.NewSession(c.logger, sessionParams)
 
 			if err != nil {
-				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.cfg.Addr, "msg", "failed to create BGP session")
+				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.id, "msg", "failed to create BGP session")
 				errs++
 			} else {
 				p.session = s
diff --git a/tasks.py b/tasks.py
index f2bdae0b..a3702734 100644
--- a/tasks.py
+++ b/tasks.py
@@ -32,8 +32,10 @@ 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")
+yq_path = os.path.join(build_path, "bin", "yq")
 kubectl_version = "v1.31.0"
 kind_version = "v0.24.0"
+yq_version = "v4.45.1"
 
 
 def _check_architectures(architectures):
@@ -1466,10 +1468,9 @@ def generate_deepcopy():
 
 
 def _align_helm_crds(source, output):
+    fetch_yq()
     run(
-        """yq eval-all 'select(.kind == "CustomResourceDefinition")' {} > {}""".format(
-            source, output
-        )
+        f"""{yq_path} eval-all 'select(.kind == "CustomResourceDefinition")' {source} > {output}"""
     )
     run("sed -i 's/metallb-system/{{{{ .Release.Namespace }}}}/g' {}".format(output))
 
@@ -1530,7 +1531,7 @@ def fetch_kubectl():
     curl_command = "curl -o {} -LO https://dl.k8s.io/release/{}/bin/$(go env GOOS)/$(go env GOARCH)/kubectl".format(
         kubectl_path, kubectl_version
     )
-    get_version_command = f"{kubectl_path} version --short"
+    get_version_command = f"{kubectl_path} version"
     fetch_dependency(
         kubectl_path,
         kubectl_version,
@@ -1590,6 +1591,22 @@ def fetch_ginkgo():
     )
 
 
+@cache
+def fetch_yq():
+    fetch_command = (
+        f"GOBIN={build_path}/bin/ GOPATH={build_path} go install github.com/mikefarah"
+        f"/yq/v4@{yq_version}"
+    )
+    get_version_command = f"{yq_path} --version"
+    fetch_dependency(
+        yq_path,
+        yq_version,
+        fetch_command,
+        get_version_command,
+        "yq (https://github.com/mikefarah/yq/) version ",
+    )
+
+
 def fetch_dependency(
     path: str,
     version: str,
diff --git a/website/content/apis/_index.md b/website/content/apis/_index.md
index 6ea3adc4..91ae9eb6 100644
--- a/website/content/apis/_index.md
+++ b/website/content/apis/_index.md
@@ -288,6 +288,7 @@ _Appears in:_
 | `peerASN` _integer_ | AS number to expect from the remote end of the session.<br />ASN and DynamicASN are mutually exclusive and one of them must be specified. |
 | `dynamicASN` _[DynamicASNMode](#dynamicasnmode)_ | DynamicASN detects the AS number to use for the remote end of the session<br />without explicitly setting it via the ASN field. Limited to:<br />internal - if the neighbor's ASN is different than MyASN connection is denied.<br />external - if the neighbor's ASN is the same as MyASN the connection is denied.<br />ASN and DynamicASN are mutually exclusive and one of them must be specified. |
 | `peerAddress` _string_ | Address to dial when establishing the session. |
+| `interface` _string_ | Interface is the node interface over which the unnumbered BGP peering will<br />be established. No API validation takes place as that string value<br />represents an interface name on the host and if user provides an invalid<br />value, only the actual BGP session will not be established.<br />Address and Interface are mutually exclusive and one of them must be specified. |
 | `sourceAddress` _string_ | Source address to use when establishing the session. |
 | `peerPort` _integer_ | Port to dial when establishing the session. |
 | `holdTime` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#duration-v1-meta)_ | Requested BGP hold time, per RFC4271. |
diff --git a/website/content/installation/clouds.md b/website/content/installation/clouds.md
index fc8b1a15..c7908d3b 100644
--- a/website/content/installation/clouds.md
+++ b/website/content/installation/clouds.md
@@ -85,6 +85,9 @@ load balancers work. You can do this with:
 oc adm policy add-scc-to-user privileged -n metallb-system -z speaker
 ```
 
+Please note that if you are deploying MetalLB via helm, the ServiceAccount
+name is metallb-speaker.
+
 After that, MetalLB should work normally.
 
 ### MetalLB on OpenStack
diff --git a/website/content/metallb.md b/website/content/metallb.md
new file mode 100644
index 00000000..f537e2dc
--- /dev/null
+++ b/website/content/metallb.md
@@ -0,0 +1,4 @@
+---
+title: "vanity url"
+layout: vanityurl
+---
diff --git a/website/layouts/_default/vanityurl.html b/website/layouts/_default/vanityurl.html
new file mode 100644
index 00000000..074fd996
--- /dev/null
+++ b/website/layouts/_default/vanityurl.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+<meta name="go-import" content="metallb.io/metallb git https://github.com/metallb/metallb">
+<meta name="go-source" content="metallb.io/metallb https://github.com/metallb/metallb https://github.com/metallb/metallb/tree/master{/dir} https://github.com/metallb/metallb/blob/master{/dir}/{file}#L{line}">
+<meta http-equiv="refresh" content="0; url=https://godoc.org/metallb.io/metallb">
+</head>
+<body>
+Nothing to see here; <a href="https://godoc.org/metallb.io/metallb">move along</a>.
+</body>
+</html>


Test patch:
diff --git a/configmaptocrs/testdata/configmap-data/config-data-address-pools.golden b/configmaptocrs/testdata/configmap-data/config-data-address-pools.golden
index 9e9dc473..2ca944ec 100644
--- a/configmaptocrs/testdata/configmap-data/config-data-address-pools.golden
+++ b/configmaptocrs/testdata/configmap-data/config-data-address-pools.golden
@@ -11,7 +11,11 @@ spec:
   - fc00:f853:0ccd:e799::/124
   autoAssign: true
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: IPAddressPool
@@ -22,7 +26,11 @@ metadata:
 spec:
   addresses:
   - 192.168.1.240/28
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: BGPAdvertisement
diff --git a/configmaptocrs/testdata/configmap-data/config-data-convert-names.golden b/configmaptocrs/testdata/configmap-data/config-data-convert-names.golden
index 9bd7b7af..76f5570f 100644
--- a/configmaptocrs/testdata/configmap-data/config-data-convert-names.golden
+++ b/configmaptocrs/testdata/configmap-data/config-data-convert-names.golden
@@ -11,7 +11,11 @@ spec:
   - fc00:f853:0ccd:e799::/124
   autoAssign: true
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: IPAddressPool
@@ -22,7 +26,11 @@ metadata:
 spec:
   addresses:
   - 192.168.1.240/28
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: BGPAdvertisement
diff --git a/configmaptocrs/testdata/configmap-data/config-data-full.golden b/configmaptocrs/testdata/configmap-data/config-data-full.golden
index 693457a6..47c392c6 100644
--- a/configmaptocrs/testdata/configmap-data/config-data-full.golden
+++ b/configmaptocrs/testdata/configmap-data/config-data-full.golden
@@ -40,7 +40,11 @@ spec:
   - 192.168.0.150-192.168.0.200
   autoAssign: true
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: IPAddressPool
@@ -53,7 +57,11 @@ spec:
   - 192.168.11.0/24
   - 192.168.1.150-192.168.1.200
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: BGPAdvertisement
diff --git a/configmaptocrs/testdata/configmap-yaml/config-yaml-address-pools.golden b/configmaptocrs/testdata/configmap-yaml/config-yaml-address-pools.golden
index 9e9dc473..2ca944ec 100644
--- a/configmaptocrs/testdata/configmap-yaml/config-yaml-address-pools.golden
+++ b/configmaptocrs/testdata/configmap-yaml/config-yaml-address-pools.golden
@@ -11,7 +11,11 @@ spec:
   - fc00:f853:0ccd:e799::/124
   autoAssign: true
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: IPAddressPool
@@ -22,7 +26,11 @@ metadata:
 spec:
   addresses:
   - 192.168.1.240/28
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: BGPAdvertisement
diff --git a/configmaptocrs/testdata/configmap-yaml/config-yaml-convert-names.golden b/configmaptocrs/testdata/configmap-yaml/config-yaml-convert-names.golden
index 9bd7b7af..76f5570f 100644
--- a/configmaptocrs/testdata/configmap-yaml/config-yaml-convert-names.golden
+++ b/configmaptocrs/testdata/configmap-yaml/config-yaml-convert-names.golden
@@ -11,7 +11,11 @@ spec:
   - fc00:f853:0ccd:e799::/124
   autoAssign: true
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: IPAddressPool
@@ -22,7 +26,11 @@ metadata:
 spec:
   addresses:
   - 192.168.1.240/28
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: BGPAdvertisement
diff --git a/configmaptocrs/testdata/configmap-yaml/config-yaml-full.golden b/configmaptocrs/testdata/configmap-yaml/config-yaml-full.golden
index 693457a6..47c392c6 100644
--- a/configmaptocrs/testdata/configmap-yaml/config-yaml-full.golden
+++ b/configmaptocrs/testdata/configmap-yaml/config-yaml-full.golden
@@ -40,7 +40,11 @@ spec:
   - 192.168.0.150-192.168.0.200
   autoAssign: true
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: IPAddressPool
@@ -53,7 +57,11 @@ spec:
   - 192.168.11.0/24
   - 192.168.1.150-192.168.1.200
   avoidBuggyIPs: true
-status: {}
+status:
+  assignedIPv4: 0
+  assignedIPv6: 0
+  availableIPv4: 0
+  availableIPv6: 0
 ---
 apiVersion: metallb.io/v1beta1
 kind: BGPAdvertisement
diff --git a/controller/controller_test.go b/controller/controller_test.go
index cfe11d1b..c04b922e 100644
--- a/controller/controller_test.go
+++ b/controller/controller_test.go
@@ -108,6 +108,8 @@ func (s *testK8S) gotService(in *v1.Service) *v1.Service {
 	return ret
 }
 
+func noopCallback(_ string) {}
+
 func TestControllerMutation(t *testing.T) {
 	testSelector, err := labels.Parse("team=metallb")
 	if err != nil {
@@ -115,7 +117,7 @@ func TestControllerMutation(t *testing.T) {
 	}
 	k := &testK8S{t: t}
 	c := &controller{
-		ips:    allocator.New(),
+		ips:    allocator.New(noopCallback),
 		client: k,
 	}
 	pools := &config.Pools{ByName: map[string]*config.Pool{
@@ -1105,7 +1107,7 @@ func TestControllerMutation(t *testing.T) {
 func TestControllerConfig(t *testing.T) {
 	k := &testK8S{t: t}
 	c := &controller{
-		ips:    allocator.New(),
+		ips:    allocator.New(noopCallback),
 		client: k,
 	}
 
@@ -1202,7 +1204,7 @@ func TestControllerConfig(t *testing.T) {
 func TestDeleteRecyclesIP(t *testing.T) {
 	k := &testK8S{t: t}
 	c := &controller{
-		ips:    allocator.New(),
+		ips:    allocator.New(noopCallback),
 		client: k,
 	}
 
@@ -1272,7 +1274,7 @@ func TestDeleteRecyclesIP(t *testing.T) {
 func TestControllerReassign(t *testing.T) {
 	k := &testK8S{t: t}
 	c := &controller{
-		ips:    allocator.New(),
+		ips:    allocator.New(noopCallback),
 		client: k,
 	}
 
@@ -1368,7 +1370,7 @@ func TestControllerReassign(t *testing.T) {
 func TestControllerDualStackConfig(t *testing.T) {
 	k := &testK8S{t: t}
 	c := &controller{
-		ips:    allocator.New(),
+		ips:    allocator.New(noopCallback),
 		client: k,
 	}
 
diff --git a/e2etest/bgptests/dump.go b/e2etest/bgptests/dump.go
index 68d51599..c85cdb6c 100644
--- a/e2etest/bgptests/dump.go
+++ b/e2etest/bgptests/dump.go
@@ -12,11 +12,12 @@ import (
 	"github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"go.universe.tf/e2etest/pkg/frr"
+	frrcontainer "go.universe.tf/e2etest/pkg/frr/container"
 	"go.universe.tf/e2etest/pkg/metallb"
 	clientset "k8s.io/client-go/kubernetes"
 )
 
-func dumpBGPInfo(basePath, testName string, cs clientset.Interface, namespace string) {
+func dumpBGPInfo(basePath, testName string, cs clientset.Interface, namespace string, extraContainers ...*frrcontainer.FRR) {
 	testPath := path.Join(basePath, strings.ReplaceAll(testName, " ", "-"))
 	err := os.Mkdir(testPath, 0755)
 	if err != nil && !errors.Is(err, os.ErrExist) {
@@ -24,7 +25,7 @@ func dumpBGPInfo(basePath, testName string, cs clientset.Interface, namespace st
 		return
 	}
 
-	for _, c := range FRRContainers {
+	for _, c := range append(FRRContainers, extraContainers...) {
 		dump, err := frr.RawDump(c, "/etc/frr/bgpd.conf", "/tmp/frr.log", "/etc/frr/daemons")
 		if err != nil {
 			ginkgo.GinkgoWriter.Printf("External frr dump for container %s failed %v", c.Name, err)
diff --git a/e2etest/bgptests/unnummbered.go b/e2etest/bgptests/unnummbered.go
new file mode 100644
index 00000000..5a25f4b8
--- /dev/null
+++ b/e2etest/bgptests/unnummbered.go
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package bgptests
+
+import (
+	"context"
+	"fmt"
+	"net/netip"
+	"time"
+
+	"go.universe.tf/e2etest/pkg/config"
+	"go.universe.tf/e2etest/pkg/container"
+	"go.universe.tf/e2etest/pkg/executor"
+	"go.universe.tf/e2etest/pkg/frr"
+	frrconfig "go.universe.tf/e2etest/pkg/frr/config"
+	frrcontainer "go.universe.tf/e2etest/pkg/frr/container"
+	jigservice "go.universe.tf/e2etest/pkg/jigservice"
+	"go.universe.tf/e2etest/pkg/netdev"
+	testservice "go.universe.tf/e2etest/pkg/service"
+
+	"go.universe.tf/e2etest/pkg/k8s"
+	"go.universe.tf/e2etest/pkg/k8sclient"
+	"go.universe.tf/e2etest/pkg/metallb"
+	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
+	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientset "k8s.io/client-go/kubernetes"
+)
+
+var FRRImage string
+var _ = ginkgo.Describe("FRR Unnumbered BGP", func() {
+	var (
+		testNamespace         string
+		nodeWithP2PConnection corev1.Node
+		remoteP2PContainer    *frrcontainer.FRR
+
+		cs clientset.Interface
+	)
+
+	ginkgo.BeforeEach(func() {
+		ginkgo.By("Clearing any previous configuration")
+		err := ConfigUpdater.Clean()
+		Expect(err).NotTo(HaveOccurred())
+
+		cs = k8sclient.New()
+		allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+		Expect(err).NotTo(HaveOccurred())
+		nodeWithP2PConnection = allNodes.Items[0]
+
+		err = k8s.DeleteNamespace(k8sclient.New(), "unnumbered-bgp")
+		Expect(err).NotTo(HaveOccurred())
+		testNamespace, err = k8s.CreateTestNamespace(k8sclient.New(), "unnumbered-bgp")
+		Expect(err).NotTo(HaveOccurred())
+		ginkgo.DeferCleanup(func() {
+			err := k8s.DeleteNamespace(k8sclient.New(), testNamespace)
+			Expect(err).NotTo(HaveOccurred())
+		})
+	})
+
+	ginkgo.AfterEach(func() {
+		if ginkgo.CurrentSpecReport().Failed() {
+			k8s.DumpInfo(Reporter, ginkgo.CurrentSpecReport().LeafNodeText)
+			dumpBGPInfo(ReportPath, ginkgo.CurrentSpecReport().LeafNodeText, cs, testNamespace, remoteP2PContainer)
+		}
+
+		err := frrcontainer.Delete([]*frrcontainer.FRR{remoteP2PContainer})
+		Expect(err).NotTo(HaveOccurred())
+	})
+
+	ginkgo.DescribeTable("Session is established and route is advertised", func(prefixSendFromLocal []string, p2pInterface string, tweakService func(svc *corev1.Service)) {
+		rc := frrconfig.RouterConfigUnnumbered{
+			ASNLocal:  metalLBASN,
+			ASNRemote: metalLBASN,
+			Hostname:  "tor1",
+			Interface: p2pInterface,
+			RouterID:  "1.1.1.1",
+		}
+
+		var err error
+		ginkgo.By(fmt.Sprintf("creating p2p %s:%s -- %s:remote-p2p-container", nodeWithP2PConnection.Name, p2pInterface, p2pInterface))
+		remoteP2PContainer, err = frrcontainer.CreateP2PPeerFor(nodeWithP2PConnection.Name, p2pInterface, FRRImage)
+		Expect(err).NotTo(HaveOccurred())
+		ginkgo.By(fmt.Sprintf("updating frrconfig to %s", remoteP2PContainer.Name))
+
+		c, err := rc.Config()
+		Expect(err).NotTo(HaveOccurred())
+		err = remoteP2PContainer.UpdateBGPConfigFile(c)
+		Expect(err).NotTo(HaveOccurred())
+
+		resources := config.Resources{
+			Peers: []metallbv1beta2.BGPPeer{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "tor",
+						Namespace: metallb.Namespace,
+					},
+					Spec: metallbv1beta2.BGPPeerSpec{
+						Interface:  p2pInterface,
+						ASN:        rc.ASNRemote,
+						MyASN:      rc.ASNLocal,
+						BFDProfile: "simple",
+					},
+				},
+			},
+			BFDProfiles: []metallbv1beta1.BFDProfile{{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "simple",
+				},
+			},
+			},
+			Pools: []metallbv1beta1.IPAddressPool{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "bgp-test",
+					},
+					Spec: metallbv1beta1.IPAddressPoolSpec{
+						Addresses: prefixSendFromLocal,
+					},
+				},
+			},
+			BGPAdvs: []metallbv1beta1.BGPAdvertisement{
+				{ObjectMeta: metav1.ObjectMeta{Name: "empty"}},
+			},
+		}
+
+		err = ConfigUpdater.Update(resources)
+		Expect(err).NotTo(HaveOccurred(), "apply the CR in k8s api failed")
+
+		nodeP2PContainer := executor.ForContainer(nodeWithP2PConnection.Name)
+		nodeLLA, err := netdev.LinkLocalAddressForDevice(nodeP2PContainer, p2pInterface)
+		Expect(err).NotTo(HaveOccurred())
+		ginkgo.By("validating the node and p2p container peered")
+		validateUnnumberedBGPPeering(remoteP2PContainer, nodeLLA)
+
+		svc, _ := testservice.CreateWithBackend(cs, testNamespace, "unnumbered-lb", tweakService)
+		ginkgo.By("checking the service gets an ip assigned")
+		for _, i := range svc.Status.LoadBalancer.Ingress {
+			ingressIP := jigservice.GetIngressPoint(&i)
+			err = config.ValidateIPInRange(resources.Pools, ingressIP)
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		ginkgo.By(fmt.Sprintf("validating the p2p peer %s received the routes from node", remoteP2PContainer.Name))
+		validatePeerRoutesViaDevice(remoteP2PContainer, p2pInterface, nodeLLA, prefixSendFromLocal)
+
+	},
+		ginkgo.Entry("IPV4", []string{"5.5.5.5/32"}, "net10", func(_ *corev1.Service) {}),
+		ginkgo.Entry("IPV6", []string{"5555::1/128"}, "net20", func(_ *corev1.Service) {}),
+		ginkgo.Entry("DUALSTACK", []string{"5.5.5.5/32", "5555::1/128"}, "net30",
+			func(svc *corev1.Service) { testservice.DualStack(svc) }),
+	)
+})
+
+func validateUnnumberedBGPPeering(peer *frrcontainer.FRR, nodeLLA string) {
+	ginkgo.By(fmt.Sprintf("validating BGP peering to %s", peer.Name))
+	Eventually(func() error {
+		neighbors, err := frr.NeighborsInfo(peer)
+		if err != nil {
+			return err
+		}
+		for _, n := range neighbors {
+			if n.BGPNeighborAddr == nodeLLA && n.Connected && n.BFDInfo.Status == "Up" {
+				return nil
+			}
+		}
+		return fmt.Errorf("no connected neighbor was found with %s", nodeLLA)
+	}, 2*time.Minute, 10*time.Second).ShouldNot(HaveOccurred(),
+		"timed out waiting to validate nodes peered with the frr instance")
+}
+
+// validatePeerRoutesViaDevice validates that the peer has BGP routes to the
+// specified prefixes with the expected next-hop address on the specified device.
+func validatePeerRoutesViaDevice(peer executor.Executor, dev, nextHop string, prefixes ...[]string) {
+	ginkgo.By(fmt.Sprintf("validating prefix %s to %s dev %s", prefixes, nextHop, dev))
+	Eventually(func() error {
+		nextHopAddr := netip.MustParseAddr(nextHop)
+		want := make(map[netip.Prefix]map[netip.Addr]struct{})
+		for _, prf := range prefixes {
+			for _, p := range prf {
+				want[netip.MustParsePrefix(p)] = map[netip.Addr]struct{}{nextHopAddr: {}}
+			}
+		}
+
+		got, err := container.BGPRoutes(peer, dev)
+		if err != nil {
+			return err
+		}
+		if !cmp.Equal(want, got) {
+			return fmt.Errorf("want %v\n got %v\n diff %v", want, got, cmp.Diff(want, got))
+		}
+		return nil
+	}, 30*time.Second, 5*time.Second).ShouldNot(HaveOccurred(), fmt.Sprintf("peer should have the routes %s", prefixes))
+}
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index 7fa8367b..5dae4350 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -133,6 +133,7 @@ var _ = ginkgo.BeforeSuite(func() {
 		bgptests.FRRContainers, err = bgptests.HostContainerSetup(frrImage, hostBGPMode)
 		Expect(err).NotTo(HaveOccurred())
 	default:
+		bgptests.FRRImage = frrImage
 		bgptests.FRRContainers, err = bgptests.KindnetContainersSetup(cs, frrImage)
 		Expect(err).NotTo(HaveOccurred())
 		if withVRF {
diff --git a/e2etest/go.mod b/e2etest/go.mod
index 86af1371..d4079d3c 100644
--- a/e2etest/go.mod
+++ b/e2etest/go.mod
@@ -55,7 +55,7 @@ require (
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/glog v1.0.0 // indirect
+	github.com/golang/glog v1.2.4 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
diff --git a/e2etest/go.sum b/e2etest/go.sum
index a7068993..66eb5c66 100644
--- a/e2etest/go.sum
+++ b/e2etest/go.sum
@@ -61,8 +61,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
+github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
+github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
diff --git a/e2etest/go.work.sum b/e2etest/go.work.sum
index 3fed727b..f85097d6 100644
--- a/e2etest/go.work.sum
+++ b/e2etest/go.work.sum
@@ -648,6 +648,7 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -680,6 +681,7 @@ github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6
 github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
 github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
 github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -697,6 +699,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
 github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
@@ -716,7 +719,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
@@ -780,6 +782,7 @@ github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHz
 github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
 github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
 github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -797,6 +800,7 @@ github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLO
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -804,6 +808,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+github.com/metallb/frr-k8s v0.0.0-20241217094531-80d4b7a5591b h1:3+sSwasbGnVDjF84TRsxlCjbgko9tkDbw/kaj+NKPD4=
+github.com/metallb/frr-k8s v0.0.0-20241217094531-80d4b7a5591b/go.mod h1:gcOVS4IiQ7r6SRKc2HBgYll3Ku9SHEatxaqgNiuC/Tg=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
 github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
@@ -841,6 +847,7 @@ github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xl
 github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
 github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
 github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
 github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
 github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
@@ -850,6 +857,7 @@ github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P
 github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
 github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk=
 github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
 github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
 github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
 github.com/open-policy-agent/cert-controller v0.10.1/go.mod h1:4uRbBLY5DsPOog+a9pqk3JLxuuhrWsbUedQW65HcLTI=
@@ -893,6 +901,8 @@ github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr
 github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
 github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
 github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
 github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
 github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
 github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
@@ -904,6 +914,7 @@ github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO
 github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
 github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
 github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
 github.com/prometheus/exporter-toolkit v0.7.1 h1:c6RXaK8xBVercEeUQ4tRNL8UGWzDHfvj9dseo1FcK1Y=
 github.com/prometheus/exporter-toolkit v0.7.1/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -976,6 +987,7 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@@ -1182,6 +1194,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1210,6 +1223,7 @@ golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1250,9 +1264,11 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
 golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1267,6 +1283,9 @@ golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQ
 golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
 golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
 golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1343,6 +1362,7 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
@@ -1350,6 +1370,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -1362,13 +1383,16 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -1554,7 +1578,9 @@ google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -1659,6 +1685,7 @@ k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/s
 k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo=
 k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
+k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
 k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
@@ -1679,6 +1706,7 @@ k8s.io/kube-aggregator v0.29.3/go.mod h1:xGJqV/SJJ1fbwTGfQLAZfwgqX1EMoaqfotDTkDr
 k8s.io/kube-controller-manager v0.29.3/go.mod h1:Tf+BQG52nu8f2Ra9he9MsNFJ2sK7U3TSVvmTB+OVgiw=
 k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
 k8s.io/kube-proxy v0.29.3/go.mod h1:VyAG5i+d+crJ0WM6Ik/4wSq+VttAFDfD4n3z+0hEQl8=
 k8s.io/kube-scheduler v0.29.3/go.mod h1:1NLHViSwFFddWHH4U9UGD57clINAtje/PEs6PjOYQZg=
 k8s.io/kubectl v0.29.3 h1:RuwyyIU42MAISRIePaa8Q7A3U74Q9P4MoJbDFz9o3us=
@@ -1701,6 +1729,8 @@ k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
@@ -1713,8 +1743,11 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
 sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s=
 sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3/go.mod h1:/d88dHCvoy7d0AKFT0yytezSGZKjsZBVs9YTkBHSGFk=
 sigs.k8s.io/kustomize/kustomize/v5 v5.4.2/go.mod h1:5ypfJVYlPb2MKKeoGknVLxvHemDlQT+szI4+KOhnD6k=
 sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
 sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/e2etest/l2tests/assignment.go b/e2etest/l2tests/assignment.go
index 6c88c5a9..d74bc876 100644
--- a/e2etest/l2tests/assignment.go
+++ b/e2etest/l2tests/assignment.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"go.universe.tf/e2etest/pkg/config"
@@ -21,6 +22,7 @@ import (
 	jigservice "go.universe.tf/e2etest/pkg/jigservice"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	clientset "k8s.io/client-go/kubernetes"
 	admissionapi "k8s.io/pod-security-admission/api"
 	"k8s.io/utils/ptr"
@@ -949,13 +951,12 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 		})
 		ginkgo.It("When current dualstack pool becomes single-stack, svc should pick another dualstack pool if possible", func() {
 			pool1 := metallbv1beta1.IPAddressPool{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-dualstack-pool"},
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-dualstack-pool-to-edit"},
 				Spec: metallbv1beta1.IPAddressPoolSpec{
 					Addresses: []string{
 						v4PoolAddresses,
 						v6PoolAddresses,
 					},
-					AllocateTo: &metallbv1beta1.ServiceAllocation{Namespaces: []string{testNamespace}},
 				},
 			}
 			resources := config.Resources{
@@ -985,13 +986,27 @@ 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 have an additional dualstack pool")
+			ginkgo.By("Adding a 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")
+			ginkgo.By("Verifying that the dualstack pool was loaded")
+			Eventually(func() error {
+				pool := metallbv1beta1.IPAddressPool{}
+				err := ConfigUpdater.Client().Get(context.TODO(), types.NamespacedName{Name: poolDual.Name, Namespace: ConfigUpdater.Namespace()}, &pool)
+				if err != nil {
+					return err
+				}
+
+				if pool.Status.AvailableIPv6 == 0 {
+					return fmt.Errorf("pool %s was not loaded, status is: %v", poolDual.Name, pool.Status)
+				}
+
+				return nil
+			}, 30*time.Second, 1*time.Second).Should(Not(HaveOccurred()))
+			ginkgo.By("Updating pool1 to exclude ipv4 address")
 			pool1.Spec.Addresses = []string{v6PoolAddresses}
 			resources = config.Resources{
 				Pools: []metallbv1beta1.IPAddressPool{pool1, poolDual},
@@ -1002,7 +1017,7 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 			Eventually(func() []string {
 				newIps := getServiceIps(cs, svc1.Namespace, svc1.Name)
 				return newIps
-			}, 5*time.Minute, 1*time.Second).Should(And(
+			}, 30*time.Second, 1*time.Second).Should(And(
 				HaveLen(2),
 				Not(Equal(originallyAssignedIps)),
 			))
@@ -1015,6 +1030,102 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 			Expect(err).NotTo(HaveOccurred())
 		})
 	})
+
+	ginkgo.Context("Pool Status", func() {
+		ginkgo.It("DUALSTACK", func() {
+			testPool := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "status-pool",
+					Namespace: metallb.Namespace,
+				},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.10.0/30",
+						"fc00:f853:0ccd:e799::/126",
+					},
+				},
+			}
+			validateStatus := func(expected metallbv1beta1.IPAddressPoolStatus) {
+				Eventually(func() error {
+					p := metallbv1beta1.IPAddressPool{}
+					err := ConfigUpdater.Client().Get(context.Background(), types.NamespacedName{Name: testPool.Name, Namespace: testPool.Namespace}, &p)
+					if err != nil {
+						return err
+					}
+
+					if !cmp.Equal(p.Status, expected) {
+						return fmt.Errorf("pool does not have the expected status (-want +got)\n %v", cmp.Diff(expected, p.Status))
+					}
+
+					return nil
+				}, 30*time.Second, time.Second).ShouldNot(HaveOccurred())
+			}
+
+			ginkgo.By("Creating a Dualstack pool the status should be populated")
+			resources := config.Resources{
+				Pools: []metallbv1beta1.IPAddressPool{testPool},
+			}
+			err := ConfigUpdater.Update(resources)
+			Expect(err).ToNot(HaveOccurred())
+
+			expectedStatus := metallbv1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 4,
+				AvailableIPv6: 4,
+				AssignedIPv4:  0,
+				AssignedIPv6:  0,
+			}
+			validateStatus(expectedStatus)
+
+			ginkgo.By("Creating a service the pool status should be updated")
+			svc1, _ := service.CreateWithBackend(cs, testNamespace, "status-svc", service.DualStack)
+
+			expectedStatus = metallbv1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 3,
+				AvailableIPv6: 3,
+				AssignedIPv4:  1,
+				AssignedIPv6:  1,
+			}
+			validateStatus(expectedStatus)
+
+			ginkgo.By("Expanding the pool's addresses the status should be updated")
+			testPool.Spec.Addresses = []string{"192.168.10.0/29", "fc00:f853:0ccd:e799::/125"}
+			resources = config.Resources{
+				Pools: []metallbv1beta1.IPAddressPool{testPool},
+			}
+			err = ConfigUpdater.Update(resources)
+			Expect(err).ToNot(HaveOccurred())
+			expectedStatus = metallbv1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 7,
+				AvailableIPv6: 7,
+				AssignedIPv4:  1,
+				AssignedIPv6:  1,
+			}
+			validateStatus(expectedStatus)
+
+			ginkgo.By("Deleting the service the status should be updated")
+			service.Delete(cs, svc1)
+			expectedStatus = metallbv1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 8,
+				AvailableIPv6: 8,
+				AssignedIPv4:  0,
+				AssignedIPv6:  0,
+			}
+			validateStatus(expectedStatus)
+
+			ginkgo.By("Manually updating the status it should be reverted")
+			err = ConfigUpdater.Client().Get(context.Background(), types.NamespacedName{Name: testPool.Name, Namespace: testPool.Namespace}, &testPool)
+			Expect(err).ToNot(HaveOccurred())
+			testPool.Status = metallbv1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 5,
+				AvailableIPv6: 5,
+				AssignedIPv4:  5,
+				AssignedIPv6:  5,
+			}
+			err = ConfigUpdater.Client().Status().Update(context.TODO(), &testPool)
+			Expect(err).ToNot(HaveOccurred())
+			validateStatus(expectedStatus)
+		})
+	})
 })
 
 // updateServiceIPFamilyPolicy updates the ipFamilyPolicy of the given svc with the given policy.
diff --git a/e2etest/pkg/config/update.go b/e2etest/pkg/config/update.go
index b44380a0..64244cfd 100644
--- a/e2etest/pkg/config/update.go
+++ b/e2etest/pkg/config/update.go
@@ -126,7 +126,7 @@ func (o beta1Updater) Update(r Resources) error {
 	// Iterating over the map will return the items in a random order.
 	for i, obj := range objects {
 		obj.SetNamespace(o.namespace)
-		_, err := controllerutil.CreateOrUpdate(context.Background(), o.cli, obj, func() error {
+		_, err := controllerutil.CreateOrPatch(context.Background(), o.cli, obj, func() error {
 			// the mutate function is expected to change the object when updating.
 			// we always override with the old version, and we change only the spec part.
 			switch toChange := obj.(type) {
diff --git a/e2etest/pkg/container/routes.go b/e2etest/pkg/container/routes.go
index 8c2ebfd5..45561c9f 100644
--- a/e2etest/pkg/container/routes.go
+++ b/e2etest/pkg/container/routes.go
@@ -134,30 +134,80 @@ func WireContainers(containerA, containerB, dev string) error {
 	return nil
 }
 
+type Nexthop struct {
+	Gateway string `json:"gateway"`
+	Dev     string `json:"dev"`
+}
+
+type IPRoute struct {
+	Dst      string    `json:"dst"`
+	Nexthops []Nexthop `json:"nexthops,omitempty"`
+	Gateway  string    `json:"gateway,omitempty"`
+	Via      *struct {
+		Family string `json:"family"`
+		Host   string `json:"host"`
+	} `json:"via,omitempty"`
+	Dev string `json:"dev"`
+}
+
+func parseIPRouteOutput(r IPRoute, dev string) (netip.Prefix, map[netip.Addr]struct{}, error) {
+	dst, err := netip.ParsePrefix(r.Dst)
+	if err != nil {
+		// `ip route` command does not print the prefix when /32 or /128
+		addr, err := netip.ParseAddr(r.Dst)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid prefix %s: %w", r.Dst, err)
+		}
+		dst, err = netip.ParsePrefix(r.Dst + "/32")
+		if addr.Is6() {
+			dst, err = netip.ParsePrefix(r.Dst + "/128")
+		}
+	}
+
+	nextHops := make(map[netip.Addr]struct{})
+	// this is for ipv4 single next-hop
+	if r.Via != nil {
+		addr, err := netip.ParseAddr(r.Via.Host)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid next-hop %s: %w", r.Via.Host, err)
+		}
+		if dev == "" || r.Dev == dev {
+			nextHops[addr] = struct{}{}
+		}
+	}
+
+	// this is for ipv4 multiple next-hops
+	for _, nh := range r.Nexthops {
+		addr, err := netip.ParseAddr(nh.Gateway)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid next-hop %s: %w", nh.Gateway, err)
+		}
+
+		if dev == "" || nh.Dev == dev {
+			nextHops[addr] = struct{}{}
+		}
+	}
+
+	// this is for ipv6
+	if r.Gateway != "" {
+		addr, err := netip.ParseAddr(r.Gateway)
+		if err != nil {
+			return netip.Prefix{}, nil, fmt.Errorf("invalid next-hop %s: %w", r.Gateway, err)
+		}
+		if dev == "" || r.Dev == dev {
+			nextHops[addr] = struct{}{}
+		}
+	}
+
+	return dst, nextHops, nil
+}
+
 // BGPRoutes executes `ip route show proto bgp` in the executor and returns all
 // routes filtered by device name e.g. net0. If device name is the empty string
 // no filtering takes place. The return is map[destination CIDR]-> set[nextHops].
 func BGPRoutes(exc executor.Executor, dev string) (map[netip.Prefix]map[netip.Addr]struct{}, error) {
 	ret := make(map[netip.Prefix]map[netip.Addr]struct{})
 
-	type Nexthop struct {
-		Gateway string   `json:"gateway"`
-		Dev     string   `json:"dev"`
-		Weight  int      `json:"weight"`
-		Flags   []string `json:"flags"`
-	}
-
-	type IPRoute struct {
-		Dst      string    `json:"dst"`
-		Nexthops []Nexthop `json:"nexthops,omitempty"`
-		Gateway  string    `json:"gateway,omitempty"`
-		Via      *struct {
-			Family string `json:"family"`
-			Host   string `json:"host"`
-		} `json:"via,omitempty"`
-		Dev string `json:"dev"`
-	}
-
 	for _, proto := range []string{"-4", "-6"} {
 		out, err := exc.Exec("ip", proto, "--json", "route", "show", "proto", "bgp")
 		if err != nil {
@@ -171,49 +221,15 @@ func BGPRoutes(exc executor.Executor, dev string) (map[netip.Prefix]map[netip.Ad
 		}
 
 		for _, r := range routes {
-			dst, err := netip.ParsePrefix(r.Dst)
+			dst, nextHops, err := parseIPRouteOutput(r, dev)
 			if err != nil {
-				return nil, fmt.Errorf("invalid prefix %s: %w", r.Dst, err)
-			}
-
-			nextHops := make(map[netip.Addr]struct{})
-			// this is for ipv4 single next-hop
-			if r.Via != nil {
-				addr, err := netip.ParseAddr(r.Via.Host)
-				if err != nil {
-					return nil, fmt.Errorf("invalid next-hop %s: %w", r.Via.Host, err)
-				}
-				if dev == "" || r.Dev == dev {
-					nextHops[addr] = struct{}{}
-				}
-			}
-
-			// this is for ipv4 multiple next-hops
-			for _, nh := range r.Nexthops {
-				addr, err := netip.ParseAddr(nh.Gateway)
-				if err != nil {
-					return nil, fmt.Errorf("invalid next-hop %s: %w", nh.Gateway, err)
-				}
-
-				if dev == "" || nh.Dev == dev {
-					nextHops[addr] = struct{}{}
-				}
-			}
-
-			// this is for ipv6
-			if r.Gateway != "" {
-				addr, err := netip.ParseAddr(r.Gateway)
-				if err != nil {
-					return nil, fmt.Errorf("invalid next-hop %s: %w", r.Gateway, err)
-				}
-				if dev == "" || r.Dev == dev {
-					nextHops[addr] = struct{}{}
-				}
+				return nil, err
 			}
 
 			if len(nextHops) == 0 {
 				continue
 			}
+
 			ret[dst] = nextHops
 		}
 	}
diff --git a/e2etest/webhookstests/webhooks.go b/e2etest/webhookstests/webhooks.go
index 166788c3..3e391973 100644
--- a/e2etest/webhookstests/webhooks.go
+++ b/e2etest/webhookstests/webhooks.go
@@ -21,6 +21,7 @@ package webhookstests
 import (
 	"context"
 	"fmt"
+	"net"
 
 	"go.universe.tf/e2etest/pkg/config"
 	"go.universe.tf/e2etest/pkg/k8s"
@@ -30,9 +31,12 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/openshift-kni/k8sreporter"
 
+	"go.universe.tf/e2etest/pkg/ipfamily"
+	"go.universe.tf/e2etest/pkg/k8sclient"
 	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
 	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientset "k8s.io/client-go/kubernetes"
 	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
@@ -44,7 +48,10 @@ var (
 )
 
 var _ = ginkgo.Describe("Webhooks", func() {
+	var cs clientset.Interface
 	ginkgo.BeforeEach(func() {
+		cs = k8sclient.New()
+
 		ginkgo.By("Clearing any previous configuration")
 		err := ConfigUpdater.Clean()
 		Expect(err).NotTo(HaveOccurred())
@@ -106,6 +113,45 @@ var _ = ginkgo.Describe("Webhooks", func() {
 			Expect(err).To(HaveOccurred())
 			Expect(err.Error()).To(ContainSubstring("overlaps with already defined CIDR"))
 		})
+
+		ginkgo.DescribeTable("IPAddressPool with overlapping addresses of the nodes",
+			func(ipFamily ipfamily.Family) {
+				nodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+				Expect(err).NotTo(HaveOccurred())
+				nodeIps, err := k8s.NodeIPsForFamily(nodes.Items, ipFamily, "")
+				Expect(err).NotTo(HaveOccurred())
+				Expect(len(nodeIps)).To(BeNumerically(">", 0), "empty node ips list")
+				nodeIP := net.ParseIP(nodeIps[0])
+				cidr := &net.IPNet{
+					IP:   nodeIP,
+					Mask: net.CIDRMask(32, 32),
+				}
+				if ipFamily == ipfamily.IPv6 {
+					cidr.Mask = net.CIDRMask(128, 128)
+				}
+
+				ginkgo.By("Creating IPAddressPool")
+				var nodeCIDRs []string
+				nodeCIDRs = append(nodeCIDRs, cidr.String())
+				resources := config.Resources{
+					Pools: []metallbv1beta1.IPAddressPool{
+						{
+							ObjectMeta: metav1.ObjectMeta{
+								Name: "webhooks-test3",
+							},
+							Spec: metallbv1beta1.IPAddressPoolSpec{
+								Addresses: nodeCIDRs,
+							},
+						},
+					},
+				}
+				err = ConfigUpdater.Update(resources)
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(ContainSubstring("contains nodeIp"))
+			},
+			ginkgo.Entry("IPV4", ipfamily.IPv4),
+			ginkgo.Entry("IPV6", ipfamily.IPv6),
+		)
 	})
 
 	ginkgo.Context("for BGPAdvertisement", func() {
diff --git a/internal/allocator/allocator_test.go b/internal/allocator/allocator_test.go
index d97d4b4a..35e2e0ac 100644
--- a/internal/allocator/allocator_test.go
+++ b/internal/allocator/allocator_test.go
@@ -3,6 +3,7 @@
 package allocator
 
 import (
+	"fmt"
 	"math"
 	"net"
 	"reflect"
@@ -16,6 +17,7 @@ import (
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/sets"
 
+	"github.com/google/go-cmp/cmp"
 	ptu "github.com/prometheus/client_golang/prometheus/testutil"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
@@ -76,8 +78,10 @@ func selector(s string) labels.Selector {
 	return ret
 }
 
+func noopCallback(_ string) {}
+
 func TestAssignment(t *testing.T) {
-	alloc := New()
+	alloc := New(noopCallback)
 	alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test": {
 			Name:       "test",
@@ -682,7 +686,7 @@ func TestAssignment(t *testing.T) {
 }
 
 func TestPoolAllocation(t *testing.T) {
-	alloc := New()
+	alloc := New(noopCallback)
 	// Majority of this test only allocates from the "test" pool,
 	// so it will run out of IPs quickly even though there are
 	// tons available in other pools.
@@ -1245,7 +1249,7 @@ func TestPoolAllocation(t *testing.T) {
 }
 
 func TestAllocation(t *testing.T) {
-	alloc := New()
+	alloc := New(noopCallback)
 	alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test1": {
 			Name:       "test1",
@@ -1616,7 +1620,7 @@ func TestAllocation(t *testing.T) {
 }
 
 func TestBuggyIPs(t *testing.T) {
-	alloc := New()
+	alloc := New(noopCallback)
 	alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test": {
 			Name:       "test",
@@ -1800,7 +1804,7 @@ func TestConfigReload(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		alloc := New()
+		alloc := New(noopCallback)
 		alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 			"test": {
 				Name:       "test",
@@ -1833,7 +1837,7 @@ func TestConfigReload(t *testing.T) {
 }
 
 func TestAutoAssign(t *testing.T) {
-	alloc := New()
+	alloc := New(noopCallback)
 	alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test1": {
 			Name:       "test1",
@@ -2105,7 +2109,9 @@ func TestPoolCount(t *testing.T) {
 }
 
 func TestPoolMetrics(t *testing.T) {
-	alloc := New()
+	callbackCounter := 0
+	callback := func(_ string) { callbackCounter++ }
+	alloc := New(callback)
 	alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test": {
 			Name:       "test",
@@ -2209,14 +2215,32 @@ func TestPoolMetrics(t *testing.T) {
 	if int(value) != 8 {
 		t.Errorf("stats.poolCapacity invalid %f. Expected 8", value)
 	}
+	counters := alloc.CountersForPool("test")
+	err := validateCounters(counters, 4, 4, 0, 0)
+	if err != nil {
+		t.Error(err)
+	}
+	expectedCallbackCounter := 1
+	if callbackCounter != expectedCallbackCounter {
+		t.Errorf("callbackCounter invalid %d. Expected %d", callbackCounter, expectedCallbackCounter)
+	}
 
 	for _, test := range tests {
+		expectedCallbackCounter++
 		if len(test.ips) == 0 {
 			alloc.Unassign(test.svcKey)
 			value := ptu.ToFloat64(stats.poolActive.WithLabelValues("test"))
 			if value != test.ipsInUse {
 				t.Errorf("%v; in-use %v. Expected %v", test.desc, value, test.ipsInUse)
 			}
+			counters = alloc.CountersForPool("test")
+			err = validateCounters(counters, 4-int64(test.ipsInUse), 4, int64(test.ipsInUse), 0)
+			if err != nil {
+				t.Error(err)
+			}
+			if callbackCounter != expectedCallbackCounter {
+				t.Errorf("callbackCounter invalid %d. Expected %d", callbackCounter, expectedCallbackCounter)
+			}
 			continue
 		}
 
@@ -2238,6 +2262,29 @@ func TestPoolMetrics(t *testing.T) {
 		if value != test.ipsInUse {
 			t.Errorf("%v; in-use %v. Expected %v", test.desc, value, test.ipsInUse)
 		}
+		counters = alloc.CountersForPool("test")
+		err = validateCounters(counters, 4-int64(test.ipsInUse), 4, int64(test.ipsInUse), 0)
+		if err != nil {
+			t.Error(err)
+		}
+		if callbackCounter != expectedCallbackCounter {
+			t.Errorf("callbackCounter invalid %d. Expected %d", callbackCounter, expectedCallbackCounter)
+		}
+	}
+	ips := []net.IP{net.ParseIP("1.2.3.4")}
+	err = alloc.Assign("s1", svc, ips, []Port{}, "", "")
+	if err != nil {
+		t.Errorf("assign failed: %v", err)
+	}
+	alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{}})
+	val := ptu.ToFloat64(stats.poolActive.WithLabelValues("test")) // we actually expect the value to not be there, but since we can't access it this recreates it with 0
+	if val != 0 {
+		t.Errorf("expected value to be 0, got %v", val)
+	}
+	counters = alloc.CountersForPool("test")
+	err = validateCounters(counters, 0, 0, 0, 0)
+	if err != nil {
+		t.Error(err)
 	}
 }
 
@@ -2296,3 +2343,17 @@ func compareIPs(ips1, ips2 []string) bool {
 	}
 	return true
 }
+
+//nolint:unparam
+func validateCounters(c PoolCounters, avIPv4, avIPv6, asIPv4, asIPv6 int64) error {
+	n := PoolCounters{
+		AvailableIPv4: avIPv4,
+		AvailableIPv6: avIPv6,
+		AssignedIPv4:  asIPv4,
+		AssignedIPv6:  asIPv6,
+	}
+	if !cmp.Equal(c, n) {
+		return fmt.Errorf("unexpected pool counters (-want +got):\n%s", cmp.Diff(n, c))
+	}
+	return nil
+}
diff --git a/internal/bgp/frr/frr_bfd_test.go b/internal/bgp/frr/frr_bfd_test.go
index e7cb6a0f..b2bc0779 100644
--- a/internal/bgp/frr/frr_bfd_test.go
+++ b/internal/bgp/frr/frr_bfd_test.go
@@ -124,7 +124,8 @@ func TestBFDWithSession(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index 0bcdfd4a..ff4fd07c 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -119,7 +119,8 @@ func TestSingleEBGPSessionMultiHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -146,7 +147,8 @@ func TestSingleEBGPSessionOneHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "127.0.0.2:179",
+			PeerAddress:   "127.0.0.2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -174,7 +176,8 @@ func TestSingleIPv6EBGPSessionOneHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[127:0:0::2]:179",
+			PeerAddress:   "127:0:0::2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -202,7 +205,8 @@ func TestSingleIBGPSession(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -230,7 +234,8 @@ func TestSingleIPv6IBGPSession(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -259,7 +264,8 @@ func TestSingleSessionClose(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -287,7 +293,8 @@ func TestSingleSessionWithGracefulRestart(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           102,
 			RouterID:        net.ParseIP("10.1.1.254"),
@@ -311,7 +318,8 @@ func TestTwoSessions(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -330,7 +338,8 @@ func TestTwoSessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -360,7 +369,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -377,7 +387,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:4:4::255]:179",
+			PeerAddress:   "10:4:4::255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:3:3::254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -405,7 +416,8 @@ func TestIPv4AndIPv6SessionsDisableMP(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -424,7 +436,8 @@ func TestIPv4AndIPv6SessionsDisableMP(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -452,7 +465,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -470,7 +484,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -498,7 +513,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -515,7 +531,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -543,7 +560,8 @@ func TestSingleAdvertisement(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -591,7 +609,8 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      nil,
@@ -625,34 +644,6 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	testCheckConfigFile(t)
 }
 
-func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
-	testSetup(t)
-
-	l := log.NewNopLogger()
-	sessionManager := mockNewSessionManager(l, logging.LevelInfo)
-	defer close(sessionManager.reloadConfig)
-	session, err := sessionManager.NewSession(l,
-		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254",
-			SourceAddress: net.ParseIP("10.1.1.254"),
-			MyASN:         100,
-			RouterID:      net.ParseIP("10.1.1.254"),
-			PeerASN:       200,
-			HoldTime:      ptr.To(time.Second),
-			KeepAliveTime: ptr.To(time.Second),
-			Password:      "password",
-			CurrentNode:   "hostname",
-			EBGPMultiHop:  true,
-			SessionName:   "test-peer"})
-
-	if err == nil {
-		session.Close()
-		t.Fatalf("Should not be able to create session")
-	}
-
-	// Not checking the file since this test won't create it
-}
-
 func TestSingleAdvertisementStop(t *testing.T) {
 	testSetup(t)
 
@@ -661,7 +652,8 @@ func TestSingleAdvertisementStop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -708,7 +700,8 @@ func TestSingleAdvertisementChange(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -763,7 +756,8 @@ func TestTwoAdvertisements(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -816,7 +810,8 @@ func TestTwoAdvertisementsDuplicate(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -863,7 +858,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 		func() {
 			sessionsParameters := []bgp.SessionParameters{
 				{
-					PeerAddress:   "10.2.2.254:179",
+					PeerAddress:   "10.2.2.254",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -876,7 +872,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					EBGPMultiHop:  true,
 					SessionName:   "test-peer"},
 				{
-					PeerAddress:   "10.2.2.255:179",
+					PeerAddress:   "10.2.2.255",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -965,7 +962,8 @@ func TestSingleSessionExtras(t *testing.T) {
 	}
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "127.0.0.2:179",
+			PeerAddress:   "127.0.0.2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -1046,7 +1044,8 @@ func TestLargeCommunities(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -1095,7 +1094,8 @@ func TestManyAdvertisementsSameCommunity(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -1143,7 +1143,8 @@ func TestSingleSessionWithNoTimers(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       102,
 			PeerASN:     100,
 			SessionName: "test-peer"})
@@ -1164,7 +1165,8 @@ func TestSingleSessionWithZeroTimers(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         102,
 			HoldTime:      ptr.To(0 * time.Second),
 			KeepAliveTime: ptr.To(0 * time.Second),
@@ -1366,7 +1368,8 @@ func TestSingleSessionWithInternalASN(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           102,
 			RouterID:        net.ParseIP("10.1.1.254"),
@@ -1390,7 +1393,33 @@ func TestSingleSessionWithExternalASN(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
+			SourceAddress:   net.ParseIP("10.1.1.254"),
+			MyASN:           102,
+			RouterID:        net.ParseIP("10.1.1.254"),
+			DynamicASN:      "external",
+			GracefulRestart: true,
+			SessionName:     "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
+
+func TestUnnumberedSession(t *testing.T) {
+	testSetup(t)
+
+	l := log.NewNopLogger()
+	sessionManager := mockNewSessionManager(l, logging.LevelInfo)
+	defer close(sessionManager.reloadConfig)
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerInterface:   "net0",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           102,
 			RouterID:        net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frr/frr_vrf_test.go b/internal/bgp/frr/frr_vrf_test.go
index c21a1e1f..3d1dc7f9 100644
--- a/internal/bgp/frr/frr_vrf_test.go
+++ b/internal/bgp/frr/frr_vrf_test.go
@@ -22,7 +22,8 @@ func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -50,7 +51,8 @@ func TestSingleVRFIBGPSession(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -79,7 +81,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -97,7 +100,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -126,7 +130,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -144,7 +149,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -173,7 +179,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -189,7 +196,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -216,7 +224,8 @@ func TestSingleAdvertisementVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -264,7 +273,8 @@ func TestSingleAdvertisementChangeVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -320,7 +330,8 @@ func TestTwoAdvertisementsVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -374,7 +385,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 	defer close(sessionManager.reloadConfig)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -392,7 +404,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.255:179",
+			PeerAddress:   "10.2.2.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frr/testdata/TestUnnumberedSession.golden b/internal/bgp/frr/testdata/TestUnnumberedSession.golden
new file mode 100644
index 00000000..2319ea93
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestUnnumberedSession.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 net0-in deny 20
+
+
+
+
+ip prefix-list net0-pl-dual seq 1 deny any
+ipv6 prefix-list net0-pl-dual seq 2 deny any
+
+route-map net0-out permit 1
+  match ip address prefix-list net0-pl-dual
+route-map net0-out permit 2
+  match ipv6 address prefix-list net0-pl-dual
+
+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 net0 interface remote-as external
+  neighbor net0 port 179
+  
+  neighbor net0 update-source 10.1.1.254
+  neighbor net0 graceful-restart
+
+  address-family ipv4 unicast
+    neighbor net0 activate
+    neighbor net0 route-map net0-in in
+    neighbor net0 route-map net0-out out
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor net0 activate
+    neighbor net0 route-map net0-in in
+    neighbor net0 route-map net0-out out
+  exit-address-family
+
diff --git a/internal/bgp/frrk8s/frrk8s_bfd_test.go b/internal/bgp/frrk8s/frrk8s_bfd_test.go
index 075e0f92..f98092cf 100644
--- a/internal/bgp/frrk8s/frrk8s_bfd_test.go
+++ b/internal/bgp/frrk8s/frrk8s_bfd_test.go
@@ -112,7 +112,8 @@ func TestBFDWithSession(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frrk8s/frrk8s_test.go b/internal/bgp/frrk8s/frrk8s_test.go
index a7cafdd4..9c044939 100644
--- a/internal/bgp/frrk8s/frrk8s_test.go
+++ b/internal/bgp/frrk8s/frrk8s_test.go
@@ -121,7 +121,8 @@ func TestSingleEBGPSessionGracefullRestart(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:     "10.2.2.254:179",
+			PeerAddress:     "10.2.2.254",
+			PeerPort:        179,
 			SourceAddress:   net.ParseIP("10.1.1.254"),
 			MyASN:           100,
 			RouterID:        net.ParseIP("10.1.1.254"),
@@ -146,7 +147,8 @@ func TestSingleEBGPSessionMultiHop(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -170,7 +172,8 @@ func TestSingleEBGPSessionOneHop(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "127.0.0.2:179",
+			PeerAddress:   "127.0.0.2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -195,7 +198,8 @@ func TestSingleIPv6EBGPSessionOneHop(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[127:0:0::2]:179",
+			PeerAddress:   "127:0:0::2",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -220,7 +224,8 @@ func TestSingleIBGPSession(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -245,7 +250,8 @@ func TestSingleIPv6IBGPSession(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -270,7 +276,8 @@ func TestSingleSessionClose(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -294,7 +301,8 @@ func TestTwoSessions(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -312,7 +320,8 @@ func TestTwoSessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -341,7 +350,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:2:2::254]:179",
+			PeerAddress:   "10:2:2::254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:1:1::254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -358,7 +368,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "[10:4:4::255]:179",
+			PeerAddress:   "10:4:4::255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10:3:3::254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -382,7 +393,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -400,7 +412,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -425,7 +438,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -442,7 +456,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -467,7 +482,8 @@ func TestSingleAdvertisement(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -511,7 +527,8 @@ func TestTwoAdvertisementsWithSamePrefix(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -564,7 +581,8 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	sessionManager := newTestSessionManager(t)
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      nil,
@@ -598,39 +616,14 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 	testCheckConfigFile(t)
 }
 
-func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
-	sessionManager := newTestSessionManager(t)
-
-	l := log.NewNopLogger()
-	session, err := sessionManager.NewSession(l,
-		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254",
-			SourceAddress: net.ParseIP("10.1.1.254"),
-			MyASN:         100,
-			RouterID:      net.ParseIP("10.1.1.254"),
-			PeerASN:       200,
-			HoldTime:      ptr.To(time.Second),
-			KeepAliveTime: ptr.To(time.Second),
-			Password:      "password",
-			CurrentNode:   "hostname",
-			EBGPMultiHop:  true,
-			SessionName:   "test-peer"})
-
-	if err == nil {
-		session.Close()
-		t.Fatalf("Should not be able to create session")
-	}
-
-	// Not checking the file since this test won't create it
-}
-
 func TestSingleAdvertisementStop(t *testing.T) {
 	l := log.NewNopLogger()
 	sessionManager := newTestSessionManager(t)
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -675,7 +668,8 @@ func TestSingleAdvertisementChange(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -728,7 +722,8 @@ func TestTwoAdvertisements(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -781,7 +776,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 		func() {
 			sessionsParameters := []bgp.SessionParameters{
 				{
-					PeerAddress:   "10.2.2.254:179",
+					PeerAddress:   "10.2.2.254",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -793,7 +789,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					EBGPMultiHop:  true,
 					SessionName:   "test-peer"},
 				{
-					PeerAddress:   "10.2.2.255:179",
+					PeerAddress:   "10.2.2.255",
+					PeerPort:      179,
 					SourceAddress: net.ParseIP("10.1.1.254"),
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
@@ -874,7 +871,8 @@ func TestLargeCommunities(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -921,7 +919,8 @@ func TestSingleSessionWithNoTimers(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       102,
 			PeerASN:     100,
 			SessionName: "test-peer"})
@@ -940,7 +939,8 @@ func TestSingleSessionWithZeroTimers(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         102,
 			HoldTime:      ptr.To(0 * time.Second),
 			KeepAliveTime: ptr.To(0 * time.Second),
@@ -961,7 +961,8 @@ func TestSingleSessionWithInternalASN(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       100,
 			RouterID:    net.ParseIP("10.1.1.254"),
 			DynamicASN:  "internal",
@@ -981,7 +982,8 @@ func TestSingleSessionWithExternalASN(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress: "10.2.2.254:179",
+			PeerAddress: "10.2.2.254",
+			PeerPort:    179,
 			MyASN:       100,
 			RouterID:    net.ParseIP("10.1.1.254"),
 			DynamicASN:  "external",
@@ -994,3 +996,24 @@ func TestSingleSessionWithExternalASN(t *testing.T) {
 
 	testCheckConfigFile(t)
 }
+
+func TestSingleSessionWithInterface(t *testing.T) {
+	sessionManager := newTestSessionManager(t)
+	l := log.NewNopLogger()
+
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerPort:      179,
+			PeerInterface: "net0",
+			MyASN:         100,
+			RouterID:      net.ParseIP("10.1.1.254"),
+			DynamicASN:    "internal",
+			CurrentNode:   "hostname",
+			SessionName:   "test-peer"})
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
diff --git a/internal/bgp/frrk8s/frrk8s_vrf_test.go b/internal/bgp/frrk8s/frrk8s_vrf_test.go
index 681cbfe5..d89c4aec 100644
--- a/internal/bgp/frrk8s/frrk8s_vrf_test.go
+++ b/internal/bgp/frrk8s/frrk8s_vrf_test.go
@@ -19,7 +19,8 @@ func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -45,7 +46,8 @@ func TestSingleVRFIBGPSession(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -72,7 +74,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -90,7 +93,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.4.4.255:179",
+			PeerAddress:   "10.4.4.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -117,7 +121,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -135,7 +140,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.3.3.254"),
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
@@ -162,7 +168,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -178,7 +185,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 	defer session1.Close()
 	session2, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			MyASN:         100,
 			PeerASN:       200,
 			HoldTime:      ptr.To(time.Second),
@@ -203,7 +211,8 @@ func TestSingleAdvertisementVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -249,7 +258,8 @@ func TestSingleAdvertisementChangeVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -303,7 +313,8 @@ func TestTwoAdvertisementsVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -355,7 +366,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 
 	session, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.254:179",
+			PeerAddress:   "10.2.2.254",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
@@ -373,7 +385,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 
 	session1, err := sessionManager.NewSession(l,
 		bgp.SessionParameters{
-			PeerAddress:   "10.2.2.255:179",
+			PeerAddress:   "10.2.2.255",
+			PeerPort:      179,
 			SourceAddress: net.ParseIP("10.1.1.254"),
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
diff --git a/internal/bgp/frrk8s/testdata/TestSingleSessionWithInterface.golden b/internal/bgp/frrk8s/testdata/TestSingleSessionWithInterface.golden
new file mode 100644
index 00000000..e7320660
--- /dev/null
+++ b/internal/bgp/frrk8s/testdata/TestSingleSessionWithInterface.golden
@@ -0,0 +1,38 @@
+{
+    "metadata": {
+        "name": "metallb-testnodename",
+        "namespace": "testnamespace",
+        "creationTimestamp": null
+    },
+    "spec": {
+        "bgp": {
+            "routers": [
+                {
+                    "asn": 100,
+                    "id": "10.1.1.254",
+                    "neighbors": [
+                        {
+                            "dynamicASN": "internal",
+                            "interface": "net0",
+                            "port": 179,
+                            "passwordSecret": {},
+                            "toAdvertise": {
+                                "allowed": {}
+                            },
+                            "toReceive": {
+                                "allowed": {}
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        "raw": {},
+        "nodeSelector": {
+            "matchLabels": {
+                "kubernetes.io/hostname": "testnodename"
+            }
+        }
+    },
+    "status": {}
+}
\ No newline at end of file
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index d08ba6a2..39866140 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -3499,6 +3499,72 @@ func TestParse(t *testing.T) {
 				},
 			},
 		},
+		{
+			desc: "unnumbered peer without address with interface ok",
+			crs: ClusterResources{
+
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "peer1",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:      42,
+							DynamicASN: v1beta2.InternalASNMode,
+							Interface:  "net0",
+						},
+					},
+				},
+			},
+			want: &Config{
+				Peers: map[string]*Peer{
+					"peer1": {
+						Name:          "peer1",
+						Iface:         "net0",
+						MyASN:         42,
+						DynamicASN:    "internal",
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						EBGPMultiHop:  false,
+					},
+				},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+		{
+			desc: "unnumbered peer without address without interface nok",
+			crs: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "invalid",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:      42,
+							DynamicASN: "internal",
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "unnumbered peer with address with interface nok",
+			crs: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "invalid",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							Address:    "1.2.3.4",
+							Interface:  "net0",
+							MyASN:      42,
+							DynamicASN: "internal",
+						},
+					},
+				},
+			},
+		},
 	}
 
 	for _, test := range tests {
diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go
index cd4ae44c..5a2a0a1b 100644
--- a/internal/config/validation_test.go
+++ b/internal/config/validation_test.go
@@ -207,6 +207,19 @@ func TestValidate(t *testing.T) {
 			},
 			mustFail: true,
 		},
+		{
+			desc: "interface",
+			config: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						Spec: v1beta2.BGPPeerSpec{
+							Interface: "eth0",
+						},
+					},
+				},
+			},
+			mustFail: true,
+		},
 		{
 			desc: "keepalive time",
 			config: ClusterResources{
diff --git a/internal/k8s/controllers/reconciliation_test.go b/internal/k8s/controllers/reconciliation_test.go
index de717b04..1d846066 100644
--- a/internal/k8s/controllers/reconciliation_test.go
+++ b/internal/k8s/controllers/reconciliation_test.go
@@ -34,6 +34,7 @@ import (
 
 	v1beta1 "go.universe.tf/metallb/api/v1beta1"
 	v1beta2 "go.universe.tf/metallb/api/v1beta2"
+	"go.universe.tf/metallb/internal/allocator"
 	frrk8s "go.universe.tf/metallb/internal/bgp/frrk8s"
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/layer2"
@@ -74,6 +75,10 @@ var (
 	bgpStatusReconcileChan = make(chan event.GenericEvent)
 	bgpAdvs                = map[string]sets.Set[string]{}
 	bgpAdvsMutex           = sync.Mutex{}
+
+	poolStatusReconcileChan = make(chan event.GenericEvent)
+	poolCounters            = map[string]allocator.PoolCounters{}
+	poolCountersMutex       sync.Mutex
 )
 
 const (
@@ -236,6 +241,18 @@ var _ = BeforeSuite(func() {
 	err = bgpStatusReconciler.SetupWithManager(k8sManager)
 	Expect(err).ToNot(HaveOccurred())
 
+	poolStatusReconciler := &PoolStatusReconciler{
+		Client: k8sManager.GetClient(),
+		Logger: log.NewNopLogger(),
+		CountersFetcher: func(s string) allocator.PoolCounters {
+			poolCountersMutex.Lock()
+			defer poolCountersMutex.Unlock()
+			return poolCounters[s]
+		},
+		ReconcileChan: poolStatusReconcileChan,
+	}
+	err = poolStatusReconciler.SetupWithManager(k8sManager)
+
 	go func() {
 		defer func() { mgrDone.Store(true) }()
 		defer GinkgoRecover()
@@ -732,3 +749,87 @@ var _ = Describe("BGP Status Controller", func() {
 		})
 	})
 })
+
+var _ = Describe("PoolStatus Controller", func() {
+	Context("SetupWithManager", func() {
+		testPoolName := "test"
+		It("Should Reconcile correctly", func() {
+			pool := v1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      testPoolName,
+					Namespace: testNamespace,
+				},
+				Spec: v1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"1.2.3.4/30",
+						"1000::4/126",
+					},
+				},
+			}
+			validateStatus := func(expected v1beta1.IPAddressPoolStatus) {
+				Eventually(func() error {
+					newPool := v1beta1.IPAddressPool{}
+					err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: pool.Name, Namespace: testNamespace}, &newPool)
+					if err != nil {
+						return err
+					}
+					if !reflect.DeepEqual(newPool.Status, expected) {
+						return fmt.Errorf("pool status does not match, got [%v] expected [%v]", newPool.Status, expected)
+					}
+					return nil
+				}, 5*time.Second, 200*time.Millisecond).ShouldNot(HaveOccurred())
+			}
+
+			poolCountersMutex.Lock()
+			poolCounters[testPoolName] = allocator.PoolCounters{
+				AvailableIPv4: 4,
+				AvailableIPv6: 4,
+				AssignedIPv4:  0,
+				AssignedIPv6:  0,
+			}
+			poolCountersMutex.Unlock()
+			err := k8sClient.Create(ctx, &pool)
+			Expect(err).ToNot(HaveOccurred())
+
+			expectedStatus := v1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 4,
+				AvailableIPv6: 4,
+				AssignedIPv4:  0,
+				AssignedIPv6:  0,
+			}
+			validateStatus(expectedStatus)
+
+			// Generate a status event
+			poolCountersMutex.Lock()
+			poolCounters[testPoolName] = allocator.PoolCounters{
+				AvailableIPv4: 3,
+				AvailableIPv6: 3,
+				AssignedIPv4:  1,
+				AssignedIPv6:  1,
+			}
+			poolCountersMutex.Unlock()
+			poolStatusReconcileChan <- NewPoolStatusEvent(testNamespace, testPoolName)
+
+			expectedStatus = v1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 3,
+				AvailableIPv6: 3,
+				AssignedIPv4:  1,
+				AssignedIPv6:  1,
+			}
+			validateStatus(expectedStatus)
+
+			// Manual updates should be reverted by the controller
+			err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: pool.Name, Namespace: testNamespace}, &pool)
+			Expect(err).To(Not(HaveOccurred()))
+			pool.Status = v1beta1.IPAddressPoolStatus{
+				AvailableIPv4: 0,
+				AvailableIPv6: 0,
+				AssignedIPv4:  1,
+				AssignedIPv6:  1,
+			}
+			err = k8sClient.Status().Update(context.TODO(), &pool)
+			Expect(err).To(Not(HaveOccurred()))
+			validateStatus(expectedStatus)
+		})
+	})
+})
diff --git a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go
index abc4ad0c..7a7ed80f 100644
--- a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go
+++ b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/go-kit/log"
 	"github.com/google/go-cmp/cmp"
 	"go.universe.tf/metallb/api/v1beta1"
+	v1core "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -29,9 +30,14 @@ func TestValidateIPAddressPool(t *testing.T) {
 			},
 		}, nil
 	}
+	toRestoreNodes := getExistingNodes
+	getExistingNodes = func() (*v1core.NodeList, error) {
+		return &v1core.NodeList{}, nil
+	}
 
 	defer func() {
 		getExistingIPAddressPools = toRestoreIPAddressPools
+		getExistingNodes = toRestoreNodes
 	}()
 
 	tests := []struct {
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index 1490737d..0c131d98 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -285,7 +285,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				}},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 		{
@@ -313,7 +313,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -343,7 +343,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -377,7 +377,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -420,7 +420,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -493,7 +493,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -509,7 +509,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			},
 			eps: []discovery.EndpointSlice{},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -539,7 +539,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -578,7 +578,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -613,7 +613,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -648,7 +648,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -712,7 +712,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix:    ipnet("10.20.30.1/32"),
 						LocalPref: 100,
@@ -786,7 +786,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix:    ipnet("10.20.30.1/32"),
 						LocalPref: 100,
@@ -859,7 +859,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			},
 
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix:    ipnet("10.20.30.1/32"),
 						LocalPref: 100,
@@ -927,12 +927,12 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -965,12 +965,12 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1004,7 +1004,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1012,7 +1012,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						Prefix: ipnet("10.20.30.5/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1049,7 +1049,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					// Prefixes duplicated because the dedupe happens
 					// inside the real BGP session.
 					{
@@ -1059,7 +1059,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1074,12 +1074,12 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			desc:     "Delete svc",
 			balancer: "test1",
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": {
+				"1.2.3.4": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
 				},
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1132,7 +1132,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.5:0": {
+				"1.2.3.5": {
 					{
 						Prefix: ipnet("10.20.30.1/32"),
 					},
@@ -1144,7 +1144,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 			desc:     "Delete second svc",
 			balancer: "test2",
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.5:0": nil,
+				"1.2.3.5": nil,
 			},
 		},
 	}
@@ -1223,7 +1223,7 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -1245,7 +1245,7 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -1260,8 +1260,8 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1276,7 +1276,7 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
+				"1.2.3.4": nil,
 			},
 		},
 
@@ -1298,8 +1298,8 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1314,8 +1314,8 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1338,8 +1338,8 @@ func TestNodeSelectors(t *testing.T) {
 				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1354,8 +1354,8 @@ func TestNodeSelectors(t *testing.T) {
 				},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1371,8 +1371,8 @@ func TestNodeSelectors(t *testing.T) {
 				Spec: v1.NodeSpec{Unschedulable: true},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 			wantReturnState: controllers.SyncStateReprocessAll,
 		},
@@ -1389,8 +1389,8 @@ func TestNodeSelectors(t *testing.T) {
 				Spec: v1.NodeSpec{Unschedulable: true},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 		},
 
@@ -1406,8 +1406,8 @@ func TestNodeSelectors(t *testing.T) {
 				Spec: v1.NodeSpec{Unschedulable: false},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
-				"1.2.3.4:0": nil,
-				"2.3.4.5:0": nil,
+				"1.2.3.4": nil,
+				"2.3.4.5": nil,
 			},
 			wantReturnState: controllers.SyncStateReprocessAll,
 		},