Repository: metallb/metallb
Pull request #1863 (2023-03-17 15:57:06)

FRR Mode: provide a way to append a piece of configuration
Currently it is not possible to run multiple FRR instances on the same
node, due to port clashing in first instance.
In order to provide a way to overcome this issue, we provide a _non
supported - use at your own risk_ way to append a fixed configuration
right after the one generated by metallb. This can be used in short term
to mitigate some of the issues reported.

This solution will be deprecated in the long term, being replaced by a
more cohesive one.


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index ae08778d..a22cb0b1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -56,7 +56,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v3
 
-      - uses: actions/setup-go@v3
+      - uses: actions/setup-go@v4
         with:
           go-version-file: "go.mod"
           cache: true
@@ -96,7 +96,7 @@ jobs:
         uses: docker/setup-buildx-action@v2
 
       - name: Build and export ${{ matrix.image }}
-        uses: docker/build-push-action@v3
+        uses: docker/build-push-action@v4
         with:
           context: .
           tags: quay.io/metallb/${{ matrix.image }}:dev-amd64
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 86fe71f1..599caf3b 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -19,7 +19,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v3
 
-      - uses: actions/setup-go@v3
+      - uses: actions/setup-go@v4
         with:
           go-version-file: "go.mod"
           cache: true
@@ -94,7 +94,7 @@ jobs:
             org.opencontainers.image.description=${{ matrix.image }} for metallb, a network load-balancer implementation for Kubernetes using standard routing protocols
 
       - name: Build and push ${{ matrix.image }}
-        uses: docker/build-push-action@v3
+        uses: docker/build-push-action@v4
         id: build-and-push
         with:
           context: .
diff --git a/.golangci.yml b/.golangci.yml
index 8dfb1a06..b6cc9ad8 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -2,6 +2,7 @@ linters:
   enable:
   - godot
   - goimports
+  - gofmt
 linters-settings:
   errcheck:
     exclude: .golangci.exclude
diff --git a/charts/metallb/README.md b/charts/metallb/README.md
index 11a9b077..29f7eb12 100644
--- a/charts/metallb/README.md
+++ b/charts/metallb/README.md
@@ -110,6 +110,7 @@ Kubernetes: `>= 1.19.0-0`
 | rbac.create | bool | `true` |  |
 | speaker.affinity | object | `{}` |  |
 | speaker.enabled | bool | `true` |  |
+| speaker.excludeInterfaces.enabled | bool | `true` |  |
 | speaker.frr.enabled | bool | `false` |  |
 | speaker.frr.image.pullPolicy | string | `nil` |  |
 | speaker.frr.image.repository | string | `"quay.io/frrouting/frr"` |  |
diff --git a/charts/metallb/templates/exclude-l2-config.yaml b/charts/metallb/templates/exclude-l2-config.yaml
new file mode 100644
index 00000000..7cc2ff34
--- /dev/null
+++ b/charts/metallb/templates/exclude-l2-config.yaml
@@ -0,0 +1,22 @@
+{{- if .Values.speaker.excludeInterfaces.enabled }}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: metallb-excludel2
+data:
+  excludel2.yaml: |
+    announcedInterfacesToExclude:
+    - docker.*
+    - cbr.*
+    - dummy.*
+    - virbr.*
+    - lxcbr.*
+    - veth.*
+    - lo
+    - ^cali.*
+    - ^tunl.*
+    - flannel.*
+    - kube-ipvs.*
+    - cni.*
+    - ^nodelocaldns.*
+{{- end }}
\ No newline at end of file
diff --git a/charts/metallb/templates/rbac.yaml b/charts/metallb/templates/rbac.yaml
index 1d046257..d8e42b2f 100644
--- a/charts/metallb/templates/rbac.yaml
+++ b/charts/metallb/templates/rbac.yaml
@@ -76,6 +76,9 @@ rules:
 - apiGroups: [""]
   resources: ["secrets"]
   verbs: ["get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["configmaps"]
+  verbs: ["get", "list", "watch"]
 - apiGroups: ["metallb.io"]
   resources: ["addresspools"]
   verbs: ["get", "list", "watch"]
diff --git a/charts/metallb/templates/servicemonitor.yaml b/charts/metallb/templates/servicemonitor.yaml
index 5c2ab422..43825b46 100644
--- a/charts/metallb/templates/servicemonitor.yaml
+++ b/charts/metallb/templates/servicemonitor.yaml
@@ -58,14 +58,6 @@ spec:
   selector:
     matchLabels:
       name: {{ template "metallb.fullname" . }}-speaker-monitor-service
-{{- if .Values.prometheus.serviceMonitor.metricRelabelings }}
-  metricRelabelings:
-{{- toYaml .Values.prometheus.serviceMonitor.metricRelabelings | nindent 4 }}
-{{- end }}
-{{- if .Values.prometheus.serviceMonitor.relabelings }}
-  relabelings:
-{{- toYaml .Values.prometheus.serviceMonitor.relabelings | nindent 4 }}
-{{- end }}      
 ---
 apiVersion: v1
 kind: Service
@@ -112,6 +104,14 @@ metadata:
 spec:
   endpoints:
     - port: {{ template "metrics.exposedportname" . }}
+      {{- if .Values.prometheus.serviceMonitor.metricRelabelings }}
+      metricRelabelings:
+      {{- toYaml .Values.prometheus.serviceMonitor.metricRelabelings | nindent 8 }}
+      {{- end -}}
+      {{- if .Values.prometheus.serviceMonitor.relabelings }}
+      relabelings:
+      {{- toYaml .Values.prometheus.serviceMonitor.relabelings | nindent 8 }}
+      {{- end }}
       {{- if .Values.prometheus.serviceMonitor.interval }}
       interval: {{ .Values.prometheus.serviceMonitor.interval }}
       {{- end }}
@@ -131,14 +131,6 @@ spec:
   selector:
     matchLabels:
       name: {{ template "metallb.fullname" . }}-controller-monitor-service
-{{- if .Values.prometheus.serviceMonitor.metricRelabelings }}
-  metricRelabelings:
-{{- toYaml .Values.prometheus.serviceMonitor.metricRelabelings | nindent 4 }}
-{{- end }}
-{{- if .Values.prometheus.serviceMonitor.relabelings }}
-  relabelings:
-{{- toYaml .Values.prometheus.serviceMonitor.relabelings | nindent 4 }}
-{{- end }}      
 ---
 apiVersion: v1
 kind: Service
diff --git a/charts/metallb/templates/speaker.yaml b/charts/metallb/templates/speaker.yaml
index c94eb615..d982f2f5 100644
--- a/charts/metallb/templates/speaker.yaml
+++ b/charts/metallb/templates/speaker.yaml
@@ -159,6 +159,12 @@ spec:
             secretName: {{ include "metallb.secretName" . }}
             defaultMode: 420
       {{- end }}
+      {{- if .Values.speaker.excludeInterfaces.enabled }}
+        - name: metallb-excludel2
+          configMap:
+            defaultMode: 256
+            name: metallb-excludel2
+      {{- end }}
       {{- if .Values.speaker.frr.enabled }}
         - name: frr-sockets
           emptyDir: {}
@@ -297,7 +303,7 @@ spec:
             - ALL
             add:
             - NET_RAW
-        {{- if or .Values.speaker.frr.enabled .Values.speaker.memberlist.enabled }}
+        {{- if or .Values.speaker.frr.enabled .Values.speaker.memberlist.enabled .Values.speaker.excludeInterfaces.enabled }}
         volumeMounts:
           {{- if .Values.speaker.memberlist.enabled }}
           - name: memberlist 
@@ -307,6 +313,10 @@ spec:
           - name: reloader
             mountPath: /etc/frr_reloader
           {{- end }}
+          {{- if .Values.speaker.excludeInterfaces.enabled }}
+          - name: metallb-excludel2
+            mountPath: /etc/metallb
+          {{- end }}
         {{- end }}
       {{- if .Values.speaker.frr.enabled }}
       - name: frr
diff --git a/charts/metallb/values.schema.json b/charts/metallb/values.schema.json
index 9d3516d0..5a92e56a 100644
--- a/charts/metallb/values.schema.json
+++ b/charts/metallb/values.schema.json
@@ -322,6 +322,14 @@
                 }
               }
             },
+            "excludeInterfaces": {
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean"
+                }
+              }
+            },
             "updateStrategy": {
               "type": "object",
               "properties": {
diff --git a/charts/metallb/values.yaml b/charts/metallb/values.yaml
index 1844f8fb..689424ca 100644
--- a/charts/metallb/values.yaml
+++ b/charts/metallb/values.yaml
@@ -261,6 +261,8 @@ speaker:
     enabled: true
     mlBindPort: 7946
     mlSecretKeyPath: "/etc/ml_secret_key"
+  excludeInterfaces:
+    enabled: true
   image:
     repository: quay.io/metallb/speaker
     tag:
diff --git a/config/controllers/speaker.yaml b/config/controllers/speaker.yaml
index bdf73c96..e45a2d32 100644
--- a/config/controllers/speaker.yaml
+++ b/config/controllers/speaker.yaml
@@ -85,6 +85,9 @@ spec:
         - mountPath: /etc/ml_secret_key
           name: memberlist
           readOnly: true
+        - mountPath: /etc/metallb
+          name: metallb-excludel2
+          readOnly: true
       hostNetwork: true
       nodeSelector:
         kubernetes.io/os: linux
@@ -102,3 +105,7 @@ spec:
         secret: 
           secretName: memberlist
           defaultMode: 420
+      - name: metallb-excludel2
+        configMap:
+          defaultMode: 256
+          name: metallb-excludel2
diff --git a/config/manifests/metallb-frr-prometheus.yaml b/config/manifests/metallb-frr-prometheus.yaml
index 3443f9e5..89999855 100644
--- a/config/manifests/metallb-frr-prometheus.yaml
+++ b/config/manifests/metallb-frr-prometheus.yaml
@@ -1410,6 +1410,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - metallb.io
   resources:
@@ -1813,6 +1821,15 @@ metadata:
   namespace: metallb-system
 ---
 apiVersion: v1
+data:
+  excludel2.yaml: |
+    announcedInterfacesToExclude: ["docker.*", "cbr.*", "dummy.*", "virbr.*", "lxcbr.*", "veth.*", "lo", "^cali.*", "^tunl.*", "flannel.*", "kube-ipvs.*", "cni.*", "^nodelocaldns.*"]
+kind: ConfigMap
+metadata:
+  name: metallb-excludel2
+  namespace: metallb-system
+---
+apiVersion: v1
 kind: Secret
 metadata:
   name: webhook-server-cert
@@ -2194,6 +2211,9 @@ spec:
         - mountPath: /etc/ml_secret_key
           name: memberlist
           readOnly: true
+        - mountPath: /etc/metallb
+          name: metallb-excludel2
+          readOnly: true
       hostNetwork: true
       initContainers:
       - command:
@@ -2256,6 +2276,10 @@ spec:
         secret:
           defaultMode: 420
           secretName: memberlist
+      - configMap:
+          defaultMode: 256
+          name: metallb-excludel2
+        name: metallb-excludel2
 ---
 apiVersion: monitoring.coreos.com/v1
 kind: ServiceMonitor
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index 404daaca..504d7384 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -1410,6 +1410,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - metallb.io
   resources:
@@ -1758,6 +1766,15 @@ metadata:
   namespace: metallb-system
 ---
 apiVersion: v1
+data:
+  excludel2.yaml: |
+    announcedInterfacesToExclude: ["docker.*", "cbr.*", "dummy.*", "virbr.*", "lxcbr.*", "veth.*", "lo", "^cali.*", "^tunl.*", "flannel.*", "kube-ipvs.*", "cni.*", "^nodelocaldns.*"]
+kind: ConfigMap
+metadata:
+  name: metallb-excludel2
+  namespace: metallb-system
+---
+apiVersion: v1
 kind: Secret
 metadata:
   name: webhook-server-cert
@@ -2017,6 +2034,9 @@ spec:
         - mountPath: /etc/ml_secret_key
           name: memberlist
           readOnly: true
+        - mountPath: /etc/metallb
+          name: metallb-excludel2
+          readOnly: true
       hostNetwork: true
       initContainers:
       - command:
@@ -2079,6 +2099,10 @@ spec:
         secret:
           defaultMode: 420
           secretName: memberlist
+      - configMap:
+          defaultMode: 256
+          name: metallb-excludel2
+        name: metallb-excludel2
 ---
 apiVersion: admissionregistration.k8s.io/v1
 kind: ValidatingWebhookConfiguration
diff --git a/config/manifests/metallb-native-prometheus.yaml b/config/manifests/metallb-native-prometheus.yaml
index 3232cd84..a5e9d335 100644
--- a/config/manifests/metallb-native-prometheus.yaml
+++ b/config/manifests/metallb-native-prometheus.yaml
@@ -1410,6 +1410,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - metallb.io
   resources:
@@ -1715,6 +1723,15 @@ subjects:
   namespace: metallb-system
 ---
 apiVersion: v1
+data:
+  excludel2.yaml: |
+    announcedInterfacesToExclude: ["docker.*", "cbr.*", "dummy.*", "virbr.*", "lxcbr.*", "veth.*", "lo", "^cali.*", "^tunl.*", "flannel.*", "kube-ipvs.*", "cni.*", "^nodelocaldns.*"]
+kind: ConfigMap
+metadata:
+  name: metallb-excludel2
+  namespace: metallb-system
+---
+apiVersion: v1
 kind: Secret
 metadata:
   name: webhook-server-cert
@@ -1988,6 +2005,9 @@ spec:
         - mountPath: /etc/ml_secret_key
           name: memberlist
           readOnly: true
+        - mountPath: /etc/metallb
+          name: metallb-excludel2
+          readOnly: true
       hostNetwork: true
       nodeSelector:
         kubernetes.io/os: linux
@@ -2005,6 +2025,10 @@ spec:
         secret:
           defaultMode: 420
           secretName: memberlist
+      - configMap:
+          defaultMode: 256
+          name: metallb-excludel2
+        name: metallb-excludel2
 ---
 apiVersion: monitoring.coreos.com/v1
 kind: ServiceMonitor
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index ffdf2416..c59bddb7 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -1410,6 +1410,14 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - metallb.io
   resources:
@@ -1660,6 +1668,15 @@ subjects:
   namespace: metallb-system
 ---
 apiVersion: v1
+data:
+  excludel2.yaml: |
+    announcedInterfacesToExclude: ["docker.*", "cbr.*", "dummy.*", "virbr.*", "lxcbr.*", "veth.*", "lo", "^cali.*", "^tunl.*", "flannel.*", "kube-ipvs.*", "cni.*", "^nodelocaldns.*"]
+kind: ConfigMap
+metadata:
+  name: metallb-excludel2
+  namespace: metallb-system
+---
+apiVersion: v1
 kind: Secret
 metadata:
   name: webhook-server-cert
@@ -1842,6 +1859,9 @@ spec:
         - mountPath: /etc/ml_secret_key
           name: memberlist
           readOnly: true
+        - mountPath: /etc/metallb
+          name: metallb-excludel2
+          readOnly: true
       hostNetwork: true
       nodeSelector:
         kubernetes.io/os: linux
@@ -1859,6 +1879,10 @@ spec:
         secret:
           defaultMode: 420
           secretName: memberlist
+      - configMap:
+          defaultMode: 256
+          name: metallb-excludel2
+        name: metallb-excludel2
 ---
 apiVersion: admissionregistration.k8s.io/v1
 kind: ValidatingWebhookConfiguration
diff --git a/config/native/exclude-l2-config.yaml b/config/native/exclude-l2-config.yaml
new file mode 100644
index 00000000..25a80a00
--- /dev/null
+++ b/config/native/exclude-l2-config.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: metallb-excludel2
+  namespace: metallb-system
+data:
+  excludel2.yaml: |
+    announcedInterfacesToExclude: ["docker.*", "cbr.*", "dummy.*", "virbr.*", "lxcbr.*", "veth.*", "lo", "^cali.*", "^tunl.*", "flannel.*", "kube-ipvs.*", "cni.*", "^nodelocaldns.*"]
diff --git a/config/native/kustomization.yaml b/config/native/kustomization.yaml
index 2c741aaa..6085bacc 100644
--- a/config/native/kustomization.yaml
+++ b/config/native/kustomization.yaml
@@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
 - ns.yaml
+- exclude-l2-config.yaml
 - ../crd
 - ../rbac
 - ../controllers
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 7bb5f510..dce1bf0f 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -148,6 +148,14 @@ rules:
       - get
       - list
       - watch
+  - apiGroups:
+      - ""
+    resources:
+      - configmaps
+    verbs:
+      - get
+      - list
+      - watch
   - apiGroups:
       - metallb.io
     resources:
diff --git a/controller/service.go b/controller/service.go
index f78f6ebc..e6ccdae7 100644
--- a/controller/service.go
+++ b/controller/service.go
@@ -96,6 +96,7 @@ func (c *controller) convergeBalancer(l log.Logger, key string, svc *v1.Service)
 		// otherwise it'll fail and tell us why.
 		if err = c.ips.Assign(key, svc, lbIPs, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc)); err != nil {
 			level.Info(l).Log("event", "clearAssignment", "error", err, "msg", "current IP not allowed by config, clearing")
+			c.client.Infof(svc, "ClearAssignment", "current IP for %q not allowed by config, will attempt for new IP assignment: %s", key, err)
 			c.clearServiceState(key, svc)
 			lbIPs = []net.IP{}
 		}
diff --git a/go.mod b/go.mod
index 9e65bd42..81015b3d 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721
 	github.com/onsi/ginkgo/v2 v2.6.0
 	github.com/onsi/gomega v1.24.1
-	github.com/open-policy-agent/cert-controller v0.6.0
+	github.com/open-policy-agent/cert-controller v0.7.0
 	github.com/openshift-kni/k8sreporter v1.0.2
 	github.com/ory/dockertest/v3 v3.9.1
 	github.com/osrg/gobgp v2.0.0+incompatible
@@ -27,11 +27,11 @@ require (
 	k8s.io/api v0.26.1
 	k8s.io/apiextensions-apiserver v0.26.1
 	k8s.io/apimachinery v0.26.1
-	k8s.io/client-go v1.5.2
+	k8s.io/client-go v0.26.1
 	k8s.io/klog v1.0.0
 	k8s.io/kubernetes v1.26.1
 	k8s.io/pod-security-admission v0.0.0
-	sigs.k8s.io/controller-runtime v0.14.1
+	sigs.k8s.io/controller-runtime v0.14.5
 	sigs.k8s.io/yaml v1.3.0
 )
 
@@ -135,10 +135,10 @@ require (
 	go.uber.org/multierr v1.7.0 // indirect
 	go.uber.org/zap v1.24.0 // indirect
 	golang.org/x/crypto v0.5.0 // indirect
-	golang.org/x/net v0.5.0 // indirect
+	golang.org/x/net v0.7.0 // indirect
 	golang.org/x/oauth2 v0.4.0 // indirect
-	golang.org/x/term v0.4.0 // indirect
-	golang.org/x/text v0.6.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index 460a1197..e14d1a1d 100644
--- a/go.sum
+++ b/go.sum
@@ -400,8 +400,9 @@ github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ
 github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
 github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
 github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
-github.com/open-policy-agent/cert-controller v0.6.0 h1:HBhe1kS0GTk5dRHdklwgJKoGIctWisueIYnIYJu65Q0=
-github.com/open-policy-agent/cert-controller v0.6.0/go.mod h1:uOQW+2tMU51vSxy1Yt162oVUTMdqLuotC0aObQxrh6k=
+github.com/open-policy-agent/cert-controller v0.7.0 h1:5ggZjSQJ1YgkT+ngNAGBGHaOYfGuYq97IrNldchCoHI=
+github.com/open-policy-agent/cert-controller v0.7.0/go.mod h1:Dkkdcr1BeSUig/62dYLqgvx3lIN0XtEikKTGW1lKSQo=
+github.com/open-policy-agent/frameworks/constraint v0.0.0-20230201235642-777dc99a6669 h1:vKt4PhZXBxYHeLujYraNVpkoILQ/NISiifzaq1DkMXk=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
@@ -670,8 +671,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 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=
@@ -759,8 +760,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
-golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
-golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -771,8 +772,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 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=
@@ -1019,8 +1020,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.33 h1:LYqFq+6Cj2D0gFfrJvL7iElD4ET6ir3VDdhDdTK7rgc=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.33/go.mod h1:soWkSNf2tZC7aMibXEqVhCd73GOY5fJikn8qbdzemB0=
-sigs.k8s.io/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GIpxpzM=
-sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU=
+sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s=
+sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0=
 sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
diff --git a/internal/bgp/bgp.go b/internal/bgp/bgp.go
index 24e58647..c5950755 100644
--- a/internal/bgp/bgp.go
+++ b/internal/bgp/bgp.go
@@ -78,4 +78,5 @@ type SessionParameters struct {
 type SessionManager interface {
 	NewSession(logger log.Logger, args SessionParameters) (Session, error)
 	SyncBFDProfiles(profiles map[string]*config.BFDProfile) error
+	SyncExtraInfo(extras string) error
 }
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index 7f7b4c13..818dcdda 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -31,6 +31,7 @@ type frrConfig struct {
 	Hostname    string
 	Routers     []*routerConfig
 	BFDProfiles []BFDProfile
+	ExtraConfig string
 }
 
 type reloadEvent struct {
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index 2f6217e4..0a275eef 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -26,6 +26,7 @@ import (
 type sessionManager struct {
 	sessions     map[string]*session
 	bfdProfiles  []BFDProfile
+	extraConfig  string
 	reloadConfig chan reloadEvent
 	logLevel     string
 	sync.Mutex
@@ -153,6 +154,19 @@ func (sm *sessionManager) deleteSession(s *session) error {
 	return nil
 }
 
+func (sm *sessionManager) SyncExtraInfo(extraInfo string) error {
+	sm.Lock()
+	defer sm.Unlock()
+	sm.extraConfig = extraInfo
+	frrConfig, err := sm.createConfig()
+	if err != nil {
+		return err
+	}
+
+	sm.reloadConfig <- reloadEvent{config: frrConfig}
+	return nil
+}
+
 func (sm *sessionManager) SyncBFDProfiles(profiles map[string]*metallbconfig.BFDProfile) error {
 	sm.Lock()
 	defer sm.Unlock()
@@ -184,6 +198,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 		Hostname:    hostname,
 		Loglevel:    sm.logLevel,
 		BFDProfiles: sm.bfdProfiles,
+		ExtraConfig: sm.extraConfig,
 	}
 
 	type router struct {
diff --git a/internal/bgp/frr/templates/frr.tmpl b/internal/bgp/frr/templates/frr.tmpl
index d1274137..f732de91 100644
--- a/internal/bgp/frr/templates/frr.tmpl
+++ b/internal/bgp/frr/templates/frr.tmpl
@@ -64,3 +64,7 @@ bfd
 {{- template "bfdprofile" dict "profile" . -}}
 {{- end }}
 {{- end }}
+
+{{- if .ExtraConfig }}
+{{ .ExtraConfig }}
+{{- end }}
diff --git a/internal/bgp/native/native.go b/internal/bgp/native/native.go
index d6084775..4825e520 100644
--- a/internal/bgp/native/native.go
+++ b/internal/bgp/native/native.go
@@ -78,6 +78,13 @@ func (sm *sessionManager) SyncBFDProfiles(profiles map[string]*config.BFDProfile
 	return errors.New("bfd profiles not supported in native mode")
 }
 
+func (sm *sessionManager) SyncExtraInfo(extras string) error {
+	if extras != "" {
+		return errors.New("bgp extra info not supported in native mode")
+	}
+	return nil
+}
+
 // run tries to stay connected to the peer, and pumps route updates to it.
 func (s *session) run() {
 	defer stats.DeleteSession(s.PeerAddress)
diff --git a/internal/config/config.go b/internal/config/config.go
index dfd2600f..44815a0f 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -45,6 +45,7 @@ type ClusterResources struct {
 	PasswordSecrets    map[string]corev1.Secret          `json:"passwordsecrets"`
 	Nodes              []corev1.Node                     `json:"nodes"`
 	Namespaces         []corev1.Namespace                `json:"namespaces"`
+	BGPExtras          corev1.ConfigMap                  `json:"bgpextras"`
 }
 
 // Config is a parsed MetalLB configuration.
@@ -55,6 +56,8 @@ type Config struct {
 	Pools *Pools
 	// BFD profiles that can be used by peers.
 	BFDProfiles map[string]*BFDProfile
+	// Protocol dependent extra config. Currently used only by FRR
+	BGPExtras string
 }
 
 // Pools contains address pools and its namespace/service specific allocations.
@@ -76,6 +79,8 @@ const (
 	Layer2 Proto = "layer2"
 )
 
+const bgpExtrasField = "extras"
+
 var Protocols = []Proto{
 	BGP, Layer2,
 }
@@ -226,6 +231,11 @@ func For(resources ClusterResources, validate Validate) (*Config, error) {
 		return nil, err
 	}
 
+	cfg.BGPExtras = bgpExtrasFor(resources)
+	if err != nil {
+		return nil, err
+	}
+
 	err = validateConfig(cfg)
 	if err != nil {
 		return nil, err
@@ -351,6 +361,13 @@ func poolsFor(resources ClusterResources) (*Pools, error) {
 		ByServiceSelector: poolsByServiceSelector(pools)}, nil
 }
 
+func bgpExtrasFor(resources ClusterResources) string {
+	if resources.BGPExtras.Data == nil {
+		return ""
+	}
+	return resources.BGPExtras.Data[bgpExtrasField]
+}
+
 func communitiesFromCrs(cs []metallbv1beta1.Community) (map[string]uint32, error) {
 	communities := map[string]uint32{}
 	for _, c := range cs {
@@ -524,8 +541,22 @@ func addressPoolServiceAllocationsFromCR(p metallbv1beta1.IPAddressPool, namespa
 	if p.Spec.AllocateTo == nil {
 		return nil, nil
 	}
-	serviceAllocations := &ServiceAllocation{Priority: p.Spec.AllocateTo.Priority,
-		Namespaces: sets.New(p.Spec.AllocateTo.Namespaces...)}
+	poolNamespaces := sets.Set[string]{}
+	for _, poolNs := range p.Spec.AllocateTo.Namespaces {
+		if poolNamespaces.Has(poolNs) {
+			return nil, errors.New("duplicate definition in namespaces field")
+		}
+		poolNamespaces.Insert(poolNs)
+	}
+	err := validateLabelSelectorDuplicate(p.Spec.AllocateTo.NamespaceSelectors, "namespaceSelectors")
+	if err != nil {
+		return nil, err
+	}
+	err = validateLabelSelectorDuplicate(p.Spec.AllocateTo.ServiceSelectors, "serviceSelectors")
+	if err != nil {
+		return nil, err
+	}
+	serviceAllocations := &ServiceAllocation{Priority: p.Spec.AllocateTo.Priority, Namespaces: poolNamespaces}
 	for i := range p.Spec.AllocateTo.NamespaceSelectors {
 		l, err := metav1.LabelSelectorAsSelector(&p.Spec.AllocateTo.NamespaceSelectors[i])
 		if err != nil {
diff --git a/internal/k8s/controllers/config_controller.go b/internal/k8s/controllers/config_controller.go
index 8a9702fe..56866f68 100644
--- a/internal/k8s/controllers/config_controller.go
+++ b/internal/k8s/controllers/config_controller.go
@@ -26,6 +26,7 @@ import (
 	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
 	"go.universe.tf/metallb/internal/config"
 	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -36,6 +37,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/source"
 )
 
+const bgpExtrasConfigName = "bgpextras"
+
 type ConfigReconciler struct {
 	client.Client
 	Logger         log.Logger
@@ -116,6 +119,13 @@ var requestHandler = func(r *ConfigReconciler, ctx context.Context, req ctrl.Req
 		return ctrl.Result{}, err
 	}
 
+	var extrasMap corev1.ConfigMap
+	key := client.ObjectKey{Name: bgpExtrasConfigName, Namespace: r.Namespace}
+	if err := r.Get(ctx, key, &extrasMap); err != nil && !apierrors.IsNotFound(err) {
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get the frr configmap", "error", err)
+		return ctrl.Result{}, err
+	}
+
 	resources := config.ClusterResources{
 		Pools:              ipAddressPools.Items,
 		Peers:              bgpPeers.Items,
@@ -127,6 +137,7 @@ var requestHandler = func(r *ConfigReconciler, ctx context.Context, req ctrl.Req
 		PasswordSecrets:    secrets,
 		Nodes:              nodes.Items,
 		Namespaces:         namespaces.Items,
+		BGPExtras:          extrasMap,
 	}
 
 	level.Debug(r.Logger).Log("controller", "ConfigReconciler", "metallb CRs and Secrets", dumpClusterResources(&resources))
@@ -138,6 +149,9 @@ var requestHandler = func(r *ConfigReconciler, ctx context.Context, req ctrl.Req
 		return ctrl.Result{}, nil
 	}
 
+	if cfg.BGPExtras != "" {
+		level.Info(r.Logger).Log("controller", "ConfigReconciler", "warning message", "BGP Extras provided, please note that this configuration is not supported and used at your own risk")
+	}
 	level.Debug(r.Logger).Log("controller", "ConfigReconciler", "rendered config", dumpConfig(cfg))
 	if r.currentConfig != nil && reflect.DeepEqual(r.currentConfig, cfg) {
 		level.Debug(r.Logger).Log("controller", "ConfigReconciler", "event", "configuration did not change, ignoring")
@@ -176,7 +190,7 @@ var requestHandler = func(r *ConfigReconciler, ctx context.Context, req ctrl.Req
 func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	p := predicate.Funcs{
 		UpdateFunc: func(e event.UpdateEvent) bool {
-			return filterNodeEvent(e) && filterNamespaceEvent(e)
+			return filterNodeEvent(e) && filterNamespaceEvent(e) && filterConfigmapEvent(e)
 		},
 	}
 	return ctrl.NewControllerManagedBy(mgr).
@@ -190,6 +204,7 @@ func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		Watches(&source.Kind{Type: &metallbv1beta1.Community{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestForObject{}).
+		Watches(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestForObject{}).
 		WithEventFilter(p).
 		Complete(r)
 }
@@ -225,6 +240,17 @@ func filterNamespaceEvent(e event.UpdateEvent) bool {
 	return true
 }
 
+func filterConfigmapEvent(e event.UpdateEvent) bool {
+	cm, ok := e.ObjectNew.(*corev1.ConfigMap)
+	if !ok {
+		return true
+	}
+	if cm.Name != bgpExtrasConfigName {
+		return false
+	}
+	return true
+}
+
 func (r *ConfigReconciler) getSecrets(ctx context.Context) (map[string]corev1.Secret, error) {
 	var secrets corev1.SecretList
 	if err := r.List(ctx, &secrets, client.InNamespace(r.Namespace)); err != nil {
diff --git a/internal/k8s/controllers/dump.go b/internal/k8s/controllers/dump.go
index c9b55d6a..9311381c 100644
--- a/internal/k8s/controllers/dump.go
+++ b/internal/k8s/controllers/dump.go
@@ -20,6 +20,7 @@ func dumpClusterResources(c *config.ClusterResources) string {
 		BGPAdvs:            c.BGPAdvs,
 		LegacyAddressPools: c.LegacyAddressPools,
 		Communities:        c.Communities,
+		BGPExtras:          c.BGPExtras,
 	}
 	withNoSecret.PasswordSecrets = make(map[string]corev1.Secret)
 	for k, s := range c.PasswordSecrets {
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index 745daffd..2eec5fa6 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -142,6 +142,7 @@ func New(cfg *Config) (*Client, error) {
 				&metallbv1beta2.BGPPeer{}:          namespaceSelector,
 				&metallbv1beta1.Community{}:        namespaceSelector,
 				&corev1.Secret{}:                   namespaceSelector,
+				&corev1.ConfigMap{}:                namespaceSelector,
 			},
 		}),
 	})
diff --git a/internal/layer2/announcer.go b/internal/layer2/announcer.go
index 65a0fbb3..4ae3a46d 100644
--- a/internal/layer2/announcer.go
+++ b/internal/layer2/announcer.go
@@ -5,6 +5,7 @@ package layer2
 import (
 	"net"
 	"os"
+	"regexp"
 	"strconv"
 	"sync"
 	"time"
@@ -26,11 +27,12 @@ type Announce struct {
 
 	// This channel can block - do not write to it while holding the mutex
 	// to avoid deadlocking.
-	spamCh chan IPAdvertisement
+	spamCh        chan IPAdvertisement
+	excludeRegexp *regexp.Regexp
 }
 
 // New returns an initialized Announce.
-func New(l log.Logger) (*Announce, error) {
+func New(l log.Logger, excludeRegexp *regexp.Regexp) (*Announce, error) {
 	ret := &Announce{
 		logger:         l,
 		nodeInterfaces: []string{},
@@ -39,7 +41,9 @@ func New(l log.Logger) (*Announce, error) {
 		ips:            map[string][]IPAdvertisement{},
 		ipRefcnt:       map[string]int{},
 		spamCh:         make(chan IPAdvertisement, 1024),
+		excludeRegexp:  excludeRegexp,
 	}
+
 	go ret.interfaceScan()
 	go ret.spamLoop()
 
@@ -67,6 +71,12 @@ func (a *Announce) updateInterfaces() {
 	curIfs := make([]string, 0, len(ifs))
 	for _, intf := range ifs {
 		ifi := intf
+
+		if (a.excludeRegexp != nil) && a.excludeRegexp.MatchString(ifi.Name) {
+			level.Info(a.logger).Log("event", "announced interface to exclude", "interface", ifi.Name)
+			continue
+		}
+
 		curIfs = append(curIfs, ifi.Name)
 		l := log.With(a.logger, "interface", ifi.Name)
 		addrs, err := ifi.Addrs()
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 638f4927..01abe343 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -98,6 +98,11 @@ newPeers:
 	if err != nil {
 		return errors.Wrap(err, "failed to sync bfd profiles")
 	}
+	err = c.sessionManager.SyncExtraInfo(cfg.BGPExtras)
+	if err != nil {
+		return errors.Wrap(err, "failed to sync extra info")
+	}
+
 	return c.syncPeers(l)
 }
 
diff --git a/speaker/main.go b/speaker/main.go
index e372fc71..8b944df3 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -21,6 +21,8 @@ import (
 	"os"
 	"os/signal"
 	"path/filepath"
+	"regexp"
+	"strings"
 	"syscall"
 
 	"github.com/go-kit/log"
@@ -37,6 +39,7 @@ import (
 	"go.universe.tf/metallb/internal/speakerlist"
 	"go.universe.tf/metallb/internal/version"
 	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/yaml"
 )
 
 var announcing = prometheus.NewGaugeVec(prometheus.GaugeOpts{
@@ -51,6 +54,10 @@ var announcing = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 	"ip",
 })
 
+const (
+	excludeL2ConfigPath = "/etc/metallb/excludel2.yaml"
+)
+
 // Service offers methods to mutate a Kubernetes service object.
 type service interface {
 	UpdateStatus(svc *v1.Service) error
@@ -133,13 +140,21 @@ func main() {
 		os.Exit(1)
 	}
 
+	var interfacesToExclude *regexp.Regexp
+	interfacesToExclude, err = parseAnnouncedInterfacesToExclude()
+	if err != nil {
+		level.Error(logger).Log("op", "startup", "msg", "failed to parse announcedInterfacesToExclude from configMap", "error", err)
+		os.Exit(1)
+	}
+
 	// Setup all clients and speakers, config decides what is being done runtime.
 	ctrl, err := newController(controllerConfig{
-		MyNode:   *myNode,
-		Logger:   logger,
-		LogLevel: logging.Level(*logLevel),
-		SList:    sList,
-		bgpType:  bgpImplementation(bgpType),
+		MyNode:                 *myNode,
+		Logger:                 logger,
+		LogLevel:               logging.Level(*logLevel),
+		SList:                  sList,
+		bgpType:                bgpImplementation(bgpType),
+		InterfaceExcludeRegexp: interfacesToExclude,
 	})
 	if err != nil {
 		level.Error(logger).Log("op", "startup", "error", err, "msg", "failed to create MetalLB controller")
@@ -212,8 +227,10 @@ type controllerConfig struct {
 
 	// For testing only, and will be removed in a future release.
 	// See: https://github.com/metallb/metallb/issues/152.
-	DisableLayer2      bool
-	SupportedProtocols []config.Proto
+	DisableLayer2                bool
+	SupportedProtocols           []config.Proto
+	AnnouncedInterfacesToExclude []string `yaml:"announcedInterfacesToExclude"`
+	InterfaceExcludeRegexp       *regexp.Regexp
 }
 
 func newController(cfg controllerConfig) (*controller, error) {
@@ -229,7 +246,7 @@ func newController(cfg controllerConfig) (*controller, error) {
 	protocols := []config.Proto{config.BGP}
 
 	if !cfg.DisableLayer2 {
-		a, err := layer2.New(cfg.Logger)
+		a, err := layer2.New(cfg.Logger, cfg.InterfaceExcludeRegexp)
 		if err != nil {
 			return nil, fmt.Errorf("making layer2 announcer: %s", err)
 		}
@@ -443,6 +460,27 @@ func compareIPs(ips1, ips2 []net.IP) bool {
 	return true
 }
 
+func parseAnnouncedInterfacesToExclude() (*regexp.Regexp, error) {
+	configmapBytes, err := os.ReadFile(filepath.Clean(excludeL2ConfigPath))
+	if err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
+
+	var conf controllerConfig
+	if err = yaml.Unmarshal(configmapBytes, &conf); err != nil {
+		return nil, err
+	}
+
+	var excludeRegexp *regexp.Regexp
+	if len(conf.AnnouncedInterfacesToExclude) > 0 {
+		excludeRegexp, err = regexp.Compile("(" + strings.Join(conf.AnnouncedInterfacesToExclude, ")|(") + ")")
+		if err != nil {
+			return nil, err
+		}
+	}
+	return excludeRegexp, nil
+}
+
 func (c *controller) SetConfig(l log.Logger, cfg *config.Config) controllers.SyncState {
 
 	level.Debug(l).Log("event", "startUpdate", "msg", "start of config update")


Test patch:
diff --git a/e2etest/bgptests/bgp.go b/e2etest/bgptests/bgp.go
index ac0241d8..2fedab35 100644
--- a/e2etest/bgptests/bgp.go
+++ b/e2etest/bgptests/bgp.go
@@ -977,70 +977,98 @@ var _ = ginkgo.Describe("BGP", func() {
 
 	})
 
-	ginkgo.DescribeTable("MetalLB FRR rejects any routes advertised by any neighbor", func(addressesRange, toInject string, pairingIPFamily ipfamily.Family) {
-		resources := metallbconfig.ClusterResources{
-			Pools: []metallbv1beta1.IPAddressPool{
-				{
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "rejectroutes",
-					},
-					Spec: metallbv1beta1.IPAddressPoolSpec{
-						Addresses: []string{
-							addressesRange,
+	ginkgo.Context("MetalLB FRR rejects", func() {
+		ginkgo.AfterEach(func() {
+			err := k8s.RemoveConfigmap(cs, "bgpextras", metallb.Namespace)
+			framework.ExpectNoError(err)
+		})
+		ginkgo.DescribeTable("any routes advertised by any neighbor", func(addressesRange, toInject string, pairingIPFamily ipfamily.Family) {
+			resources := metallbconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "rejectroutes",
+						},
+						Spec: metallbv1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								addressesRange,
+							},
 						},
 					},
 				},
-			},
-			Peers:   metallb.PeersForContainers(FRRContainers, pairingIPFamily),
-			BGPAdvs: []metallbv1beta1.BGPAdvertisement{emptyBGPAdvertisement},
-		}
+				Peers:   metallb.PeersForContainers(FRRContainers, pairingIPFamily),
+				BGPAdvs: []metallbv1beta1.BGPAdvertisement{emptyBGPAdvertisement},
+			}
 
-		neighborAnnounce := func(frr *frrcontainer.FRR) {
-			frr.NeighborConfig.ToAdvertise = toInject
-		}
+			neighborAnnounce := func(frr *frrcontainer.FRR) {
+				frr.NeighborConfig.ToAdvertise = toInject
+			}
 
-		for _, c := range FRRContainers {
-			err := frrcontainer.PairWithNodes(cs, c, pairingIPFamily, neighborAnnounce)
-			framework.ExpectNoError(err)
-		}
+			for _, c := range FRRContainers {
+				err := frrcontainer.PairWithNodes(cs, c, pairingIPFamily, neighborAnnounce)
+				framework.ExpectNoError(err)
+			}
 
-		err := ConfigUpdater.Update(resources)
-		framework.ExpectNoError(err)
+			err := ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
 
-		for _, c := range FRRContainers {
-			validateFRRPeeredWithAllNodes(cs, c, pairingIPFamily)
-		}
-		speakerPods, err := metallb.SpeakerPods(cs)
-		framework.ExpectNoError(err)
+			for _, c := range FRRContainers {
+				validateFRRPeeredWithAllNodes(cs, c, pairingIPFamily)
+			}
+			speakerPods, err := metallb.SpeakerPods(cs)
+			framework.ExpectNoError(err)
 
-		checkRoutesInjected := func() error {
-			for _, pod := range speakerPods {
-				podExec := executor.ForPod(pod.Namespace, pod.Name, "frr")
-				routes, frrRoutesV6, err := frr.Routes(podExec)
-				framework.ExpectNoError(err)
+			checkRoutesInjected := func() error {
+				for _, pod := range speakerPods {
+					podExec := executor.ForPod(pod.Namespace, pod.Name, "frr")
+					routes, frrRoutesV6, err := frr.Routes(podExec)
+					framework.ExpectNoError(err)
 
-				if pairingIPFamily == ipfamily.IPv6 {
-					routes = frrRoutesV6
-				}
+					if pairingIPFamily == ipfamily.IPv6 {
+						routes = frrRoutesV6
+					}
 
-				for _, route := range routes {
-					if route.Destination.String() == toInject {
-						return fmt.Errorf("Found %s in %s routes", toInject, pod.Name)
+					for _, route := range routes {
+						if route.Destination.String() == toInject {
+							return fmt.Errorf("Found %s in %s routes", toInject, pod.Name)
+						}
 					}
 				}
+				return nil
 			}
-			return nil
-		}
 
-		Consistently(checkRoutesInjected, 30*time.Second, 1*time.Second).ShouldNot(HaveOccurred())
-		svc, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "external-local-lb")
-		defer testservice.Delete(cs, svc)
+			Consistently(checkRoutesInjected, 30*time.Second, 1*time.Second).ShouldNot(HaveOccurred())
+			svc, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "external-local-lb")
+			defer testservice.Delete(cs, svc)
 
-		Consistently(checkRoutesInjected, 30*time.Second, 1*time.Second).ShouldNot(HaveOccurred())
-	},
-		ginkgo.Entry("IPV4", "192.168.10.0/24", "172.16.1.10/32", ipfamily.IPv4),
-		ginkgo.Entry("IPV6", "fc00:f853:0ccd:e799::/116", "fc00:f853:ccd:e800::1/128", ipfamily.IPv6),
-	)
+			Consistently(checkRoutesInjected, 30*time.Second, 1*time.Second).ShouldNot(HaveOccurred())
+
+			ginkgo.By("adding a custom configuration that allows incoming routes")
+
+			data := ""
+			for _, c := range FRRContainers {
+				ip := c.Ipv4
+				if pairingIPFamily == ipfamily.IPv6 {
+					ip = c.Ipv6
+				}
+				ruleName := ip
+				if c.RouterConfig.VRF != "" {
+					ruleName = fmt.Sprintf("%s-%s", ip, c.RouterConfig.VRF)
+				}
+				data = data + fmt.Sprintf("route-map %s-in permit 20\n", ruleName)
+			}
+			extraData := map[string]string{
+				"extras": data,
+			}
+
+			err = k8s.CreateConfigmap(cs, "bgpextras", metallb.Namespace, extraData)
+			framework.ExpectNoError(err)
+			Eventually(checkRoutesInjected, 30*time.Second, 1*time.Second).Should(HaveOccurred())
+		},
+			ginkgo.Entry("IPV4", "192.168.10.0/24", "172.16.1.10/32", ipfamily.IPv4),
+			ginkgo.Entry("IPV6", "fc00:f853:0ccd:e799::/116", "fc00:f853:ccd:e800::1/128", ipfamily.IPv6),
+		)
+	})
 
 	ginkgo.Context("FRR validate reload feedback", func() {
 		ginkgo.It("should update MetalLB config and log reload-validate success", func() {
diff --git a/e2etest/bgptests/node_selector.go b/e2etest/bgptests/node_selector.go
index ff6c4a4e..5d45bffe 100644
--- a/e2etest/bgptests/node_selector.go
+++ b/e2etest/bgptests/node_selector.go
@@ -315,18 +315,18 @@ var _ = ginkgo.Describe("BGP Node Selector", func() {
 
 		},
 			ginkgo.Entry("First to one, second to two", map[int][]int{
-				0: []int{0},
-				1: []int{1, 2},
+				0: {0},
+				1: {1, 2},
 			}),
 			ginkgo.Entry("First to one, second to one, third to three", map[int][]int{
-				0: []int{0},
-				1: []int{1},
-				2: []int{0, 1, 2},
+				0: {0},
+				1: {1},
+				2: {0, 1, 2},
 			}),
 			ginkgo.Entry("First to one, second to one, third to same as first", map[int][]int{
-				0: []int{0},
-				1: []int{1},
-				2: []int{0},
+				0: {0},
+				1: {1},
+				2: {0},
 			}),
 		)
 	})
diff --git a/e2etest/pkg/k8s/configmap.go b/e2etest/pkg/k8s/configmap.go
new file mode 100644
index 00000000..7150711f
--- /dev/null
+++ b/e2etest/pkg/k8s/configmap.go
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package k8s
+
+import (
+	"context"
+
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientset "k8s.io/client-go/kubernetes"
+)
+
+func CreateConfigmap(cs clientset.Interface, name, namespace string, data map[string]string) error {
+	cm := corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Data: data,
+	}
+	_, err := cs.CoreV1().ConfigMaps(namespace).Create(context.Background(), &cm, metav1.CreateOptions{})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func RemoveConfigmap(cs clientset.Interface, name, namespace string) error {
+	err := cs.CoreV1().ConfigMaps(namespace).Delete(context.Background(), name, metav1.DeleteOptions{})
+	if err != nil && !apierrors.IsNotFound(err) {
+		return err
+	}
+	return nil
+}
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index 91a976ca..6d46b380 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -970,6 +970,38 @@ func TestTwoAdvertisementsTwoSessionsOneWithPeerSelector(t *testing.T) {
 	testCheckConfigFile(t)
 }
 
+func TestSingleSessionExtras(t *testing.T) {
+	testSetup(t)
+
+	l := log.NewNopLogger()
+	sessionManager := NewSessionManager(l, logging.LevelInfo)
+	defer close(sessionManager.reloadConfig)
+	err := sessionManager.SyncExtraInfo("# hello")
+	if err != nil {
+		t.Fatalf("Could not sync extra info")
+	}
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress:   "127.0.0.2:179",
+			SourceAddress: net.ParseIP("10.1.1.254"),
+			MyASN:         100,
+			RouterID:      net.ParseIP("10.1.1.254"),
+			PeerASN:       200,
+			HoldTime:      time.Second,
+			KeepAliveTime: time.Second,
+			Password:      "password",
+			CurrentNode:   "hostname",
+			EBGPMultiHop:  false,
+			SessionName:   "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
+
 func TestLoggingConfiguration(t *testing.T) {
 	testSetup(t)
 
diff --git a/internal/bgp/frr/testdata/TestSingleSessionExtras.golden b/internal/bgp/frr/testdata/TestSingleSessionExtras.golden
new file mode 100644
index 00000000..3f7e0eeb
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestSingleSessionExtras.golden
@@ -0,0 +1,40 @@
+log file /etc/frr/frr.log informational
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+route-map 127.0.0.2-in deny 20
+
+route-map 127.0.0.2-out permit 1
+  match ip address prefix-list 127.0.0.2-pl-ipv4
+route-map 127.0.0.2-out permit 2
+  match ipv6 address prefix-list 127.0.0.2-pl-ipv4
+
+
+ip prefix-list 127.0.0.2-pl-ipv4 deny any
+ipv6 prefix-list 127.0.0.2-pl-ipv4 deny any
+
+router bgp 100
+  no bgp ebgp-requires-policy
+  no bgp network import-check
+  no bgp default ipv4-unicast
+
+  bgp router-id 10.1.1.254
+  neighbor 127.0.0.2 remote-as 200
+  neighbor 127.0.0.2 port 179
+  neighbor 127.0.0.2 timers 1 1
+  neighbor 127.0.0.2 password password
+  neighbor 127.0.0.2 update-source 10.1.1.254
+
+  address-family ipv4 unicast
+    neighbor 127.0.0.2 activate
+    neighbor 127.0.0.2 route-map 127.0.0.2-in in
+    neighbor 127.0.0.2 route-map 127.0.0.2-out out
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 127.0.0.2 activate
+    neighbor 127.0.0.2 route-map 127.0.0.2-in in
+    neighbor 127.0.0.2 route-map 127.0.0.2-out out
+  exit-address-family
+
+# hello
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index a1a66ba5..7a2a9dc5 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -270,7 +270,7 @@ func TestParse(t *testing.T) {
 								Nodes:               map[string]bool{},
 							},
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+						L2Advertisements: []*L2Advertisement{{
 							Nodes:         map[string]bool{},
 							AllInterfaces: true,
 						}},
@@ -288,7 +288,7 @@ func TestParse(t *testing.T) {
 								Nodes:               map[string]bool{},
 							},
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+						L2Advertisements: []*L2Advertisement{{
 							Nodes:         map[string]bool{},
 							AllInterfaces: true,
 						}},
@@ -309,7 +309,7 @@ func TestParse(t *testing.T) {
 							ipnet("40.0.0.240/32"),
 							ipnet("40.0.0.250/32"),
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+						L2Advertisements: []*L2Advertisement{{
 							Nodes:         map[string]bool{},
 							AllInterfaces: true,
 						}},
@@ -318,7 +318,7 @@ func TestParse(t *testing.T) {
 					"pool4": {
 						Name: "pool4",
 						CIDR: []*net.IPNet{ipnet("2001:db8::/64")},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+						L2Advertisements: []*L2Advertisement{{
 							Nodes:         map[string]bool{},
 							AllInterfaces: true,
 						}},
@@ -416,6 +416,70 @@ func TestParse(t *testing.T) {
 			},
 		},
 
+		{
+			desc: "ip address pool with duplicate namespace selection",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"10.20.0.0/16",
+								"10.50.0.0/24",
+							},
+							AvoidBuggyIPs: true,
+							AutoAssign:    pointer.BoolPtr(false),
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 1,
+								Namespaces: []string{"test-ns1", "test-ns1"}},
+						},
+					},
+				},
+				Namespaces: []corev1.Namespace{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "test-ns1",
+						},
+					},
+				},
+			},
+		},
+
+		{
+			desc: "ip address pool with duplicate namespace selectors",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"10.20.0.0/16",
+								"10.50.0.0/24",
+							},
+							AvoidBuggyIPs: true,
+							AutoAssign:    pointer.BoolPtr(false),
+							AllocateTo: &v1beta1.ServiceAllocation{
+								Priority: 1,
+								NamespaceSelectors: []v1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}},
+									{MatchLabels: map[string]string{"foo": "bar"}}},
+							},
+						},
+					},
+				},
+				Namespaces: []corev1.Namespace{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name:   "test-ns1",
+							Labels: map[string]string{"foo": "bar"},
+						},
+					},
+				},
+			},
+		},
+
 		{
 			desc: "ip address pool with service selection",
 			crs: ClusterResources{
@@ -467,6 +531,32 @@ func TestParse(t *testing.T) {
 			},
 		},
 
+		{
+			desc: "ip address pool with duplicate service selection",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"30.0.0.0/8",
+							},
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 2,
+								ServiceSelectors: []v1.LabelSelector{{MatchExpressions: []metav1.LabelSelectorRequirement{
+									{
+										Key:      "foo",
+										Operator: metav1.LabelSelectorOpIn,
+										Values:   []string{"bar", "bar"},
+									},
+								}}}},
+						},
+					},
+				},
+			},
+		},
+
 		{
 			desc: "ip address pool with namespace and service selection",
 			crs: ClusterResources{
@@ -2731,7 +2821,7 @@ func TestParse(t *testing.T) {
 								Nodes:               map[string]bool{"second": true},
 							},
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+						L2Advertisements: []*L2Advertisement{{
 							Nodes: map[string]bool{
 								"first": true,
 							},
@@ -2982,7 +3072,7 @@ func TestParse(t *testing.T) {
 								},
 							},
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+						L2Advertisements: []*L2Advertisement{{
 							Nodes: map[string]bool{
 								"first":  true,
 								"second": true,
@@ -3150,7 +3240,7 @@ func TestContainsAdvertisement(t *testing.T) {
 		{
 			desc: "Contain",
 			advs: []*L2Advertisement{
-				&L2Advertisement{
+				{
 					Nodes: map[string]bool{
 						"nodeA": true,
 						"nodeB": true,
@@ -3158,7 +3248,7 @@ func TestContainsAdvertisement(t *testing.T) {
 					Interfaces:    []string{"eth0", "eth1"},
 					AllInterfaces: false,
 				},
-				&L2Advertisement{
+				{
 					Nodes: map[string]bool{
 						"nodeB": true,
 					},
@@ -3179,7 +3269,7 @@ func TestContainsAdvertisement(t *testing.T) {
 		{
 			desc: "Not contain: Nodes don't equal",
 			advs: []*L2Advertisement{
-				&L2Advertisement{
+				{
 					Nodes: map[string]bool{
 						"nodeA": true,
 						"nodeB": true,
@@ -3201,7 +3291,7 @@ func TestContainsAdvertisement(t *testing.T) {
 		{
 			desc: "Not contain: Interfaces don't equal",
 			advs: []*L2Advertisement{
-				&L2Advertisement{
+				{
 					Nodes: map[string]bool{
 						"nodeA": true,
 						"nodeB": true,
@@ -3223,7 +3313,7 @@ func TestContainsAdvertisement(t *testing.T) {
 		{
 			desc: "Not contain: AllInterfaces doesn't equal",
 			advs: []*L2Advertisement{
-				&L2Advertisement{
+				{
 					Nodes: map[string]bool{
 						"nodeA": true,
 						"nodeB": true,
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index b41c92f7..9fc7869b 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -126,6 +126,10 @@ func (f *fakeBGPSessionManager) SyncBFDProfiles(profiles map[string]*config.BFDP
 	return nil
 }
 
+func (f *fakeBGPSessionManager) SyncExtraInfo(extra string) error {
+	return nil
+}
+
 func (f *fakeBGPSessionManager) Ads() map[string][]*bgp.Advertisement {
 	ret := map[string][]*bgp.Advertisement{}