Repository: metallb/metallb
Pull request #1302 (2022-04-01 17:08:17)

Add a node selector in L2 announcement and BGP announcement
As described in https://github.com/metallb/metallb/blob/main/design/pool-configuration.md , we add a node selector to both the L2 advertisement and the BGP advertisement.

The semantic is, if the selector is present we should advertise only from the nodes selected by the selector. An empty selector is interpreted as willing to select all the nodes.

fedepaol: > "Fix helm when memberlist is enabled" commit: double "the" in the commit message

I intended it as "memberlist as a library", so I think it's fine as it is

Patch:
diff --git a/api/v1beta1/bgpadvertisement_types.go b/api/v1beta1/bgpadvertisement_types.go
index 0488daf1..d95b9063 100644
--- a/api/v1beta1/bgpadvertisement_types.go
+++ b/api/v1beta1/bgpadvertisement_types.go
@@ -46,6 +46,9 @@ type BGPAdvertisementSpec struct {
 
 	// IPAddressPools is the list of ipaddresspools to advertise via this advertisement.
 	IPAddressPools []string `json:"ipAddressPools,omitempty"`
+
+	// NodeSelectors is a selector on the node we should perform this advertisement from.
+	NodeSelectors []metav1.LabelSelector `json:"nodeSelectors,omitempty" yaml:"node-selectors,omitempty"`
 }
 
 // BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
diff --git a/api/v1beta1/l2advertisement_types.go b/api/v1beta1/l2advertisement_types.go
index cb5656ee..0fc48b1d 100644
--- a/api/v1beta1/l2advertisement_types.go
+++ b/api/v1beta1/l2advertisement_types.go
@@ -27,6 +27,8 @@ import (
 type L2AdvertisementSpec struct {
 	// IPAddressPools is the list of ipaddresspools to advertise via this advertisement.
 	IPAddressPools []string `json:"ipAddressPools,omitempty"`
+	// NodeSelectors is a selector on the node we should perform this advertisement from.
+	NodeSelectors []metav1.LabelSelector `json:"nodeSelectors,omitempty" yaml:"node-selectors,omitempty"`
 }
 
 // L2AdvertisementStatus defines the observed state of L2Advertisement.
diff --git a/charts/metallb/templates/crds.yaml b/charts/metallb/templates/crds.yaml
index 1489e217..19b8a185 100644
--- a/charts/metallb/templates/crds.yaml
+++ b/charts/metallb/templates/crds.yaml
@@ -363,6 +363,57 @@ spec:
                   lower localpref.
                 format: int32
                 type: integer
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
@@ -776,6 +827,57 @@ spec:
                 items:
                   type: string
                 type: array
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: L2AdvertisementStatus defines the observed state of L2Advertisement.
diff --git a/charts/metallb/templates/rbac.yaml b/charts/metallb/templates/rbac.yaml
index 584a6156..1040a524 100644
--- a/charts/metallb/templates/rbac.yaml
+++ b/charts/metallb/templates/rbac.yaml
@@ -75,7 +75,6 @@ rules:
 - apiGroups: ["metallb.io"]
   resources: ["ipaddresspools"]
   verbs: ["get", "list", "watch"]
-{{- if .Values.speaker.memberlist.enabled }}
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -83,6 +82,7 @@ metadata:
   name: {{ include "metallb.fullname" . }}-controller
   labels: {{- include "metallb.labels" . | nindent 4 }}
 rules:
+{{- if .Values.speaker.memberlist.enabled }}
 - apiGroups: [""]
   resources: ["secrets"]
   verbs: ["create", "get", "list", "watch"]
@@ -94,25 +94,22 @@ rules:
   resources: ["deployments"]
   resourceNames: ["{{ template "metallb.fullname" . }}-controller"]
   verbs: ["get"]
+{{- end }}
 - apiGroups: ["metallb.io"]
   resources: ["addresspools"]
   verbs: ["get", "list", "watch"]
 - apiGroups: ["metallb.io"]
-  resources: ["bfdprofiles"]
+  resources: ["ipaddresspools"]
   verbs: ["get", "list", "watch"]
 - apiGroups: ["metallb.io"]
   resources: ["bgppeers"]
-  verbs: ["get", "list", "watch"]
-- apiGroups: ["metallb.io"]
-  resources: ["l2advertisements"]
-  verbs: ["get", "list", "watch"]
+  verbs: ["get", "list"]
 - apiGroups: ["metallb.io"]
   resources: ["bgpadvertisements"]
-  verbs: ["get", "list", "watch"]
+  verbs: ["get", "list"]
 - apiGroups: ["metallb.io"]
-  resources: ["ipaddresspools"]
-  verbs: ["get", "list", "watch"]
-{{- end }}
+  resources: ["l2advertisements"]
+  verbs: ["get", "list"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
@@ -156,7 +153,6 @@ roleRef:
 subjects:
 - kind: ServiceAccount
   name: {{ include "metallb.speaker.serviceAccountName" . }}
-{{- if .Values.speaker.memberlist.enabled }}
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
@@ -171,4 +167,3 @@ subjects:
 - kind: ServiceAccount
   name: {{ include "metallb.controller.serviceAccountName" . }}
 {{- end -}}
-{{- end -}}
diff --git a/config/crd/bases/metallb.io_bgpadvertisements.yaml b/config/crd/bases/metallb.io_bgpadvertisements.yaml
index 15c17f6a..2915468d 100644
--- a/config/crd/bases/metallb.io_bgpadvertisements.yaml
+++ b/config/crd/bases/metallb.io_bgpadvertisements.yaml
@@ -66,6 +66,57 @@ spec:
                   lower localpref.
                 format: int32
                 type: integer
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
diff --git a/config/crd/bases/metallb.io_l2advertisements.yaml b/config/crd/bases/metallb.io_l2advertisements.yaml
index 85c628af..80488494 100644
--- a/config/crd/bases/metallb.io_l2advertisements.yaml
+++ b/config/crd/bases/metallb.io_l2advertisements.yaml
@@ -42,6 +42,57 @@ spec:
                 items:
                   type: string
                 type: array
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: L2AdvertisementStatus defines the observed state of L2Advertisement.
diff --git a/config/manifests/metallb-frr-with-webhooks.yaml b/config/manifests/metallb-frr-with-webhooks.yaml
index 0e527097..7bc727dd 100644
--- a/config/manifests/metallb-frr-with-webhooks.yaml
+++ b/config/manifests/metallb-frr-with-webhooks.yaml
@@ -362,6 +362,57 @@ spec:
                   lower localpref.
                 format: int32
                 type: integer
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
@@ -770,6 +821,57 @@ spec:
                 items:
                   type: string
                 type: array
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: L2AdvertisementStatus defines the observed state of L2Advertisement.
@@ -917,25 +1019,14 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - addresspools
-  verbs:
-  - create
-  - get
-  - list
-  - watch
-- apiGroups:
-  - metallb.io
-  resources:
-  - bfdprofiles
+  - bgppeers
   verbs:
-  - create
   - get
   - list
-  - watch
 - apiGroups:
   - metallb.io
   resources:
-  - bgppeers
+  - addresspools
   verbs:
   - get
   - list
@@ -943,7 +1034,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - l2advertisements
+  - ipaddresspools
   verbs:
   - get
   - list
@@ -959,7 +1050,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - ipaddresspools
+  - l2advertisements
   verbs:
   - get
   - list
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index b8b9a70a..9f265e7a 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -350,6 +350,57 @@ spec:
                   lower localpref.
                 format: int32
                 type: integer
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
@@ -746,6 +797,57 @@ spec:
                 items:
                   type: string
                 type: array
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: L2AdvertisementStatus defines the observed state of L2Advertisement.
@@ -893,25 +995,14 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - addresspools
-  verbs:
-  - create
-  - get
-  - list
-  - watch
-- apiGroups:
-  - metallb.io
-  resources:
-  - bfdprofiles
+  - bgppeers
   verbs:
-  - create
   - get
   - list
-  - watch
 - apiGroups:
   - metallb.io
   resources:
-  - bgppeers
+  - addresspools
   verbs:
   - get
   - list
@@ -919,7 +1010,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - l2advertisements
+  - ipaddresspools
   verbs:
   - get
   - list
@@ -935,7 +1026,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - ipaddresspools
+  - l2advertisements
   verbs:
   - get
   - list
diff --git a/config/manifests/metallb-native-with-webhooks.yaml b/config/manifests/metallb-native-with-webhooks.yaml
index d328842f..c3152d62 100644
--- a/config/manifests/metallb-native-with-webhooks.yaml
+++ b/config/manifests/metallb-native-with-webhooks.yaml
@@ -362,6 +362,57 @@ spec:
                   lower localpref.
                 format: int32
                 type: integer
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
@@ -770,6 +821,57 @@ spec:
                 items:
                   type: string
                 type: array
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: L2AdvertisementStatus defines the observed state of L2Advertisement.
@@ -917,25 +1019,14 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - addresspools
-  verbs:
-  - create
-  - get
-  - list
-  - watch
-- apiGroups:
-  - metallb.io
-  resources:
-  - bfdprofiles
+  - bgppeers
   verbs:
-  - create
   - get
   - list
-  - watch
 - apiGroups:
   - metallb.io
   resources:
-  - bgppeers
+  - addresspools
   verbs:
   - get
   - list
@@ -943,7 +1034,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - l2advertisements
+  - ipaddresspools
   verbs:
   - get
   - list
@@ -959,7 +1050,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - ipaddresspools
+  - l2advertisements
   verbs:
   - get
   - list
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index 57582bab..9f8354d9 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -350,6 +350,57 @@ spec:
                   lower localpref.
                 format: int32
                 type: integer
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
@@ -746,6 +797,57 @@ spec:
                 items:
                   type: string
                 type: array
+              nodeSelectors:
+                description: NodeSelectors is a selector on the node we should perform
+                  this advertisement from.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
             type: object
           status:
             description: L2AdvertisementStatus defines the observed state of L2Advertisement.
@@ -893,25 +995,14 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - addresspools
-  verbs:
-  - create
-  - get
-  - list
-  - watch
-- apiGroups:
-  - metallb.io
-  resources:
-  - bfdprofiles
+  - bgppeers
   verbs:
-  - create
   - get
   - list
-  - watch
 - apiGroups:
   - metallb.io
   resources:
-  - bgppeers
+  - addresspools
   verbs:
   - get
   - list
@@ -919,7 +1010,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - l2advertisements
+  - ipaddresspools
   verbs:
   - get
   - list
@@ -935,7 +1026,7 @@ rules:
 - apiGroups:
   - metallb.io
   resources:
-  - ipaddresspools
+  - l2advertisements
   verbs:
   - get
   - list
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 3c04ea92..c813f66a 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -181,25 +181,14 @@ rules:
   - apiGroups:
       - metallb.io
     resources:
-      - addresspools
-    verbs:
-      - create
-      - get
-      - list
-      - watch
-  - apiGroups:
-      - metallb.io
-    resources:
-      - bfdprofiles
+      - bgppeers
     verbs:
-      - create
       - get
       - list
-      - watch
   - apiGroups:
       - metallb.io
     resources:
-      - bgppeers
+      - addresspools
     verbs:
       - get
       - list
@@ -207,7 +196,7 @@ rules:
   - apiGroups:
       - metallb.io
     resources:
-      - l2advertisements
+      - ipaddresspools
     verbs:
       - get
       - list
@@ -223,7 +212,7 @@ rules:
   - apiGroups:
       - metallb.io
     resources:
-      - ipaddresspools
+      - l2advertisements
     verbs:
       - get
       - list
diff --git a/controller/main.go b/controller/main.go
index 1b3547bb..c4c08b35 100644
--- a/controller/main.go
+++ b/controller/main.go
@@ -43,7 +43,7 @@ type service interface {
 
 type controller struct {
 	client service
-	config *config.Config
+	pools  map[string]*config.Pool
 	ips    *allocator.Allocator
 }
 
@@ -59,7 +59,7 @@ func (c *controller) SetBalancer(l log.Logger, name string, svcRo *v1.Service, _
 		return controllers.SyncStateReprocessAll
 	}
 
-	if c.config == nil {
+	if c.pools == nil {
 		// Config hasn't been read, nothing we can do just yet.
 		level.Debug(l).Log("event", "noConfig", "msg", "not processing, still waiting for config")
 		return controllers.SyncStateSuccess
@@ -98,20 +98,20 @@ func (c *controller) deleteBalancer(l log.Logger, name string) {
 	}
 }
 
-func (c *controller) SetConfig(l log.Logger, cfg *config.Config) controllers.SyncState {
+func (c *controller) SetPools(l log.Logger, pools map[string]*config.Pool) controllers.SyncState {
 	level.Debug(l).Log("event", "startUpdate", "msg", "start of config update")
 	defer level.Debug(l).Log("event", "endUpdate", "msg", "end of config update")
 
-	if cfg == nil {
+	if len(pools) == 0 {
 		level.Error(l).Log("op", "setConfig", "error", "no MetalLB configuration in cluster", "msg", "configuration is missing, MetalLB will not function")
 		return controllers.SyncStateErrorNoRetry
 	}
 
-	if err := c.ips.SetPools(cfg.Pools); err != nil {
+	if err := c.ips.SetPools(pools); err != nil {
 		level.Error(l).Log("op", "setConfig", "error", err, "msg", "applying new configuration failed")
 		return controllers.SyncStateError
 	}
-	c.config = cfg
+	c.pools = pools
 	return controllers.SyncStateReprocessAll
 }
 
@@ -166,7 +166,7 @@ func main() {
 		Namespace: *namespace,
 		Listener: k8s.Listener{
 			ServiceChanged: c.SetBalancer,
-			ConfigChanged:  c.SetConfig,
+			PoolChanged:    c.SetPools,
 		},
 		ValidateConfig: validation,
 		EnableWebhook:  *enableWebhook,
diff --git a/controller/service.go b/controller/service.go
index c3dd1fb0..eb7adda1 100644
--- a/controller/service.go
+++ b/controller/service.go
@@ -156,7 +156,7 @@ func (c *controller) convergeBalancer(l log.Logger, key string, svc *v1.Service)
 	}
 
 	pool := c.ips.Pool(key)
-	if pool == "" || c.config.Pools[pool] == nil {
+	if pool == "" || c.pools[pool] == nil {
 		level.Error(l).Log("bug", "true", "ip", lbIPs, "msg", "internal error: allocated IP has no matching address pool")
 		c.client.Errorf(svc, "InternalError", "allocated an IP that has no pool")
 		c.clearServiceState(key, svc)
diff --git a/internal/config/config.go b/internal/config/config.go
index e1920c5b..125dc664 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -41,6 +41,7 @@ type ClusterResources struct {
 	L2Advs             []metallbv1beta1.L2Advertisement  `json:"l2advertisements"`
 	LegacyAddressPools []metallbv1beta1.AddressPool      `json:"legacyaddresspools"`
 	PasswordSecrets    map[string]corev1.Secret          `json:"passwordsecrets"`
+	Nodes              []corev1.Node                     `json:"nodes"`
 }
 
 // Config is a parsed MetalLB configuration.
@@ -135,9 +136,13 @@ type BGPAdvertisement struct {
 	LocalPref uint32
 	// Value of the COMMUNITIES path attribute.
 	Communities map[uint32]bool
+	// The map of nodes allowed for this advertisement
+	Nodes map[string]bool
 }
 
 type L2Advertisement struct {
+	// The map of nodes allowed for this advertisement
+	Nodes map[string]bool
 }
 
 // BFDProfile describes a BFD profile to be applied to a set of peers.
@@ -235,7 +240,10 @@ func For(resources ClusterResources, validate Validate) (*Config, error) {
 	}
 
 	for _, l2Adv := range resources.L2Advs {
-		adv := l2AdvertisementFromCR(l2Adv)
+		adv, err := l2AdvertisementFromCR(l2Adv, resources.Nodes)
+		if err != nil {
+			return nil, err
+		}
 		// No pool selector means select all pools
 		if len(l2Adv.Spec.IPAddressPools) == 0 {
 			for _, pool := range cfg.Pools {
@@ -260,7 +268,7 @@ func For(resources ClusterResources, validate Validate) (*Config, error) {
 	}
 
 	for _, bgpAdv := range resources.BGPAdvs {
-		adv, err := bgpAdvertisementFromCR(bgpAdv, communities)
+		adv, err := bgpAdvertisementFromCR(bgpAdv, communities, resources.Nodes)
 		if err != nil {
 			return nil, err
 		}
@@ -287,7 +295,11 @@ func For(resources ClusterResources, validate Validate) (*Config, error) {
 	}
 
 	for _, p := range resources.LegacyAddressPools {
-		pool, err := addressPoolFromLegacyCR(p, communities)
+		allNodes, err := selectedNodes(resources.Nodes, nil)
+		if err != nil {
+			return nil, err
+		}
+		pool, err := addressPoolFromLegacyCR(p, communities, allNodes)
 		if err != nil {
 			return nil, fmt.Errorf("parsing address pool %s: %s", p.Name, err)
 		}
@@ -450,7 +462,7 @@ func addressPoolFromCR(p metallbv1beta1.IPAddressPool, bgpCommunities map[string
 	return ret, nil
 }
 
-func addressPoolFromLegacyCR(p metallbv1beta1.AddressPool, bgpCommunities map[string]uint32) (*Pool, error) {
+func addressPoolFromLegacyCR(p metallbv1beta1.AddressPool, bgpCommunities map[string]uint32, allNodes map[string]bool) (*Pool, error) {
 	if p.Name == "" {
 		return nil, errors.New("missing pool name")
 	}
@@ -482,9 +494,9 @@ func addressPoolFromLegacyCR(p metallbv1beta1.AddressPool, bgpCommunities map[st
 		if len(p.Spec.BGPAdvertisements) > 0 {
 			return nil, errors.New("cannot have bgp-advertisements configuration element in a layer2 address pool")
 		}
-		ret.L2Advertisements = []*L2Advertisement{{}}
+		ret.L2Advertisements = []*L2Advertisement{{Nodes: allNodes}}
 	case BGP:
-		ads, err := bgpAdvertisementsFromLegacyCR(p.Spec.BGPAdvertisements, cidrsPerAddresses, bgpCommunities)
+		ads, err := bgpAdvertisementsFromLegacyCR(p.Spec.BGPAdvertisements, cidrsPerAddresses, bgpCommunities, allNodes)
 		if err != nil {
 			return nil, errors.Wrapf(err, "parsing BGP advertisements for %s", p.Name)
 		}
@@ -538,11 +550,17 @@ func bfdProfileFromCR(p metallbv1beta1.BFDProfile) (*BFDProfile, error) {
 	return res, nil
 }
 
-func l2AdvertisementFromCR(crdAd metallbv1beta1.L2Advertisement) *L2Advertisement {
-	return &L2Advertisement{}
+func l2AdvertisementFromCR(crdAd metallbv1beta1.L2Advertisement, nodes []corev1.Node) (*L2Advertisement, error) {
+	selected, err := selectedNodes(nodes, crdAd.Spec.NodeSelectors)
+	if err != nil {
+		return nil, errors.Wrapf(err, "Failed to parse node selector for %s", crdAd.Name)
+	}
+	return &L2Advertisement{
+		Nodes: selected,
+	}, nil
 }
 
-func bgpAdvertisementFromCR(crdAd metallbv1beta1.BGPAdvertisement, communities map[string]uint32) (*BGPAdvertisement, error) {
+func bgpAdvertisementFromCR(crdAd metallbv1beta1.BGPAdvertisement, communities map[string]uint32, nodes []corev1.Node) (*BGPAdvertisement, error) {
 	err := validateDuplicateCommunities(crdAd.Spec.Communities)
 	if err != nil {
 		return nil, err
@@ -581,10 +599,16 @@ func bgpAdvertisementFromCR(crdAd metallbv1beta1.BGPAdvertisement, communities m
 			ad.Communities[v] = true
 		}
 	}
+
+	selected, err := selectedNodes(nodes, crdAd.Spec.NodeSelectors)
+	if err != nil {
+		return nil, errors.Wrapf(err, "Failed to parse node selector for ls %s", crdAd.Name)
+	}
+	ad.Nodes = selected
 	return ad, nil
 }
 
-func bgpAdvertisementsFromLegacyCR(ads []metallbv1beta1.LegacyBgpAdvertisement, cidrsPerAddresses map[string][]*net.IPNet, communities map[string]uint32) ([]*BGPAdvertisement, error) {
+func bgpAdvertisementsFromLegacyCR(ads []metallbv1beta1.LegacyBgpAdvertisement, cidrsPerAddresses map[string][]*net.IPNet, communities map[string]uint32, allNodes map[string]bool) ([]*BGPAdvertisement, error) {
 	if len(ads) == 0 {
 		return []*BGPAdvertisement{
 			{
@@ -592,6 +616,7 @@ func bgpAdvertisementsFromLegacyCR(ads []metallbv1beta1.LegacyBgpAdvertisement,
 				AggregationLengthV6: 128,
 				LocalPref:           0,
 				Communities:         map[uint32]bool{},
+				Nodes:               allNodes,
 			},
 		}, nil
 	}
@@ -608,6 +633,7 @@ func bgpAdvertisementsFromLegacyCR(ads []metallbv1beta1.LegacyBgpAdvertisement,
 			AggregationLengthV6: 128,
 			LocalPref:           0,
 			Communities:         map[uint32]bool{},
+			Nodes:               allNodes,
 		}
 
 		if crdAd.AggregationLength != nil {
@@ -812,13 +838,40 @@ func validateDuplicateCommunities(communities []string) error {
 	return nil
 }
 
-// TODO: Currently there are no fields in the L2Advertisement, so it is enough to check
-// if the list is not empty. This must be extended if we are going to add new fields to the l2 advertisements.
 func containsAdvertisement(advs []*L2Advertisement, toCheck *L2Advertisement) bool {
 	for _, adv := range advs {
-		if *adv == *toCheck {
+		if reflect.DeepEqual(adv.Nodes, toCheck.Nodes) {
 			return true
 		}
 	}
 	return false
 }
+
+func selectedNodes(nodes []corev1.Node, selectors []metav1.LabelSelector) (map[string]bool, error) {
+	labelSelectors := []labels.Selector{}
+	for _, selector := range selectors {
+		l, err := metav1.LabelSelectorAsSelector(&selector)
+		if err != nil {
+			return nil, errors.Wrapf(err, "Invalid label selector %v", selector)
+		}
+		labelSelectors = append(labelSelectors, l)
+	}
+
+	res := make(map[string]bool)
+OUTER:
+	for _, node := range nodes {
+		if len(labelSelectors) == 0 { // no selector mean all nodes are valid
+			res[node.Name] = true
+		}
+		for _, s := range labelSelectors {
+			nodeLabels := labels.Set(node.Labels)
+			if s.Matches(nodeLabels) {
+				res[node.Name] = true
+				continue OUTER
+			}
+		}
+
+	}
+	return res, nil
+
+}
diff --git a/internal/k8s/controllers/config_controller.go b/internal/k8s/controllers/config_controller.go
index fd6b1c54..a12f268e 100644
--- a/internal/k8s/controllers/config_controller.go
+++ b/internal/k8s/controllers/config_controller.go
@@ -50,7 +50,7 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 
 	var addressPools metallbv1beta1.AddressPoolList
 	if err := r.List(ctx, &addressPools, client.InNamespace(r.Namespace)); err != nil {
-		level.Error(r.Logger).Log("controller", "ConfigReconciler", "error", "failed to get addresspools", "error", err)
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get addresspools", "error", err)
 		return ctrl.Result{}, err
 	}
 
@@ -62,25 +62,25 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 
 	var bgpPeers metallbv1beta2.BGPPeerList
 	if err := r.List(ctx, &bgpPeers, client.InNamespace(r.Namespace)); err != nil {
-		level.Error(r.Logger).Log("controller", "ConfigReconciler", "error", "failed to get bgppeers", "error", err)
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get bgppeers", "error", err)
 		return ctrl.Result{}, err
 	}
 
 	var bfdProfiles metallbv1beta1.BFDProfileList
 	if err := r.List(ctx, &bfdProfiles, client.InNamespace(r.Namespace)); err != nil {
-		level.Error(r.Logger).Log("controller", "ConfigReconciler", "error", "failed to get bfdprofiles", "error", err)
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get bfdprofiles", "error", err)
 		return ctrl.Result{}, err
 	}
 
 	var l2Advertisements metallbv1beta1.L2AdvertisementList
 	if err := r.List(ctx, &l2Advertisements, client.InNamespace(r.Namespace)); err != nil {
-		level.Error(r.Logger).Log("controller", "ConfigReconciler", "error", "failed to get l2 advertisements", "error", err)
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get l2 advertisements", "error", err)
 		return ctrl.Result{}, err
 	}
 
 	var bgpAdvertisements metallbv1beta1.BGPAdvertisementList
 	if err := r.List(ctx, &bgpAdvertisements, client.InNamespace(r.Namespace)); err != nil {
-		level.Error(r.Logger).Log("controller", "ConfigReconciler", "error", "failed to get bgp advertisements", "error", err)
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get bgp advertisements", "error", err)
 		return ctrl.Result{}, err
 	}
 
@@ -89,6 +89,12 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		return ctrl.Result{}, err
 	}
 
+	var nodes corev1.NodeList
+	if err := r.List(ctx, &nodes); err != nil {
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get nodes", "error", err)
+		return ctrl.Result{}, err
+	}
+
 	resources := config.ClusterResources{
 		Pools:              ipAddressPools.Items,
 		Peers:              bgpPeers.Items,
@@ -97,6 +103,7 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		BGPAdvs:            bgpAdvertisements.Items,
 		LegacyAddressPools: addressPools.Items,
 		PasswordSecrets:    secrets,
+		Nodes:              nodes.Items,
 	}
 
 	level.Debug(r.Logger).Log("controller", "ConfigReconciler", "metallb CRs and Secrets", dumpClusterResources(&resources))
@@ -130,6 +137,7 @@ func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&metallbv1beta2.BGPPeer{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.IPAddressPool{}}, &handler.EnqueueRequestForObject{}).
+		Watches(&source.Kind{Type: &corev1.Node{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.BGPAdvertisement{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.L2Advertisement{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.BFDProfile{}}, &handler.EnqueueRequestForObject{}).
diff --git a/internal/k8s/controllers/node_controller.go b/internal/k8s/controllers/node_controller.go
index 33e11e90..86139d3c 100644
--- a/internal/k8s/controllers/node_controller.go
+++ b/internal/k8s/controllers/node_controller.go
@@ -26,12 +26,14 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 )
 
 type NodeReconciler struct {
 	client.Client
 	Logger    log.Logger
 	Scheme    *runtime.Scheme
+	NodeName  string
 	Namespace string
 	Handler   func(log.Logger, *v1.Node) SyncState
 }
@@ -60,7 +62,18 @@ func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
 }
 
 func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	p := predicate.NewPredicateFuncs(
+		func(obj client.Object) bool {
+			node, ok := obj.(*v1.Node)
+			if !ok {
+				level.Error(r.Logger).Log("controller", "NodeReconciler", "error", "object is not node", "name", obj.GetName())
+				return false
+			}
+			return node.Name == r.NodeName
+		})
+
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1.Node{}).
+		WithEventFilter(p).
 		Complete(r)
 }
diff --git a/internal/k8s/controllers/pool_controller.go b/internal/k8s/controllers/pool_controller.go
new file mode 100644
index 00000000..b043d63d
--- /dev/null
+++ b/internal/k8s/controllers/pool_controller.go
@@ -0,0 +1,97 @@
+/*
+
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controllers
+
+import (
+	"context"
+
+	"github.com/davecgh/go-spew/spew"
+	"github.com/go-kit/kit/log"
+	"github.com/go-kit/kit/log/level"
+	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
+	"go.universe.tf/metallb/internal/config"
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/source"
+)
+
+type PoolReconciler struct {
+	client.Client
+	Logger         log.Logger
+	Scheme         *runtime.Scheme
+	Namespace      string
+	Handler        func(log.Logger, map[string]*config.Pool) SyncState
+	ValidateConfig config.Validate
+	ForceReload    func()
+}
+
+func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	level.Info(r.Logger).Log("controller", "PoolReconciler", "start reconcile", req.NamespacedName.String())
+	defer level.Info(r.Logger).Log("controller", "PoolReconciler", "end reconcile", req.NamespacedName.String())
+
+	var addressPools metallbv1beta1.AddressPoolList
+	if err := r.List(ctx, &addressPools, client.InNamespace(r.Namespace)); err != nil {
+		level.Error(r.Logger).Log("controller", "PoolReconciler", "message", "failed to get addresspools", "error", err)
+		return ctrl.Result{}, err
+	}
+
+	var ipAddressPools metallbv1beta1.IPAddressPoolList
+	if err := r.List(ctx, &ipAddressPools, client.InNamespace(r.Namespace)); err != nil {
+		level.Error(r.Logger).Log("controller", "PoolReconciler", "message", "failed to get ipaddresspools", "error", err)
+		return ctrl.Result{}, err
+	}
+
+	resources := config.ClusterResources{
+		Pools:              ipAddressPools.Items,
+		LegacyAddressPools: addressPools.Items,
+	}
+
+	level.Debug(r.Logger).Log("controller", "PoolReconciler", "metallb CRs", spew.Sdump(resources))
+
+	cfg, err := config.For(resources, r.ValidateConfig)
+	if err != nil {
+		level.Error(r.Logger).Log("controller", "PoolReconciler", "error", "failed to parse the configuration", "error", err)
+		return ctrl.Result{}, nil
+	}
+
+	level.Debug(r.Logger).Log("controller", "PoolReconciler", "rendered config", spew.Sdump(cfg))
+
+	res := r.Handler(r.Logger, cfg.Pools)
+	switch res {
+	case SyncStateError:
+		level.Error(r.Logger).Log("controller", "PoolReconciler", "metallb CRs and Secrets", spew.Sdump(resources), "event", "reload failed, retry")
+		return ctrl.Result{}, retryError
+	case SyncStateReprocessAll:
+		level.Info(r.Logger).Log("controller", "PoolReconciler", "event", "force service reload")
+		r.ForceReload()
+	case SyncStateErrorNoRetry:
+		level.Error(r.Logger).Log("controller", "PoolReconciler", "metallb CRs and Secrets", spew.Sdump(resources), "event", "reload failed, no retry")
+		return ctrl.Result{}, nil
+	}
+
+	level.Info(r.Logger).Log("controller", "PoolReconciler", "event", "config reloaded")
+	return ctrl.Result{}, nil
+}
+
+func (r *PoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&metallbv1beta1.IPAddressPool{}).
+		Watches(&source.Kind{Type: &metallbv1beta1.AddressPool{}}, &handler.EnqueueRequestForObject{}).
+		Complete(r)
+}
diff --git a/internal/k8s/controllers/service_controller.go b/internal/k8s/controllers/service_controller.go
index 05af3646..961d54d7 100644
--- a/internal/k8s/controllers/service_controller.go
+++ b/internal/k8s/controllers/service_controller.go
@@ -54,13 +54,13 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 
 	service, err := r.serviceFor(ctx, req.NamespacedName)
 	if err != nil {
-		level.Error(r.Logger).Log("controller", "ServiceReconciler", "error", "failed to get service", "service", req.NamespacedName, "error", err)
+		level.Error(r.Logger).Log("controller", "ServiceReconciler", "message", "failed to get service", "service", req.NamespacedName, "error", err)
 		return ctrl.Result{}, err
 	}
 
 	epSlices, err := epsOrSlicesForServices(ctx, r, req.NamespacedName, r.Endpoints)
 	if err != nil {
-		level.Error(r.Logger).Log("controller", "ServiceReconciler", "error", "failed to get endpoints", "service", req.NamespacedName, "error", err)
+		level.Error(r.Logger).Log("controller", "ServiceReconciler", "message", "failed to get endpoints", "service", req.NamespacedName, "error", err)
 		return ctrl.Result{}, err
 	}
 	if service != nil {
@@ -98,7 +98,7 @@ func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
 					}
 					serviceName, err := epslices.ServiceKeyForSlice(epSlice)
 					if err != nil {
-						level.Error(r.Logger).Log("controller", "ServiceReconciler", "error", "failed to get serviceName for slice", "error", err, "epslice", epSlice.Name)
+						level.Error(r.Logger).Log("controller", "ServiceReconciler", "message", "failed to get serviceName for slice", "error", err, "epslice", epSlice.Name)
 						return []reconcile.Request{}
 					}
 					level.Debug(r.Logger).Log("controller", "ServiceReconciler", "enqueueing", serviceName, "epslice", dumpResource(epSlice))
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index f663204d..1025db8d 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -169,12 +169,28 @@ func New(cfg *Config) (*Client, error) {
 		}
 	}
 
+	if cfg.PoolChanged != nil {
+		if err = (&controllers.PoolReconciler{
+			Client:         mgr.GetClient(),
+			Logger:         cfg.Logger,
+			Scheme:         mgr.GetScheme(),
+			Namespace:      cfg.Namespace,
+			ValidateConfig: cfg.ValidateConfig,
+			Handler:        cfg.PoolHandler,
+			ForceReload:    reload,
+		}).SetupWithManager(mgr); err != nil {
+			level.Error(c.logger).Log("error", err, "unable to create controller", "config")
+			return nil, errors.Wrap(err, "failed to create config reconciler")
+		}
+	}
+
 	if cfg.NodeChanged != nil {
 		if err = (&controllers.NodeReconciler{
-			Client:  mgr.GetClient(),
-			Logger:  cfg.Logger,
-			Scheme:  mgr.GetScheme(),
-			Handler: cfg.NodeHandler,
+			Client:   mgr.GetClient(),
+			Logger:   cfg.Logger,
+			Scheme:   mgr.GetScheme(),
+			Handler:  cfg.NodeHandler,
+			NodeName: cfg.NodeName,
 		}).SetupWithManager(mgr); err != nil {
 			level.Error(c.logger).Log("error", err, "unable to create controller", "node")
 			return nil, errors.Wrap(err, "failed to create node reconciler")
diff --git a/internal/k8s/listener.go b/internal/k8s/listener.go
index e4cd3ee8..284c3819 100644
--- a/internal/k8s/listener.go
+++ b/internal/k8s/listener.go
@@ -16,6 +16,7 @@ type Listener struct {
 	sync.Mutex
 	ServiceChanged func(log.Logger, string, *v1.Service, epslices.EpsOrSlices) controllers.SyncState
 	ConfigChanged  func(log.Logger, *config.Config) controllers.SyncState
+	PoolChanged    func(log.Logger, map[string]*config.Pool) controllers.SyncState
 	NodeChanged    func(log.Logger, *v1.Node) controllers.SyncState
 }
 
@@ -24,6 +25,7 @@ func (l *Listener) ServiceHandler(logger log.Logger, serviceName string, svc *v1
 	defer l.Unlock()
 	return l.ServiceChanged(logger, serviceName, svc, endpointsOrSlices)
 }
+
 func (l *Listener) ConfigHandler(logger log.Logger, config *config.Config) controllers.SyncState {
 	l.Lock()
 	defer l.Unlock()
@@ -35,3 +37,9 @@ func (l *Listener) NodeHandler(logger log.Logger, node *v1.Node) controllers.Syn
 	defer l.Unlock()
 	return l.NodeChanged(logger, node)
 }
+
+func (l *Listener) PoolHandler(logger log.Logger, pools map[string]*config.Pool) controllers.SyncState {
+	l.Lock()
+	defer l.Unlock()
+	return l.PoolChanged(logger, pools)
+}
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 235b9381..307a9ec3 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -154,7 +154,11 @@ func hasHealthyEndpoint(eps epslices.EpsOrSlices, filterNode func(*string) bool)
 	return false
 }
 
-func (c *bgpController) ShouldAnnounce(l log.Logger, name string, _ []net.IP, svc *v1.Service, eps epslices.EpsOrSlices) string {
+func (c *bgpController) ShouldAnnounce(l log.Logger, name string, _ []net.IP, pool *config.Pool, svc *v1.Service, eps epslices.EpsOrSlices) string {
+	if !poolMatchesNodeBGP(pool, c.myNode) {
+		level.Debug(l).Log("event", "skipping should announce bgp", "service", name, "reason", "pool not matching my node")
+		return "notOwner"
+	}
 	// Should we advertise?
 	// Yes, if externalTrafficPolicy is
 	//  Cluster && any healthy endpoint exists
@@ -247,6 +251,10 @@ func (c *bgpController) SetBalancer(l log.Logger, name string, lbIPs []net.IP, p
 	c.svcAds[name] = nil
 	for _, lbIP := range lbIPs {
 		for _, adCfg := range pool.BGPAdvertisements {
+			// skipping if this node is not enabled for this advertisement
+			if !adCfg.Nodes[c.myNode] {
+				continue
+			}
 			m := net.CIDRMask(adCfg.AggregationLength, 32)
 			if lbIP.To4() == nil {
 				m = net.CIDRMask(adCfg.AggregationLengthV6, 128)
@@ -319,10 +327,6 @@ func (c *bgpController) SetNode(l log.Logger, node *v1.Node) error {
 	return c.syncPeers(l)
 }
 
-func (c *bgpController) PoolEnabledForProtocol(pool *config.Pool) bool {
-	return len(pool.BGPAdvertisements) > 0
-}
-
 // Create a new 'bgp.SessionManager' of type 'bgpType'.
 var newBGP = func(bgpType bgpImplementation, l log.Logger, logLevel logging.Level) bgp.SessionManager {
 	switch bgpType {
@@ -334,3 +338,12 @@ var newBGP = func(bgpType bgpImplementation, l log.Logger, logLevel logging.Leve
 		panic(fmt.Sprintf("unsupported BGP implementation type: %s", bgpType))
 	}
 }
+
+func poolMatchesNodeBGP(pool *config.Pool, node string) bool {
+	for _, adv := range pool.BGPAdvertisements {
+		if adv.Nodes[node] {
+			return true
+		}
+	}
+	return false
+}
diff --git a/speaker/layer2_controller.go b/speaker/layer2_controller.go
index a07c4f66..9a07442e 100644
--- a/speaker/layer2_controller.go
+++ b/speaker/layer2_controller.go
@@ -93,16 +93,24 @@ func usableNodes(eps epslices.EpsOrSlices, speakers map[string]bool) []string {
 	return ret
 }
 
-func (c *layer2Controller) ShouldAnnounce(l log.Logger, name string, toAnnounce []net.IP, svc *v1.Service, eps epslices.EpsOrSlices) string {
+func (c *layer2Controller) ShouldAnnounce(l log.Logger, name string, toAnnounce []net.IP, pool *config.Pool, svc *v1.Service, eps epslices.EpsOrSlices) string {
 	if !activeEndpointExists(eps) { // no active endpoints, just return
 		level.Debug(l).Log("event", "shouldannounce", "protocol", "l2", "message", "failed no active endpoints", "service", name)
 		return "notOwner"
 	}
+
+	if !poolMatchesNodeL2(pool, c.myNode) {
+		level.Debug(l).Log("event", "skipping should announce l2", "service", name, "reason", "pool not matching my node")
+		return "notOwner"
+	}
+
+	// we select the nodes with at least one matching l2 advertisement
+	forPool := speakersForPool(c.sList.UsableSpeakers(), pool)
 	var nodes []string
 	if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
-		nodes = usableNodes(eps, c.sList.UsableSpeakers())
+		nodes = usableNodes(eps, forPool)
 	} else {
-		nodes = nodesWithActiveSpeakers(c.sList.UsableSpeakers())
+		nodes = nodesWithActiveSpeakers(forPool)
 	}
 	// Using the first IP should work for both single and dual stack.
 	ipString := toAnnounce[0].String()
@@ -145,10 +153,6 @@ func (c *layer2Controller) SetNode(log.Logger, *v1.Node) error {
 	return nil
 }
 
-func (c *layer2Controller) PoolEnabledForProtocol(pool *config.Pool) bool {
-	return len(pool.L2Advertisements) > 0
-}
-
 // nodesWithActiveSpeakers returns the list of nodes with active speakers.
 func nodesWithActiveSpeakers(speakers map[string]bool) []string {
 	var ret []string
@@ -179,3 +183,22 @@ func activeEndpointExists(eps epslices.EpsOrSlices) bool {
 	}
 	return false
 }
+
+func poolMatchesNodeL2(pool *config.Pool, node string) bool {
+	for _, adv := range pool.L2Advertisements {
+		if adv.Nodes[node] {
+			return true
+		}
+	}
+	return false
+}
+
+func speakersForPool(speakers map[string]bool, pool *config.Pool) map[string]bool {
+	res := map[string]bool{}
+	for s := range speakers {
+		if poolMatchesNodeL2(pool, s) {
+			res[s] = true
+		}
+	}
+	return res
+}
diff --git a/speaker/main.go b/speaker/main.go
index d2877347..2cf5b7d9 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -316,14 +316,7 @@ func (c *controller) handleService(l log.Logger,
 		return c.deleteBalancerProtocol(l, protocol, name, "internalError")
 	}
 
-	if !handler.PoolEnabledForProtocol(pool) {
-		if c.announced[protocol][name] {
-			return c.deleteBalancerProtocol(l, protocol, name, "protocol disabled")
-		}
-		return controllers.SyncStateSuccess
-	}
-
-	if deleteReason := handler.ShouldAnnounce(l, name, lbIPs, svc, eps); deleteReason != "" {
+	if deleteReason := handler.ShouldAnnounce(l, name, lbIPs, pool, svc, eps); deleteReason != "" {
 		return c.deleteBalancerProtocol(l, protocol, name, deleteReason)
 	}
 
@@ -475,11 +468,10 @@ func (c *controller) SetNode(l log.Logger, node *v1.Node) controllers.SyncState
 // A Protocol can advertise an IP address.
 type Protocol interface {
 	SetConfig(log.Logger, *config.Config) error
-	ShouldAnnounce(log.Logger, string, []net.IP, *v1.Service, epslices.EpsOrSlices) string
+	ShouldAnnounce(log.Logger, string, []net.IP, *config.Pool, *v1.Service, epslices.EpsOrSlices) string
 	SetBalancer(log.Logger, string, []net.IP, *config.Pool) error
 	DeleteBalancer(log.Logger, string, string) error
 	SetNode(log.Logger, *v1.Node) error
-	PoolEnabledForProtocol(pool *config.Pool) bool
 }
 
 // Speakerlist represents a list of healthy speakers.
diff --git a/tasks.py b/tasks.py
index 91dcdbe6..746f2300 100644
--- a/tasks.py
+++ b/tasks.py
@@ -745,7 +745,7 @@ def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="k
         ips_for_containers_v6 = "--ips-for-containers-v6=" + ips
 
     testrun = run("cd `git rev-parse --show-toplevel`/e2etest &&"
-            "KUBECONFIG={} go test -timeout 1h {} {} --provider=local --kubeconfig={} --service-pod-port={} {} {} -ipv4-service-range={} -ipv6-service-range={} {}".format(kubeconfig, ginkgo_focus, ginkgo_skip, kubeconfig, service_pod_port, ips_for_containers_v4, ips_for_containers_v6, ipv4_service_range, ipv6_service_range, opt_skip_docker), warn="True")
+            "KUBECONFIG={} go test -timeout 2h {} {} --provider=local --kubeconfig={} --service-pod-port={} {} {} -ipv4-service-range={} -ipv6-service-range={} {}".format(kubeconfig, ginkgo_focus, ginkgo_skip, kubeconfig, service_pod_port, ips_for_containers_v4, ips_for_containers_v6, ipv4_service_range, ipv6_service_range, opt_skip_docker), warn="True")
 
     if export != None:
         run("kind export logs {}".format(export))


Test patch:
diff --git a/controller/controller_test.go b/controller/controller_test.go
index 0d242730..36c9c4b2 100644
--- a/controller/controller_test.go
+++ b/controller/controller_test.go
@@ -112,28 +112,26 @@ func TestControllerMutation(t *testing.T) {
 		ips:    allocator.New(),
 		client: k,
 	}
-	cfg := &config.Config{
-		Pools: map[string]*config.Pool{
-			"pool1": {
-				AutoAssign: true,
-				CIDR:       []*net.IPNet{ipnet("1.2.3.0/31")},
-			},
-			"pool2": {
-				AutoAssign: false,
-				CIDR:       []*net.IPNet{ipnet("3.4.5.6/32")},
-			},
-			"pool3": {
-				AutoAssign: true,
-				CIDR:       []*net.IPNet{ipnet("1000::/127")},
-			},
-			"pool4": {
-				AutoAssign: false,
-				CIDR:       []*net.IPNet{ipnet("2000::1/128")},
-			},
-			"pool5": {
-				AutoAssign: true,
-				CIDR:       []*net.IPNet{ipnet("1.2.3.0/31"), ipnet("1000::/127")},
-			},
+	pools := map[string]*config.Pool{
+		"pool1": {
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("1.2.3.0/31")},
+		},
+		"pool2": {
+			AutoAssign: false,
+			CIDR:       []*net.IPNet{ipnet("3.4.5.6/32")},
+		},
+		"pool3": {
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("1000::/127")},
+		},
+		"pool4": {
+			AutoAssign: false,
+			CIDR:       []*net.IPNet{ipnet("2000::1/128")},
+		},
+		"pool5": {
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("1.2.3.0/31"), ipnet("1000::/127")},
 		},
 	}
 
@@ -142,8 +140,8 @@ func TestControllerMutation(t *testing.T) {
 	// For this test, we just set a static config and immediately sync
 	// the controller. The mutations around config setting and syncing
 	// are tested elsewhere.
-	if c.SetConfig(l, cfg) == controllers.SyncStateError {
-		t.Fatalf("SetConfig failed")
+	if c.SetPools(l, pools) == controllers.SyncStateError {
+		t.Fatalf("SetPools failed")
 	}
 
 	// In steady state, every input below should be equivalent to a
@@ -812,8 +810,8 @@ func TestControllerConfig(t *testing.T) {
 	// Set an empty config. Balancer should still not do anything to
 	// our unallocated service, and return an error to force a
 	// retry after sync is complete.
-	if c.SetConfig(l, &config.Config{}) == controllers.SyncStateError {
-		t.Fatalf("SetConfig with empty config failed")
+	if c.SetPools(l, map[string]*config.Pool{}) == controllers.SyncStateError {
+		t.Fatalf("SetPools with empty config failed")
 	}
 
 	gotSvc = k.gotService(svc)
@@ -825,16 +823,14 @@ func TestControllerConfig(t *testing.T) {
 	}
 
 	// Set a config with some IPs. Still no allocation, not synced.
-	cfg := &config.Config{
-		Pools: map[string]*config.Pool{
-			"default": {
-				AutoAssign: true,
-				CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
-			},
+	pools := map[string]*config.Pool{
+		"default": {
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 		},
 	}
-	if c.SetConfig(l, cfg) == controllers.SyncStateError {
-		t.Fatalf("SetConfig failed")
+	if c.SetPools(l, pools) == controllers.SyncStateError {
+		t.Fatalf("SetPools failed")
 	}
 
 	gotSvc = k.gotService(svc)
@@ -858,13 +854,13 @@ func TestControllerConfig(t *testing.T) {
 	}
 
 	// Now that an IP is allocated, removing the IP pool is not allowed.
-	if c.SetConfig(l, &config.Config{}) != controllers.SyncStateError {
-		t.Fatalf("SetConfig that deletes allocated IPs was accepted")
+	if c.SetPools(l, map[string]*config.Pool{}) == controllers.SyncStateError {
+		t.Fatalf("SetPools that deletes allocated IPs was accepted")
 	}
 
 	// Deleting the config also makes MetalLB sad.
-	if c.SetConfig(l, nil) != controllers.SyncStateErrorNoRetry {
-		t.Fatalf("SetConfig that deletes the config was accepted")
+	if c.SetPools(l, nil) != controllers.SyncStateErrorNoRetry {
+		t.Fatalf("SetPools that deletes the config was accepted")
 	}
 }
 
@@ -876,16 +872,14 @@ func TestDeleteRecyclesIP(t *testing.T) {
 	}
 
 	l := log.NewNopLogger()
-	cfg := &config.Config{
-		Pools: map[string]*config.Pool{
-			"default": {
-				AutoAssign: true,
-				CIDR:       []*net.IPNet{ipnet("1.2.3.0/32")},
-			},
+	pools := map[string]*config.Pool{
+		"default": {
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("1.2.3.0/32")},
 		},
 	}
-	if c.SetConfig(l, cfg) == controllers.SyncStateError {
-		t.Fatal("SetConfig failed")
+	if c.SetPools(l, pools) == controllers.SyncStateError {
+		t.Fatal("SetPools failed")
 	}
 
 	svc1 := &v1.Service{
@@ -968,8 +962,8 @@ func TestControllerDualStackConfig(t *testing.T) {
 	// Set an empty config. Balancer should still not do anything to
 	// our unallocated service, and return an error to force a
 	// retry after sync is complete.
-	if c.SetConfig(l, &config.Config{}) == controllers.SyncStateError {
-		t.Fatalf("SetConfig with empty config failed")
+	if c.SetPools(l, map[string]*config.Pool{}) == controllers.SyncStateError {
+		t.Fatalf("SetPools with empty config failed")
 	}
 
 	gotSvc = k.gotService(svc)
@@ -981,16 +975,15 @@ func TestControllerDualStackConfig(t *testing.T) {
 	}
 
 	// Set a config with some IPs. Still no allocation, not synced.
-	cfg := &config.Config{
-		Pools: map[string]*config.Pool{
-			"default": {
-				AutoAssign: true,
-				CIDR:       []*net.IPNet{ipnet("1.2.3.0/24"), ipnet("1000::1/127")},
-			},
+	pools := map[string]*config.Pool{
+		"default": {
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("1.2.3.0/24"), ipnet("1000::1/127")},
 		},
 	}
-	if c.SetConfig(l, cfg) == controllers.SyncStateError {
-		t.Fatalf("SetConfig failed")
+
+	if c.SetPools(l, pools) == controllers.SyncStateError {
+		t.Fatalf("SetPools failed")
 	}
 
 	gotSvc = k.gotService(svc)
@@ -1014,13 +1007,13 @@ func TestControllerDualStackConfig(t *testing.T) {
 	}
 
 	// Now that an IP is allocated, removing the IP pool is not allowed.
-	if c.SetConfig(l, &config.Config{}) != controllers.SyncStateError {
-		t.Fatalf("SetConfig that deletes allocated IPs was accepted")
+	if c.SetPools(l, map[string]*config.Pool{}) != controllers.SyncStateErrorNoRetry {
+		t.Fatalf("SetPools that deletes allocated IPs was accepted")
 	}
 
 	// Deleting the config also makes MetalLB sad.
-	if c.SetConfig(l, nil) != controllers.SyncStateErrorNoRetry {
-		t.Fatalf("SetConfig that deletes the config was accepted")
+	if c.SetPools(l, nil) != controllers.SyncStateErrorNoRetry {
+		t.Fatalf("SetPools that deletes the config was accepted")
 	}
 }
 
@@ -1033,16 +1026,14 @@ func TestControllerSvcAddressSharing(t *testing.T) {
 	l := log.NewNopLogger()
 
 	// Set a config with some IPs. Still no allocation, not synced.
-	cfg := &config.Config{
-		Pools: map[string]*config.Pool{
-			"default": {
-				AutoAssign: true,
-				CIDR:       []*net.IPNet{ipnet("4.5.6.0/24")},
-			},
+	pool := map[string]*config.Pool{
+		"default": {
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("4.5.6.0/24")},
 		},
 	}
-	if c.SetConfig(l, cfg) == controllers.SyncStateError {
-		t.Fatalf("SetConfig failed")
+	if c.SetPools(l, pool) == controllers.SyncStateError {
+		t.Fatalf("SetPools failed")
 	}
 
 	// Configure svc1
diff --git a/e2etest/bgptests/bgp.go b/e2etest/bgptests/bgp.go
index 7a78e6b7..43bc224a 100644
--- a/e2etest/bgptests/bgp.go
+++ b/e2etest/bgptests/bgp.go
@@ -52,11 +52,12 @@ import (
 )
 
 const (
-	v4PoolAddresses      = "192.168.10.0/24"
-	v6PoolAddresses      = "fc00:f853:0ccd:e799::/124"
-	CommunityNoAdv       = "65535:65282" // 0xFFFFFF02: NO_ADVERTISE
-	IPLocalPref          = uint32(300)
-	SpeakerContainerName = "speaker"
+	v4PoolAddresses       = "192.168.10.0/24"
+	v6PoolAddresses       = "fc00:f853:0ccd:e799::/124"
+	CommunityNoAdv        = "65535:65282" // 0xFFFFFF02: NO_ADVERTISE
+	CommunityGracefulShut = "65535:0"     // GRACEFUL_SHUTDOWN
+	IPLocalPref           = uint32(300)
+	SpeakerContainerName  = "speaker"
 )
 
 var ConfigUpdater config.Updater
@@ -72,27 +73,7 @@ var _ = ginkgo.Describe("BGP", func() {
 
 	ginkgo.AfterEach(func() {
 		if ginkgo.CurrentGinkgoTestDescription().Failed {
-			for _, c := range FRRContainers {
-				address := c.Ipv4
-				if address == "" {
-					address = c.Ipv6
-				}
-				peerAddr := address + fmt.Sprintf(":%d", c.RouterConfig.BGPPort)
-				dump, err := frr.RawDump(c, "/etc/frr/bgpd.conf", "/tmp/frr.log", "/etc/frr/daemons")
-				framework.Logf("External frr dump for %s:%s\n%s\nerrors:%v", c.Name, peerAddr, dump, err)
-			}
-
-			speakerPods, err := metallb.SpeakerPods(cs)
-			framework.ExpectNoError(err)
-			for _, pod := range speakerPods {
-				if len(pod.Spec.Containers) == 1 { // we dump only in case of frr
-					break
-				}
-				podExec := executor.ForPod(pod.Namespace, pod.Name, "frr")
-				dump, err := frr.RawDump(podExec, "/etc/frr/frr.conf", "/etc/frr/frr.log")
-				framework.Logf("External frr dump for pod %s\n%s %v", pod.Name, dump, err)
-			}
-			k8s.DescribeSvc(f.Namespace.Name)
+			dumpBGPInfo(cs, f)
 		}
 	})
 
@@ -139,7 +120,7 @@ var _ = ginkgo.Describe("BGP", func() {
 		framework.ExpectNoError(err)
 
 		for _, c := range FRRContainers {
-			validateFRRPeeredWithNodes(cs, c, pairingIPFamily)
+			validateFRRPeeredWithAllNodes(cs, c, pairingIPFamily)
 		}
 
 		allNodes, err = cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
@@ -261,7 +242,7 @@ var _ = ginkgo.Describe("BGP", func() {
 			framework.ExpectNoError(err)
 
 			for _, c := range FRRContainers {
-				validateFRRPeeredWithNodes(cs, c, ipFamily)
+				validateFRRPeeredWithAllNodes(cs, c, ipFamily)
 			}
 
 			ginkgo.By("checking the metrics when no service is added")
@@ -381,7 +362,7 @@ var _ = ginkgo.Describe("BGP", func() {
 			framework.ExpectNoError(err)
 
 			for _, c := range FRRContainers {
-				validateFRRPeeredWithNodes(cs, c, pairingFamily)
+				validateFRRPeeredWithAllNodes(cs, c, pairingFamily)
 			}
 
 			svc, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "external-local-lb", tweak)
@@ -481,7 +462,7 @@ var _ = ginkgo.Describe("BGP", func() {
 		}
 
 		for _, c := range FRRContainers {
-			validateFRRPeeredWithNodes(cs, c, ipFamily)
+			validateFRRPeeredWithAllNodes(cs, c, ipFamily)
 			neighbors, err := frr.NeighborsInfo(c)
 			framework.ExpectNoError(err)
 			for _, n := range neighbors {
@@ -517,7 +498,7 @@ var _ = ginkgo.Describe("BGP", func() {
 		}
 
 		for _, c := range FRRContainers {
-			validateFRRPeeredWithNodes(cs, c, ipFamily)
+			validateFRRPeeredWithAllNodes(cs, c, ipFamily)
 		}
 	},
 		table.Entry("IPV4 with Secret Ref set for BGPPeer CR", ipfamily.IPv4),
@@ -557,7 +538,7 @@ var _ = ginkgo.Describe("BGP", func() {
 			framework.ExpectNoError(err)
 
 			for _, c := range FRRContainers {
-				validateFRRPeeredWithNodes(cs, c, pairingFamily)
+				validateFRRPeeredWithAllNodes(cs, c, pairingFamily)
 			}
 			for _, c := range FRRContainers {
 				validateService(cs, svc, allNodes.Items, c)
@@ -701,7 +682,7 @@ var _ = ginkgo.Describe("BGP", func() {
 			}
 
 			for _, c := range FRRContainers {
-				validateFRRPeeredWithNodes(cs, c, pairingFamily)
+				validateFRRPeeredWithAllNodes(cs, c, pairingFamily)
 			}
 
 			ginkgo.By("checking metrics")
@@ -922,7 +903,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				framework.ExpectNoError(err)
 
 				for _, c := range FRRContainers {
-					validateFRRPeeredWithNodes(cs, c, ipFamily)
+					validateFRRPeeredWithAllNodes(cs, c, ipFamily)
 				}
 
 				ginkgo.By(fmt.Sprintf("configure service number %d", i+1))
@@ -969,7 +950,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				err = frrcontainer.PairWithNodes(cs, c, ipFamily)
 				framework.ExpectNoError(err)
 
-				validateFRRPeeredWithNodes(cs, FRRContainers[i], ipFamily)
+				validateFRRPeeredWithAllNodes(cs, FRRContainers[i], ipFamily)
 			}
 		},
 			table.Entry("IPV4", ipfamily.IPv4),
@@ -1027,7 +1008,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				framework.ExpectNoError(err)
 
 				for _, c := range FRRContainers {
-					validateFRRPeeredWithNodes(cs, c, ipFamily)
+					validateFRRPeeredWithAllNodes(cs, c, ipFamily)
 				}
 
 				ipWithAdvertisement, err := config.GetIPFromRangeByIndex(rangeWithAdvertisement, 0)
@@ -1242,7 +1223,7 @@ var _ = ginkgo.Describe("BGP", func() {
 		framework.ExpectNoError(err)
 
 		for _, c := range FRRContainers {
-			validateFRRPeeredWithNodes(cs, c, pairingIPFamily)
+			validateFRRPeeredWithAllNodes(cs, c, pairingIPFamily)
 		}
 		speakerPods, err := metallb.SpeakerPods(cs)
 		framework.ExpectNoError(err)
diff --git a/e2etest/bgptests/dump.go b/e2etest/bgptests/dump.go
new file mode 100644
index 00000000..da73b7e0
--- /dev/null
+++ b/e2etest/bgptests/dump.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package bgptests
+
+import (
+	"fmt"
+
+	"go.universe.tf/metallb/e2etest/pkg/executor"
+	"go.universe.tf/metallb/e2etest/pkg/frr"
+	"go.universe.tf/metallb/e2etest/pkg/k8s"
+	"go.universe.tf/metallb/e2etest/pkg/metallb"
+	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/kubernetes/test/e2e/framework"
+)
+
+func dumpBGPInfo(cs clientset.Interface, f *framework.Framework) {
+	for _, c := range FRRContainers {
+		address := c.Ipv4
+		if address == "" {
+			address = c.Ipv6
+		}
+		peerAddr := address + fmt.Sprintf(":%d", c.RouterConfig.BGPPort)
+		dump, err := frr.RawDump(c, "/etc/frr/bgpd.conf", "/tmp/frr.log", "/etc/frr/daemons")
+		framework.Logf("External frr dump for %s:%s\n%s\nerrors:%v", c.Name, peerAddr, dump, err)
+	}
+
+	speakerPods, err := metallb.SpeakerPods(cs)
+	framework.ExpectNoError(err)
+	for _, pod := range speakerPods {
+		if len(pod.Spec.Containers) == 1 { // we dump only in case of frr
+			break
+		}
+		podExec := executor.ForPod(pod.Namespace, pod.Name, "frr")
+		dump, err := frr.RawDump(podExec, "/etc/frr/frr.conf", "/etc/frr/frr.log")
+		framework.Logf("External frr dump for pod %s\n%s %v", pod.Name, dump, err)
+	}
+	k8s.DescribeSvc(f.Namespace.Name)
+}
diff --git a/e2etest/bgptests/node_selector.go b/e2etest/bgptests/node_selector.go
new file mode 100644
index 00000000..639696ea
--- /dev/null
+++ b/e2etest/bgptests/node_selector.go
@@ -0,0 +1,259 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package bgptests
+
+import (
+	"context"
+
+	"fmt"
+	"strings"
+
+	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
+	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
+	"go.universe.tf/metallb/e2etest/pkg/k8s"
+	"go.universe.tf/metallb/e2etest/pkg/metallb"
+	testservice "go.universe.tf/metallb/e2etest/pkg/service"
+	metallbconfig "go.universe.tf/metallb/internal/config"
+	"go.universe.tf/metallb/internal/ipfamily"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/onsi/ginkgo"
+	"github.com/onsi/ginkgo/extensions/table"
+
+	frrconfig "go.universe.tf/metallb/e2etest/pkg/frr/config"
+	frrcontainer "go.universe.tf/metallb/e2etest/pkg/frr/container"
+	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
+	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/kubernetes/test/e2e/framework"
+)
+
+var _ = ginkgo.Describe("BGP Node Selector", func() {
+	var cs clientset.Interface
+	var f *framework.Framework
+
+	ginkgo.AfterEach(func() {
+		if ginkgo.CurrentGinkgoTestDescription().Failed {
+			dumpBGPInfo(cs, f)
+		}
+	})
+
+	ginkgo.BeforeEach(func() {
+		ginkgo.By("Clearing any previous configuration")
+
+		err := ConfigUpdater.Clean()
+		framework.ExpectNoError(err)
+
+		for _, c := range FRRContainers {
+			err := c.UpdateBGPConfigFile(frrconfig.Empty)
+			framework.ExpectNoError(err)
+		}
+	})
+
+	f = framework.NewDefaultFramework("bgp")
+
+	ginkgo.BeforeEach(func() {
+		cs = f.ClientSet
+	})
+
+	table.DescribeTable("Two services, two distinct advertisements with different node selectors",
+		func(pairingIPFamily ipfamily.Family, addresses []string, nodesForFirstPool, nodesForSecondPool []int) {
+			var allNodes *corev1.NodeList
+			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+			framework.ExpectNoError(err)
+
+			expectedNodesForFirstPool := nodesForSelection(allNodes.Items, nodesForFirstPool)
+			expectedNodesForSecondPool := nodesForSelection(allNodes.Items, nodesForSecondPool)
+
+			resources := metallbconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "first-pool",
+						},
+						Spec: metallbv1beta1.IPAddressPoolSpec{
+							Addresses: []string{addresses[0]},
+						},
+					}, {
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "second-pool",
+						},
+						Spec: metallbv1beta1.IPAddressPoolSpec{
+							Addresses: []string{addresses[1]},
+						},
+					},
+				},
+				Peers: metallb.PeersForContainers(FRRContainers, pairingIPFamily),
+				BGPAdvs: []metallbv1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "first-adv",
+						},
+						Spec: metallbv1beta1.BGPAdvertisementSpec{
+							NodeSelectors:  k8s.SelectorsForNodes(expectedNodesForFirstPool),
+							IPAddressPools: []string{"first-pool"},
+						},
+					}, {
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "second-adv",
+						},
+						Spec: metallbv1beta1.BGPAdvertisementSpec{
+							NodeSelectors:  k8s.SelectorsForNodes(expectedNodesForSecondPool),
+							IPAddressPools: []string{"second-pool"},
+						},
+					},
+				},
+			}
+			for _, c := range FRRContainers {
+				err := frrcontainer.PairWithNodes(cs, c, pairingIPFamily)
+				framework.ExpectNoError(err)
+			}
+
+			err = ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+
+			firstService, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "first-lb", testservice.WithSpecificPool("first-pool"))
+			defer testservice.Delete(cs, firstService)
+			secondService, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "second-lb", testservice.WithSpecificPool("second-pool"))
+			defer testservice.Delete(cs, secondService)
+
+			checkServiceOnlyOnNodes(cs, firstService, expectedNodesForFirstPool, pairingIPFamily)
+			checkServiceOnlyOnNodes(cs, secondService, expectedNodesForSecondPool, pairingIPFamily)
+		},
+		table.Entry("IPV4 - two on first, two on second", ipfamily.IPv4, []string{"192.168.10.0/24", "192.168.16.0/24"}, []int{0, 1}, []int{0, 1}),
+		table.Entry("IPV4 - one on first, two on second", ipfamily.IPv4, []string{"192.168.10.0/24", "192.168.16.0/24"}, []int{0}, []int{0, 1}),
+		table.Entry("IPV4 - zero on first, two on second", ipfamily.IPv4, []string{"192.168.10.0/24", "192.168.16.0/24"}, []int{}, []int{0, 1}),
+		table.Entry("IPV6 - one on first, two on second", ipfamily.IPv6, []string{"fc00:f853:0ccd:e799::/116", "fc00:f853:0ccd:e800::/116"}, []int{0}, []int{1, 2}),
+	)
+
+	// this test is marked FFR only because of https://github.com/metallb/metallb/issues/1315
+	table.DescribeTable("Single service, two advertisement with different node selectors FRR", func(pairingIPFamily ipfamily.Family, address string, nodesForFirstAdv, nodesForSecondAdv []int) {
+		var allNodes *corev1.NodeList
+		allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+		framework.ExpectNoError(err)
+
+		expectedNodesForFirstAdv := nodesForSelection(allNodes.Items, nodesForFirstAdv)
+		expectedNodesForSecondAdv := nodesForSelection(allNodes.Items, nodesForSecondAdv)
+
+		resources := metallbconfig.ClusterResources{
+			Pools: []metallbv1beta1.IPAddressPool{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "first-pool",
+					},
+					Spec: metallbv1beta1.IPAddressPoolSpec{
+						Addresses: []string{address},
+					},
+				},
+			},
+			Peers: metallb.PeersForContainers(FRRContainers, pairingIPFamily),
+			BGPAdvs: []metallbv1beta1.BGPAdvertisement{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "first-adv",
+					},
+					Spec: metallbv1beta1.BGPAdvertisementSpec{
+						NodeSelectors:  k8s.SelectorsForNodes(expectedNodesForFirstAdv),
+						Communities:    []string{CommunityNoAdv},
+						IPAddressPools: []string{"first-pool"},
+					},
+				}, {
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "second-adv",
+					},
+					Spec: metallbv1beta1.BGPAdvertisementSpec{
+						NodeSelectors:  k8s.SelectorsForNodes(expectedNodesForSecondAdv),
+						Communities:    []string{CommunityGracefulShut},
+						IPAddressPools: []string{"first-pool"},
+					},
+				},
+			},
+		}
+		for _, c := range FRRContainers {
+			err := frrcontainer.PairWithNodes(cs, c, pairingIPFamily)
+			framework.ExpectNoError(err)
+		}
+
+		err = ConfigUpdater.Update(resources)
+		framework.ExpectNoError(err)
+
+		svc, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "first-lb", testservice.TrafficPolicyCluster)
+		defer testservice.Delete(cs, svc)
+
+		checkCommunitiesOnlyOnNodes(cs, svc, CommunityNoAdv, expectedNodesForFirstAdv, pairingIPFamily)
+		checkCommunitiesOnlyOnNodes(cs, svc, CommunityGracefulShut, expectedNodesForSecondAdv, pairingIPFamily)
+	},
+		table.Entry("IPV4 - two on first, two on second", ipfamily.IPv4, "192.168.10.0/24", []int{0, 1}, []int{0, 1}),
+		table.Entry("IPV4 - one on first, two on second", ipfamily.IPv4, "192.168.10.0/24", []int{0}, []int{0, 1}),
+		table.Entry("IPV4 - zero on first, two on second", ipfamily.IPv4, "192.168.10.0/24", []int{}, []int{0, 1}),
+		table.Entry("IPV6 - one on first, two on second", ipfamily.IPv6, "fc00:f853:0ccd:e799::/116", []int{0}, []int{1, 2}),
+	)
+
+	ginkgo.Context("Peer selector", func() {
+		// nodeForPeers is a map between container index and the indexes of the nodes we want to peer it with.
+		// we cant use strings to avoid making assumptions on the names of the containers / nodes.
+		table.DescribeTable("IPV4 Should work with a limited set of nodes", func(nodesForPeers map[int][]int) {
+			var allNodes *corev1.NodeList
+			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+			framework.ExpectNoError(err)
+
+			resources := metallbconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "pool",
+						},
+						Spec: metallbv1beta1.IPAddressPoolSpec{
+							Addresses: []string{"192.168.10.0/24"},
+						},
+					},
+				},
+				Peers: metallb.PeersForContainers(FRRContainers, ipfamily.IPv4,
+					func(p *metallbv1beta2.BGPPeer) {
+						for containerIndx, nodesIndexes := range nodesForPeers {
+							if containerIndx >= len(FRRContainers) {
+								ginkgo.Skip(fmt.Sprintf("Asking for container %d, not enough containers %d", containerIndx, len(FRRContainers)))
+							}
+							if !strings.Contains(p.Name, FRRContainers[containerIndx].Name) {
+								continue
+							}
+							nodes := nodesForSelection(allNodes.Items, nodesIndexes)
+							p.Spec.NodeSelectors = k8s.SelectorsForNodes(nodes)
+							return
+						}
+						p.Spec.NodeSelectors = k8s.SelectorsForNodes([]v1.Node{})
+					}),
+			}
+			err = ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+
+			for _, c := range FRRContainers {
+				err := frrcontainer.PairWithNodes(cs, c, ipfamily.IPv4)
+				framework.ExpectNoError(err)
+			}
+
+			for i, c := range FRRContainers {
+				selected := nodesForSelection(allNodes.Items, nodesForPeers[i])
+				nonSelected := nodesNotSelected(allNodes.Items, nodesForPeers[i])
+				validateFRRPeeredWithNodes(cs, selected, c, ipfamily.IPv4)
+				validateFRRNotPeeredWithNodes(cs, nonSelected, c, ipfamily.IPv4)
+			}
+
+		},
+			table.Entry("First to one, second to two", map[int][]int{
+				0: []int{0},
+				1: []int{1, 2},
+			}),
+			table.Entry("First to one, second to one, third to three", map[int][]int{
+				0: []int{0},
+				1: []int{1},
+				2: []int{0, 1, 2},
+			}),
+			table.Entry("First to one, second to one, third to same as first", map[int][]int{
+				0: []int{0},
+				1: []int{1},
+				2: []int{0},
+			}),
+		)
+	})
+})
diff --git a/e2etest/bgptests/validate.go b/e2etest/bgptests/validate.go
index 76ee9dc6..06a5c23a 100644
--- a/e2etest/bgptests/validate.go
+++ b/e2etest/bgptests/validate.go
@@ -16,6 +16,7 @@ import (
 	"go.universe.tf/metallb/e2etest/pkg/executor"
 	"go.universe.tf/metallb/e2etest/pkg/frr"
 	frrcontainer "go.universe.tf/metallb/e2etest/pkg/frr/container"
+	"go.universe.tf/metallb/e2etest/pkg/k8s"
 	"go.universe.tf/metallb/e2etest/pkg/metallb"
 	"go.universe.tf/metallb/e2etest/pkg/routes"
 	"go.universe.tf/metallb/e2etest/pkg/wget"
@@ -28,20 +29,41 @@ import (
 	e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
 )
 
-func validateFRRPeeredWithNodes(cs clientset.Interface, c *frrcontainer.FRR, ipFamily ipfamily.Family) {
+func validateFRRPeeredWithAllNodes(cs clientset.Interface, c *frrcontainer.FRR, ipFamily ipfamily.Family) {
 	allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 	framework.ExpectNoError(err)
+	validateFRRPeeredWithNodes(cs, allNodes.Items, c, ipFamily)
+}
+
+func validateFRRNotPeeredWithNodes(cs clientset.Interface, nodes []corev1.Node, c *frrcontainer.FRR, ipFamily ipfamily.Family) {
+	for _, node := range nodes {
+		ginkgo.By(fmt.Sprintf("checking node %s is not peered with the frr instance %s", node.Name, c.Name))
+		Eventually(func() error {
+			neighbors, err := frr.NeighborsInfo(c)
+			framework.ExpectNoError(err)
+			err = frr.NeighborsMatchNodes([]corev1.Node{node}, neighbors, ipFamily)
+			return err
+		}, 4*time.Minute, 1*time.Second).Should(MatchError(ContainSubstring("not established")))
+	}
+}
 
-	ginkgo.By(fmt.Sprintf("checking all nodes are peered with the frr instance %s", c.Name))
+func validateFRRPeeredWithNodes(cs clientset.Interface, nodes []corev1.Node, c *frrcontainer.FRR, ipFamily ipfamily.Family) {
+	ginkgo.By(fmt.Sprintf("checking nodes are peered with the frr instance %s", c.Name))
 	Eventually(func() error {
 		neighbors, err := frr.NeighborsInfo(c)
 		framework.ExpectNoError(err)
-		err = frr.NeighborsMatchNodes(allNodes.Items, neighbors, ipFamily)
+		err = frr.NeighborsMatchNodes(nodes, neighbors, ipFamily)
 		return err
 	}, 4*time.Minute, 1*time.Second).Should(BeNil())
 }
 
 func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1.Node, c *frrcontainer.FRR) {
+	Eventually(func() error {
+		return validateServiceNoWait(cs, svc, nodes, c)
+	}, 4*time.Minute, 1*time.Second).Should(BeNil())
+}
+
+func validateServiceNoWait(cs clientset.Interface, svc *corev1.Service, nodes []corev1.Node, c *frrcontainer.FRR) error {
 	port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
 
 	if len(svc.Status.LoadBalancer.Ingress) == 2 {
@@ -55,46 +77,43 @@ func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1
 		hostport := net.JoinHostPort(ingressIP, port)
 		address := fmt.Sprintf("http://%s/", hostport)
 
-		Eventually(func() error {
-			err := wget.Do(address, c)
-			if err != nil {
-				return err
-			}
+		err := wget.Do(address, c)
+		if err != nil {
+			return err
+		}
 
-			frrRoutesV4, frrRoutesV6, err := frr.Routes(c)
-			if err != nil {
-				return err
-			}
-			serviceIPFamily := ipfamily.IPv4
-			frrRoutes, ok := frrRoutesV4[ingressIP]
-			if !ok {
-				frrRoutes, ok = frrRoutesV6[ingressIP]
-				serviceIPFamily = ipfamily.IPv6
-			}
-			if !ok {
-				return fmt.Errorf("%s not found in frr routes %v %v", ingressIP, frrRoutesV4, frrRoutesV6)
-			}
-			if !strings.EqualFold(frrRoutes.Origin, "IGP") {
-				return fmt.Errorf("route for %s not set with igp origin", ingressIP)
-			}
+		frrRoutesV4, frrRoutesV6, err := frr.Routes(c)
+		if err != nil {
+			return err
+		}
+		serviceIPFamily := ipfamily.IPv4
+		frrRoutes, ok := frrRoutesV4[ingressIP]
+		if !ok {
+			frrRoutes, ok = frrRoutesV6[ingressIP]
+			serviceIPFamily = ipfamily.IPv6
+		}
+		if !ok {
+			return fmt.Errorf("%s not found in frr routes %v %v", ingressIP, frrRoutesV4, frrRoutesV6)
+		}
+		if !strings.EqualFold(frrRoutes.Origin, "IGP") {
+			return fmt.Errorf("route for %s not set with igp origin", ingressIP)
+		}
 
-			err = frr.RoutesMatchNodes(nodes, frrRoutes, serviceIPFamily)
+		err = frr.RoutesMatchNodes(nodes, frrRoutes, serviceIPFamily)
+		if err != nil {
+			return err
+		}
+
+		// The BGP routes will not match the nodes if static routes were added.
+		if !(c.Network == multiHopNetwork) {
+			advertised := routes.ForIP(ingressIP, c)
+			err = routes.MatchNodes(nodes, advertised, serviceIPFamily)
 			if err != nil {
 				return err
 			}
-
-			// The BGP routes will not match the nodes if static routes were added.
-			if !(c.Network == multiHopNetwork) {
-				advertised := routes.ForIP(ingressIP, c)
-				err = routes.MatchNodes(nodes, advertised, serviceIPFamily)
-				if err != nil {
-					return err
-				}
-			}
-
-			return nil
-		}, 4*time.Minute, 1*time.Second).Should(BeNil())
+		}
 	}
+	return nil
 }
 
 func frrIsPairedOnPods(cs clientset.Interface, n *frrcontainer.FRR, ipFamily ipfamily.Family) {
@@ -145,6 +164,89 @@ func validateDesiredLB(svc *corev1.Service) {
 	framework.ExpectEqual(desiredLbIPs, strings.Join(getIngressIPs(svc.Status.LoadBalancer.Ingress), ","))
 }
 
+func checkServiceOnlyOnNodes(cs clientset.Interface, svc *corev1.Service, expectedNodes []corev1.Node, ipFamily ipfamily.Family) {
+	if len(expectedNodes) == 0 {
+		return
+	}
+	ip := svc.Status.LoadBalancer.Ingress[0].IP
+	nodeIps := k8s.NodeIPsForFamily(expectedNodes, ipFamily)
+
+	for _, c := range FRRContainers {
+		validateService(cs, svc, expectedNodes, c)
+		Eventually(func() error {
+			routes, err := frr.RoutesForFamily(c, ipFamily)
+			if len(routes[ip].NextHops) != len(nodeIps) {
+				return fmt.Errorf("%s: invalid number of routes for %s: expecting %d got %d", c.Name, ip, len(nodeIps), len(routes[ip].NextHops))
+			}
+
+		OUTER:
+			for _, n := range routes[ip].NextHops {
+				for _, ip := range nodeIps {
+					if n.String() == ip {
+						continue OUTER
+					}
+				}
+				return fmt.Errorf("UnexpectedIP found %s, nodes %s in container %s for service %s", n.String(), nodeIps, c.Name, ip)
+			}
+			return err
+		}, time.Minute, time.Second).Should(Not(HaveOccurred()))
+	}
+}
+
+func checkCommunitiesOnlyOnNodes(cs clientset.Interface, svc *corev1.Service, community string, expectedNodes []corev1.Node, ipFamily ipfamily.Family) {
+	if len(expectedNodes) == 0 {
+		return
+	}
+	ip := svc.Status.LoadBalancer.Ingress[0].IP
+	nodeIps := k8s.NodeIPsForFamily(expectedNodes, ipFamily)
+
+	for _, c := range FRRContainers {
+		Eventually(func() error {
+			routes, err := frr.RoutesForCommunity(c, community, ipFamily)
+			if len(routes[ip].NextHops) != len(nodeIps) {
+				return fmt.Errorf("%s: invalid number of routes for %s: expecting %d got %d", c.Name, ip, len(nodeIps), len(routes[ip].NextHops))
+			}
+
+		OUTER:
+			for _, n := range routes[ip].NextHops {
+				for _, ip := range nodeIps {
+					if n.String() == ip {
+						continue OUTER
+					}
+				}
+				return fmt.Errorf("UnexpectedIP found %s, nodes %s in container %s for service %s", n.String(), nodeIps, c.Name, ip)
+			}
+			return err
+		}, 10*time.Minute, time.Second).Should(Not(HaveOccurred()))
+	}
+}
+
+func nodesForSelection(nodes []corev1.Node, selected []int) []corev1.Node {
+	selectedNodes := []corev1.Node{}
+	for _, i := range selected {
+		if i >= len(nodes) {
+			ginkgo.Skip("Not enough nodes")
+		}
+		selectedNodes = append(selectedNodes, nodes[i])
+	}
+	return selectedNodes
+}
+
+func nodesNotSelected(nodes []corev1.Node, selected []int) []corev1.Node {
+	nonSelectedNodes := []corev1.Node{}
+OUTER:
+	for i, n := range nodes {
+		for _, j := range selected {
+			if i == j {
+				continue OUTER
+			}
+		}
+		nonSelectedNodes = append(nonSelectedNodes, n)
+	}
+
+	return nonSelectedNodes
+}
+
 func getIngressIPs(ingresses []corev1.LoadBalancerIngress) []string {
 	var ips []string
 	for _, ingress := range ingresses {
diff --git a/e2etest/l2tests/node_selector.go b/e2etest/l2tests/node_selector.go
new file mode 100644
index 00000000..0cd33b8c
--- /dev/null
+++ b/e2etest/l2tests/node_selector.go
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package l2tests
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"strconv"
+	"time"
+
+	"github.com/onsi/ginkgo"
+	"github.com/onsi/gomega"
+	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
+	"go.universe.tf/metallb/e2etest/pkg/executor"
+	"go.universe.tf/metallb/e2etest/pkg/k8s"
+	"go.universe.tf/metallb/e2etest/pkg/mac"
+	"go.universe.tf/metallb/e2etest/pkg/service"
+	"go.universe.tf/metallb/e2etest/pkg/wget"
+
+	internalconfig "go.universe.tf/metallb/internal/config"
+	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/kubernetes/test/e2e/framework"
+	e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
+)
+
+var _ = ginkgo.Describe("L2", func() {
+	f := framework.NewDefaultFramework("l2")
+	var cs clientset.Interface
+
+	ginkgo.BeforeEach(func() {
+		cs = f.ClientSet
+
+		ginkgo.By("Clearing any previous configuration")
+
+		err := ConfigUpdater.Clean()
+		framework.ExpectNoError(err)
+	})
+
+	ginkgo.AfterEach(func() {
+		// Clean previous configuration.
+		err := ConfigUpdater.Clean()
+		framework.ExpectNoError(err)
+
+		if ginkgo.CurrentGinkgoTestDescription().Failed {
+			k8s.DescribeSvc(f.Namespace.Name)
+		}
+	})
+
+	ginkgo.Context("Node Selector", func() {
+		ginkgo.BeforeEach(func() {
+			resources := internalconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "l2-test",
+						},
+						Spec: metallbv1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								IPV4ServiceRange,
+								IPV6ServiceRange},
+						},
+					},
+				},
+			}
+
+			err := ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+		})
+
+		ginkgo.It("should work selecting one node", func() {
+			svc, _ := service.CreateWithBackend(cs, f.Namespace.Name, "external-local-lb", service.TrafficPolicyCluster)
+			defer func() {
+				err := cs.CoreV1().Services(svc.Namespace).Delete(context.TODO(), svc.Name, metav1.DeleteOptions{})
+				framework.ExpectNoError(err)
+			}()
+
+			ingressIP := e2eservice.GetIngressPoint(
+				&svc.Status.LoadBalancer.Ingress[0])
+
+			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+			framework.ExpectNoError(err)
+			for _, node := range allNodes.Items {
+				l2Advertisement := metallbv1beta1.L2Advertisement{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "with-selector",
+					},
+					Spec: metallbv1beta1.L2AdvertisementSpec{
+						NodeSelectors: k8s.SelectorsForNodes([]v1.Node{node}),
+					},
+				}
+
+				ginkgo.By(fmt.Sprintf("Assigning the advertisement to node %s", node.Name))
+				resources := internalconfig.ClusterResources{
+					L2Advs: []metallbv1beta1.L2Advertisement{l2Advertisement},
+				}
+
+				err := ConfigUpdater.Update(resources)
+				framework.ExpectNoError(err)
+				gomega.Eventually(func() error {
+					return mac.RequestAddressResolution(ingressIP, executor.Host)
+				}, 30*time.Second, 1*time.Second).Should(gomega.Not(gomega.HaveOccurred()))
+
+				port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
+				hostport := net.JoinHostPort(ingressIP, port)
+				address := fmt.Sprintf("http://%s/", hostport)
+
+				gomega.Eventually(func() string {
+					err = wget.Do(address, executor.Host)
+					framework.ExpectNoError(err)
+					advNode, err := advertisingNodeFromMAC(allNodes.Items, ingressIP, executor.Host)
+					if err != nil {
+						return err.Error()
+					}
+					return advNode.Name
+				}, 30*time.Second, 1*time.Second).Should(gomega.Equal(node.Name))
+			}
+		})
+
+		ginkgo.It("should with multiple node selectors", func() {
+			// ETP = local, pin the endpoint to node0, have two l2 advertisements, one for
+			// all and one for node1, check node0 is advertised.
+			jig := e2eservice.NewTestJig(cs, f.Namespace.Name, "svca")
+			loadBalancerCreateTimeout := e2eservice.GetServiceLoadBalancerCreationTimeout(cs)
+			svc, err := jig.CreateLoadBalancerService(loadBalancerCreateTimeout, func(svc *corev1.Service) {
+				svc.Spec.Ports[0].TargetPort = intstr.FromInt(service.TestServicePort)
+				svc.Spec.Ports[0].Port = int32(service.TestServicePort)
+				svc.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal
+			})
+			framework.ExpectNoError(err)
+
+			defer func() {
+				err := cs.CoreV1().Services(svc.Namespace).Delete(context.TODO(), svc.Name, metav1.DeleteOptions{})
+				framework.ExpectNoError(err)
+			}()
+
+			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+			framework.ExpectNoError(err)
+			if len(allNodes.Items) < 2 {
+				ginkgo.Skip("Not enough nodes")
+			}
+			_, err = jig.Run(
+				func(rc *corev1.ReplicationController) {
+					rc.Spec.Template.Spec.Containers[0].Args = []string{"netexec", fmt.Sprintf("--http-port=%d", service.TestServicePort), fmt.Sprintf("--udp-port=%d", service.TestServicePort)}
+					rc.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Port = intstr.FromInt(service.TestServicePort)
+					rc.Spec.Template.Spec.NodeName = allNodes.Items[0].Name
+				})
+			framework.ExpectNoError(err)
+
+			ingressIP := e2eservice.GetIngressPoint(
+				&svc.Status.LoadBalancer.Ingress[0])
+
+			l2Advertisements := []metallbv1beta1.L2Advertisement{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "with-selector",
+					},
+					Spec: metallbv1beta1.L2AdvertisementSpec{
+						NodeSelectors: k8s.SelectorsForNodes([]v1.Node{allNodes.Items[1]}),
+					},
+				}, {
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "no-selector",
+					},
+				},
+			}
+
+			ginkgo.By("Creating the l2 advertisements")
+			resources := internalconfig.ClusterResources{
+				L2Advs: l2Advertisements,
+			}
+
+			err = ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+			gomega.Eventually(func() error {
+				return mac.RequestAddressResolution(ingressIP, executor.Host)
+			}, 2*time.Minute, 1*time.Second).Should(gomega.Not(gomega.HaveOccurred()))
+
+			ginkgo.By("checking connectivity to its external VIP")
+			port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
+			hostport := net.JoinHostPort(ingressIP, port)
+			address := fmt.Sprintf("http://%s/", hostport)
+
+			gomega.Eventually(func() string {
+				err = wget.Do(address, executor.Host)
+				framework.ExpectNoError(err)
+
+				advNode, err := advertisingNodeFromMAC(allNodes.Items, ingressIP, executor.Host)
+				if err != nil {
+					return err.Error()
+				}
+				return advNode.Name
+			}, 2*time.Minute, time.Second).Should(gomega.Equal(allNodes.Items[0].Name))
+		})
+	})
+})
diff --git a/e2etest/pkg/frr/bgp.go b/e2etest/pkg/frr/bgp.go
index 5703ab68..14915f56 100644
--- a/e2etest/pkg/frr/bgp.go
+++ b/e2etest/pkg/frr/bgp.go
@@ -68,6 +68,20 @@ func Routes(exec executor.Executor) (map[string]bgpfrr.Route, map[string]bgpfrr.
 	return v4Routes, v6Routes, nil
 }
 
+func RoutesForFamily(exec executor.Executor, family ipfamily.Family) (map[string]bgpfrr.Route, error) {
+	v4, v6, err := Routes(exec)
+	if err != nil {
+		return nil, err
+	}
+	switch family {
+	case ipfamily.IPv4:
+		return v4, nil
+	case ipfamily.IPv6:
+		return v6, nil
+	}
+	return nil, fmt.Errorf("unsupported ipfamily %v", family)
+}
+
 // RoutesForCommunity returns informations about routes in the given executor related to the given community.
 func RoutesForCommunity(exec executor.Executor, community string, family ipfamily.Family) (map[string]bgpfrr.Route, error) {
 	res, err := exec.Exec("vtysh", "-c", fmt.Sprintf("show bgp %s community %s json", family, community))
diff --git a/e2etest/pkg/k8s/nodes.go b/e2etest/pkg/k8s/nodes.go
index d28c1a1b..96095488 100644
--- a/e2etest/pkg/k8s/nodes.go
+++ b/e2etest/pkg/k8s/nodes.go
@@ -7,6 +7,7 @@ import (
 
 	"go.universe.tf/metallb/internal/ipfamily"
 	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func NodeIPsForFamily(nodes []v1.Node, family ipfamily.Family) []string {
@@ -23,3 +24,24 @@ func NodeIPsForFamily(nodes []v1.Node, family ipfamily.Family) []string {
 	}
 	return res
 }
+
+func SelectorsForNodes(nodes []v1.Node) []metav1.LabelSelector {
+	selectors := []metav1.LabelSelector{}
+	if len(nodes) == 0 {
+		return []metav1.LabelSelector{
+			{
+				MatchLabels: map[string]string{
+					"non": "existent",
+				},
+			},
+		}
+	}
+	for _, node := range nodes {
+		selectors = append(selectors, metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				"kubernetes.io/hostname": node.GetLabels()["kubernetes.io/hostname"],
+			},
+		})
+	}
+	return selectors
+}
diff --git a/e2etest/pkg/service/tweak.go b/e2etest/pkg/service/tweak.go
index c2be6a26..3505b4aa 100644
--- a/e2etest/pkg/service/tweak.go
+++ b/e2etest/pkg/service/tweak.go
@@ -38,3 +38,11 @@ func WithSpecificIPs(svc *corev1.Service, ips ...string) {
 		"metallb.universe.tf/loadBalancerIPs": strings.Join(ips, ","),
 	}
 }
+
+func WithSpecificPool(poolName string) func(*corev1.Service) {
+	return func(svc *corev1.Service) {
+		svc.Annotations = map[string]string{
+			"metallb.universe.tf/address-pool": poolName,
+		}
+	}
+}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index fa3a1307..e29ce481 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -12,6 +12,7 @@ import (
 	"go.universe.tf/metallb/api/v1beta2"
 	"go.universe.tf/metallb/internal/pointer"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 )
@@ -222,14 +223,18 @@ func TestParse(t *testing.T) {
 									//0xfc0004d2: true,
 									0x04D20929: true,
 								},
+								Nodes: map[string]bool{},
 							},
 							{
 								AggregationLength:   24,
 								AggregationLengthV6: 64,
 								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{},
 							},
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{}},
+						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+							Nodes: map[string]bool{},
+						}},
 					},
 					"pool2": {
 						CIDR:       []*net.IPNet{ipnet("30.0.0.0/8")},
@@ -239,9 +244,12 @@ func TestParse(t *testing.T) {
 								AggregationLength:   32,
 								AggregationLengthV6: 128,
 								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{},
 							},
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{}},
+						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+							Nodes: map[string]bool{},
+						}},
 					},
 					"pool3": {
 						CIDR: []*net.IPNet{
@@ -258,13 +266,17 @@ func TestParse(t *testing.T) {
 							ipnet("40.0.0.240/32"),
 							ipnet("40.0.0.250/32"),
 						},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{}},
-						AutoAssign:       true,
+						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+							Nodes: map[string]bool{},
+						}},
+						AutoAssign: true,
 					},
 					"pool4": {
-						CIDR:             []*net.IPNet{ipnet("2001:db8::/64")},
-						L2Advertisements: []*L2Advertisement{&L2Advertisement{}},
-						AutoAssign:       true,
+						CIDR: []*net.IPNet{ipnet("2001:db8::/64")},
+						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+							Nodes: map[string]bool{},
+						}},
+						AutoAssign: true,
 					},
 				},
 				BFDProfiles: map[string]*BFDProfile{},
@@ -623,6 +635,7 @@ func TestParse(t *testing.T) {
 								AggregationLength:   32,
 								AggregationLengthV6: 128,
 								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{},
 							},
 						},
 					},
@@ -661,6 +674,7 @@ func TestParse(t *testing.T) {
 								AggregationLength:   32,
 								AggregationLengthV6: 128,
 								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{},
 							},
 						},
 					},
@@ -758,6 +772,7 @@ func TestParse(t *testing.T) {
 								AggregationLength:   26,
 								AggregationLengthV6: 128,
 								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{},
 							},
 						},
 					},
@@ -955,6 +970,7 @@ func TestParse(t *testing.T) {
 								AggregationLength:   32,
 								AggregationLengthV6: 128,
 								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{},
 							},
 						},
 					},
@@ -1192,6 +1208,7 @@ func TestParse(t *testing.T) {
 								AggregationLength:   32,
 								AggregationLengthV6: 128,
 								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{},
 							},
 						},
 					},
@@ -1353,6 +1370,7 @@ func TestParse(t *testing.T) {
 								Communities: map[uint32]bool{
 									0x04D20929: true,
 								},
+								Nodes: map[string]bool{},
 							},
 						},
 					},
@@ -1367,13 +1385,16 @@ func TestParse(t *testing.T) {
 								Communities: map[uint32]bool{
 									0x04D20929: true,
 								},
+								Nodes: map[string]bool{},
 							},
 						},
 					},
 					"legacyl2pool1": {
-						CIDR:             []*net.IPNet{ipnet("10.21.0.0/16"), ipnet("10.51.0.0/24")},
-						AvoidBuggyIPs:    true,
-						L2Advertisements: []*L2Advertisement{{}},
+						CIDR:          []*net.IPNet{ipnet("10.21.0.0/16"), ipnet("10.51.0.0/24")},
+						AvoidBuggyIPs: true,
+						L2Advertisements: []*L2Advertisement{{
+							Nodes: map[string]bool{},
+						}},
 					},
 				},
 				BFDProfiles: map[string]*BFDProfile{},
@@ -1428,6 +1449,265 @@ func TestParse(t *testing.T) {
 				},
 			},
 		},
+		{
+			desc: "use node selectors",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"10.20.0.0/16",
+							},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool2",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"30.0.0.0/16",
+							},
+						},
+					},
+				},
+				BGPAdvs: []v1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "adv1",
+						},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							IPAddressPools: []string{"pool1"},
+							NodeSelectors: []metav1.LabelSelector{
+								{
+									MatchLabels: map[string]string{
+										"second": "true",
+									},
+								},
+							},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "adv2",
+						},
+						Spec: v1beta1.BGPAdvertisementSpec{
+							IPAddressPools: []string{"pool2"},
+							NodeSelectors: []metav1.LabelSelector{
+								{
+									MatchLabels: map[string]string{
+										"first": "true",
+									},
+								},
+							},
+						},
+					},
+				},
+				L2Advs: []v1beta1.L2Advertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "l2adv1",
+						},
+						Spec: v1beta1.L2AdvertisementSpec{
+							NodeSelectors: []metav1.LabelSelector{
+								{
+									MatchLabels: map[string]string{
+										"first": "true",
+									},
+								},
+							},
+							IPAddressPools: []string{"pool1"},
+						},
+					},
+				},
+				LegacyAddressPools: []v1beta1.AddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "legacyl2pool1",
+						},
+						Spec: v1beta1.AddressPoolSpec{
+							Addresses: []string{
+								"10.21.0.0/16",
+								"10.51.0.0/24",
+							},
+							Protocol:      string(Layer2),
+							AvoidBuggyIPs: true,
+							AutoAssign:    pointer.BoolPtr(false),
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "legacybgppool1",
+						},
+						Spec: v1beta1.AddressPoolSpec{
+							Addresses: []string{
+								"10.40.0.0/16",
+								"10.60.0.0/24",
+							},
+							Protocol:      string(BGP),
+							AvoidBuggyIPs: true,
+							AutoAssign:    pointer.BoolPtr(false),
+						},
+					},
+				},
+				Nodes: []corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "first",
+							Labels: map[string]string{
+								"first": "true",
+							},
+						},
+					}, {
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "second",
+							Labels: map[string]string{
+								"second": "true",
+							},
+						},
+					},
+				},
+			},
+			want: &Config{
+				Pools: map[string]*Pool{
+					"legacybgppool1": {
+						CIDR:          []*net.IPNet{ipnet("10.40.0.0/16"), ipnet("10.60.0.0/24")},
+						AvoidBuggyIPs: true,
+						BGPAdvertisements: []*BGPAdvertisement{
+							{
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
+								Nodes: map[string]bool{
+									"first":  true,
+									"second": true,
+								},
+							},
+						},
+					},
+
+					"legacyl2pool1": {
+						CIDR:          []*net.IPNet{ipnet("10.21.0.0/16"), ipnet("10.51.0.0/24")},
+						AvoidBuggyIPs: true,
+						L2Advertisements: []*L2Advertisement{{
+							Nodes: map[string]bool{
+								"first":  true,
+								"second": true,
+							},
+						}},
+					},
+
+					"pool1": {
+						CIDR:       []*net.IPNet{ipnet("10.20.0.0/16")},
+						AutoAssign: true,
+						BGPAdvertisements: []*BGPAdvertisement{
+							{
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{"second": true},
+							},
+						},
+						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+							Nodes: map[string]bool{
+								"first": true,
+							},
+						}},
+					},
+					"pool2": {
+						CIDR:       []*net.IPNet{ipnet("30.0.0.0/16")},
+						AutoAssign: true,
+						BGPAdvertisements: []*BGPAdvertisement{
+							{
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
+								Nodes:               map[string]bool{"first": true},
+							},
+						},
+						L2Advertisements: nil,
+					},
+				},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+		{
+			desc: "no nodes means all nodes",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"10.20.0.0/16",
+							},
+						},
+					},
+				},
+				BGPAdvs: []v1beta1.BGPAdvertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "adv1",
+						},
+					},
+				},
+				L2Advs: []v1beta1.L2Advertisement{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "l2adv1",
+						},
+					},
+				},
+				Nodes: []corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "first",
+							Labels: map[string]string{
+								"first": "true",
+							},
+						},
+					}, {
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "second",
+							Labels: map[string]string{
+								"second": "true",
+							},
+						},
+					},
+				},
+			},
+			want: &Config{
+				Pools: map[string]*Pool{
+					"pool1": {
+						CIDR:       []*net.IPNet{ipnet("10.20.0.0/16")},
+						AutoAssign: true,
+						BGPAdvertisements: []*BGPAdvertisement{
+							{
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
+								Nodes: map[string]bool{
+									"first":  true,
+									"second": true,
+								},
+							},
+						},
+						L2Advertisements: []*L2Advertisement{&L2Advertisement{
+							Nodes: map[string]bool{
+								"first":  true,
+								"second": true,
+							},
+						}},
+					},
+				},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
 		/* TODO Add communities CRD
 		{
 			desc: "Duplicate communities definition",
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index 70a7c36f..2de7d123 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -274,6 +274,7 @@ func TestBGPSpeaker(t *testing.T) {
 						BGPAdvertisements: []*config.BGPAdvertisement{
 							{
 								AggregationLength: 32,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -583,10 +584,12 @@ func TestBGPSpeaker(t *testing.T) {
 								AggregationLength: 32,
 								LocalPref:         100,
 								Communities:       map[uint32]bool{1234: true, 2345: true},
+								Nodes:             map[string]bool{"pandora": true},
 							},
 							{
 								AggregationLength: 24,
 								LocalPref:         1000,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -631,6 +634,69 @@ func TestBGPSpeaker(t *testing.T) {
 			expectedCfgRet: controllers.SyncStateReprocessAll,
 			expectedLBRet:  controllers.SyncStateSuccess,
 		},
+		{
+			desc: "Multiple advertisement config, one only for my node",
+			config: &config.Config{
+				Peers: []*config.Peer{
+					{
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+					},
+				},
+				Pools: map[string]*config.Pool{
+					"default": {
+						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						BGPAdvertisements: []*config.BGPAdvertisement{
+							{
+								AggregationLength: 32,
+								LocalPref:         100,
+								Communities:       map[uint32]bool{1234: true, 2345: true},
+								Nodes:             map[string]bool{"pandora": true},
+							},
+							{
+								AggregationLength: 24,
+								LocalPref:         1000,
+								Nodes:             map[string]bool{"iris": true},
+							},
+						},
+					},
+				},
+			},
+			balancer: "test1",
+			svc: &v1.Service{
+				Spec: v1.ServiceSpec{
+					Type:                  "LoadBalancer",
+					ExternalTrafficPolicy: "Cluster",
+				},
+				Status: statusAssigned("10.20.30.1"),
+			},
+			eps: epslices.EpsOrSlices{
+				EpVal: &v1.Endpoints{
+					Subsets: []v1.EndpointSubset{
+						{
+							Addresses: []v1.EndpointAddress{
+								{
+									IP:       "2.3.4.5",
+									NodeName: pointer.StrPtr("iris"),
+								},
+							},
+						},
+					},
+				},
+				Type: epslices.Eps,
+			},
+			wantAds: map[string][]*bgp.Advertisement{
+				"1.2.3.4:0": {
+					{
+						Prefix:      ipnet("10.20.30.1/32"),
+						LocalPref:   100,
+						Communities: []uint32{1234, 2345},
+					},
+				},
+			},
+			expectedCfgRet: controllers.SyncStateReprocessAll,
+			expectedLBRet:  controllers.SyncStateSuccess,
+		},
 
 		{
 			desc: "Multiple peers",
@@ -651,6 +717,7 @@ func TestBGPSpeaker(t *testing.T) {
 						BGPAdvertisements: []*config.BGPAdvertisement{
 							{
 								AggregationLength: 32,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -865,6 +932,7 @@ func TestBGPSpeaker(t *testing.T) {
 						BGPAdvertisements: []*config.BGPAdvertisement{
 							{
 								AggregationLength: 32,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -1008,6 +1076,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						BGPAdvertisements: []*config.BGPAdvertisement{
 							{
 								AggregationLength: 32,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -1377,10 +1446,12 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 								AggregationLength: 32,
 								LocalPref:         100,
 								Communities:       map[uint32]bool{1234: true, 2345: true},
+								Nodes:             map[string]bool{"pandora": true},
 							},
 							{
 								AggregationLength: 24,
 								LocalPref:         1000,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -1448,6 +1519,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						BGPAdvertisements: []*config.BGPAdvertisement{
 							{
 								AggregationLength: 32,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -1672,6 +1744,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						BGPAdvertisements: []*config.BGPAdvertisement{
 							{
 								AggregationLength: 32,
+								Nodes:             map[string]bool{"pandora": true},
 							},
 						},
 					},
@@ -1766,6 +1839,7 @@ func TestNodeSelectors(t *testing.T) {
 			BGPAdvertisements: []*config.BGPAdvertisement{
 				{
 					AggregationLength: 32,
+					Nodes:             map[string]bool{"pandora": true},
 				},
 			},
 		},
diff --git a/speaker/layer2_controller_test.go b/speaker/layer2_controller_test.go
index 98d8cf55..64380d0a 100644
--- a/speaker/layer2_controller_test.go
+++ b/speaker/layer2_controller_test.go
@@ -7,6 +7,7 @@ import (
 	"net"
 	"os"
 	"sort"
+	"strings"
 	"testing"
 
 	"go.universe.tf/metallb/internal/config"
@@ -352,6 +353,14 @@ func TestShouldAnnounce(t *testing.T) {
 		t.Fatalf("creating controller: %s", err)
 	}
 	c2.client = &testK8S{t: t}
+	advertisementsForNode := []*config.L2Advertisement{
+		{
+			Nodes: map[string]bool{
+				"iris1": true,
+				"iris2": true,
+			},
+		},
+	}
 
 	tests := []struct {
 		desc string
@@ -370,7 +379,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -418,7 +428,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -465,7 +476,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -505,12 +517,67 @@ func TestShouldAnnounce(t *testing.T) {
 			c2ExpectedResult: map[string]string{
 				"10.20.30.1": "",
 			},
+		},
+		{
+			desc:     "One service, two endpoints across two hosts, advertisement only on node 1, controller1 should announce",
+			balancer: "test1",
+			config: &config.Config{
+				Pools: map[string]*config.Pool{
+					"default": {
+						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: []*config.L2Advertisement{
+							{
+								Nodes: map[string]bool{
+									"iris1": true,
+								},
+							},
+						},
+					},
+				},
+			},
+			svcs: []*v1.Service{
+				{
+					Spec: v1.ServiceSpec{
+						Type:                  "LoadBalancer",
+						ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal,
+					},
+					Status: statusAssigned("10.20.30.1"),
+				},
+			},
+			eps: map[string]epslices.EpsOrSlices{
+				"10.20.30.1": {
+					EpVal: &v1.Endpoints{
+						Subsets: []v1.EndpointSubset{
+							{
+								Addresses: []v1.EndpointAddress{
+									{
+										IP:       "2.3.4.5",
+										NodeName: pointer.StrPtr("iris1"),
+									},
+									{
+										IP:       "2.3.4.15",
+										NodeName: pointer.StrPtr("iris2"),
+									},
+								},
+							},
+						},
+					},
+					Type: epslices.Eps,
+				},
+			},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
 		}, {
 			desc: "One service, two endpoints across two hosts, neither endpoint is ready, no controllers should announce",
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -555,7 +622,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -602,7 +670,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -675,7 +744,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -750,7 +820,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -827,7 +898,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -876,7 +948,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -925,7 +998,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -976,7 +1050,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1027,7 +1102,8 @@ func TestShouldAnnounce(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1090,8 +1166,8 @@ func TestShouldAnnounce(t *testing.T) {
 		for _, svc := range test.svcs {
 			lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 			lbIP_s := lbIP.String()
-			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, svc, test.eps[lbIP_s])
-			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, svc, test.eps[lbIP_s])
+			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
+			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
 			if response1 != test.c1ExpectedResult[lbIP_s] {
 				t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIP_s, test.c1ExpectedResult[lbIP_s], response1)
 			}
@@ -1128,6 +1204,14 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		t.Fatalf("creating controller: %s", err)
 	}
 	c2.client = &testK8S{t: t}
+	advertisementsForNode := []*config.L2Advertisement{
+		{
+			Nodes: map[string]bool{
+				"iris1": true,
+				"iris2": true,
+			},
+		},
+	}
 
 	tests := []struct {
 		desc string
@@ -1146,7 +1230,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1203,7 +1288,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1260,7 +1346,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1317,7 +1404,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1374,7 +1462,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1431,7 +1520,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1528,7 +1618,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1625,7 +1716,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1722,7 +1814,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1794,7 +1887,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1862,7 +1956,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1930,7 +2025,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -1998,7 +2094,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			config: &config.Config{
 				Pools: map[string]*config.Pool{
 					"default": {
-						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+						L2Advertisements: advertisementsForNode,
 					},
 				},
 			},
@@ -2078,8 +2175,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		for _, svc := range test.svcs {
 			lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 			lbIP_s := lbIP.String()
-			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, svc, test.eps[lbIP_s])
-			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, svc, test.eps[lbIP_s])
+			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
+			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
 			if response1 != test.c1ExpectedResult[lbIP_s] {
 				t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIP_s, test.c1ExpectedResult[lbIP_s], response1)
 			}
@@ -2090,6 +2187,248 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 	}
 }
 
+func TestShouldAnnounceNodeSelector(t *testing.T) {
+	fakeSL := &fakeSpeakerList{
+		speakers: map[string]bool{
+			"iris1": true,
+			"iris2": true,
+		},
+	}
+	c1, err := newController(controllerConfig{
+		MyNode:  "iris1",
+		Logger:  log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+		SList:   fakeSL,
+		bgpType: bgpNative,
+	})
+	if err != nil {
+		t.Fatalf("creating controller: %s", err)
+	}
+	c1.client = &testK8S{t: t}
+
+	c2, err := newController(controllerConfig{
+		MyNode:  "iris2",
+		Logger:  log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+		SList:   fakeSL,
+		bgpType: bgpNative,
+	})
+	if err != nil {
+		t.Fatalf("creating controller: %s", err)
+	}
+	c2.client = &testK8S{t: t}
+	advertisementsForBoth := []*config.L2Advertisement{
+		{
+			Nodes: map[string]bool{
+				"iris1": true,
+				"iris2": true,
+			},
+		},
+	}
+
+	advertisementOnIris1 := []*config.L2Advertisement{
+		{
+			Nodes: map[string]bool{
+				"iris1": true,
+			},
+		},
+	}
+
+	advertisementSplit := []*config.L2Advertisement{
+		{
+			Nodes: map[string]bool{
+				"iris1": true,
+			},
+		},
+		{
+			Nodes: map[string]bool{
+				"iris2": true,
+			},
+		},
+	}
+
+	advertisementOnIris2 := []*config.L2Advertisement{
+		{
+			Nodes: map[string]bool{
+				"iris2": true,
+			},
+		},
+	}
+
+	epsOnBothNodes := map[string]epslices.EpsOrSlices{
+		"10.20.30.1": {
+			SlicesVal: []discovery.EndpointSlice{
+				{
+					Endpoints: []discovery.Endpoint{
+						{
+							Addresses: []string{
+								"2.3.4.5",
+							},
+							Topology: map[string]string{
+								"kubernetes.io/hostname": "iris1",
+							},
+							Conditions: discovery.EndpointConditions{
+								Ready: pointer.BoolPtr(true),
+							},
+						},
+						{
+							Addresses: []string{
+								"2.3.4.15",
+							},
+							Topology: map[string]string{
+								"kubernetes.io/hostname": "iris2",
+							},
+							Conditions: discovery.EndpointConditions{
+								Ready: pointer.BoolPtr(true),
+							},
+						},
+					},
+				},
+			},
+			Type: epslices.Slices,
+		},
+	}
+
+	epsOn := func(node string) map[string]epslices.EpsOrSlices {
+		return map[string]epslices.EpsOrSlices{
+			"10.20.30.1": {
+				SlicesVal: []discovery.EndpointSlice{
+					{
+						Endpoints: []discovery.Endpoint{
+							{
+								Addresses: []string{
+									"2.3.4.5",
+								},
+								Topology: map[string]string{
+									"kubernetes.io/hostname": node,
+								},
+								Conditions: discovery.EndpointConditions{
+									Ready: pointer.BoolPtr(true),
+								},
+							},
+						},
+					},
+				},
+				Type: epslices.Slices,
+			},
+		}
+	}
+
+	tests := []struct {
+		desc string
+
+		balancer         string
+		L2Advertisements []*config.L2Advertisement
+		eps              map[string]epslices.EpsOrSlices
+		trafficPolicy    v1.ServiceExternalTrafficPolicyType
+		c1ExpectedResult map[string]string
+		c2ExpectedResult map[string]string
+	}{
+		{
+			desc:             "One service, endpoint on iris1, selector on iris1, c1 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementOnIris1,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+		{
+			desc:             "One service, etp local, endpoint on iris1, selector on both(split), c2 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris2"),
+			L2Advertisements: advertisementSplit,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeLocal,
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+		{
+			desc:             "One service, endpoint on iris1, selector on both, c2 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+		},
+		{
+			desc:             "One service, endpoint on both nodes, advertisement on iris1, c1 should announce",
+			balancer:         "test1",
+			eps:              epsOnBothNodes,
+			L2Advertisements: advertisementOnIris1,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+		{
+			desc:             "One service, ltp endpoint on iris1, advertisement on iris2, no one should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementOnIris2,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeLocal,
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+	}
+	l := log.NewNopLogger()
+	for _, test := range tests {
+		if !strings.Contains(test.desc, "ltp") {
+			continue
+		}
+
+		cfg := config.Config{
+			Pools: map[string]*config.Pool{
+				"default": {
+					CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
+					L2Advertisements: test.L2Advertisements,
+				},
+			},
+		}
+		if c1.SetConfig(l, &cfg) == controllers.SyncStateError {
+			t.Errorf("%q: SetConfig failed", test.desc)
+		}
+		if c2.SetConfig(l, &cfg) == controllers.SyncStateError {
+			t.Errorf("%q: SetConfig failed", test.desc)
+		}
+		svc := v1.Service{
+			Spec: v1.ServiceSpec{
+				Type:                  "LoadBalancer",
+				ExternalTrafficPolicy: test.trafficPolicy,
+			},
+			Status: statusAssigned("10.20.30.1"),
+		}
+
+		lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
+		lbIP_s := lbIP.String()
+		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], &svc, test.eps[lbIP_s])
+		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], &svc, test.eps[lbIP_s])
+		if response1 != test.c1ExpectedResult[lbIP_s] {
+			t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIP_s, test.c1ExpectedResult[lbIP_s], response1)
+		}
+		if response2 != test.c2ExpectedResult[lbIP_s] {
+			t.Errorf("%q: shouldAnnounce for controller 2 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIP_s, test.c2ExpectedResult[lbIP_s], response2)
+		}
+	}
+}
+
 func TestClusterPolicy(t *testing.T) {
 	fakeSL := &fakeSpeakerList{
 		speakers: map[string]bool{
@@ -2123,6 +2462,14 @@ func TestClusterPolicy(t *testing.T) {
 		Pools: map[string]*config.Pool{
 			"default": {
 				CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+				L2Advertisements: []*config.L2Advertisement{
+					{
+						Nodes: map[string]bool{
+							"iris1": true,
+							"iris2": true,
+						},
+					},
+				},
 			},
 		},
 	}
@@ -2205,11 +2552,11 @@ func TestClusterPolicy(t *testing.T) {
 		}
 
 		lbIP := net.ParseIP(ip)
-		response1svc1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, svc1, eps1)
-		response2svc1 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, svc1, eps1)
+		response1svc1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], svc1, eps1)
+		response2svc1 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], svc1, eps1)
 
-		response1svc2 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, svc2, eps2)
-		response2svc2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, svc2, eps2)
+		response1svc2 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], svc2, eps2)
+		response2svc2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], svc2, eps2)
 
 		// We check that only one speaker announces the service, so their response must be different
 		if response1svc1 == response2svc1 {
diff --git a/speaker/speaker_test.go b/speaker/speaker_test.go
index 9a12f0f4..8a33dcb6 100644
--- a/speaker/speaker_test.go
+++ b/speaker/speaker_test.go
@@ -37,13 +37,11 @@ func NewController(l2Handler *MockProtocol, bgpHandler *MockProtocol, t *testing
 func TestLoadBalancerCreation(t *testing.T) {
 	var l2MockHandler = &MockProtocol{
 		protocol:       config.Layer2,
-		enabled:        true,
 		shouldAnnounce: true,
 	}
 
 	var bgpMockHandler = &MockProtocol{
 		protocol:       config.BGP,
-		enabled:        true,
 		shouldAnnounce: true,
 	}
 	c := NewController(l2MockHandler, bgpMockHandler, t)
@@ -98,8 +96,8 @@ func TestLoadBalancerCreation(t *testing.T) {
 
 	l2MockHandler.reset()
 	bgpMockHandler.reset()
+	l2MockHandler.shouldAnnounce = false
 
-	l2MockHandler.enabled = false
 	// the config changed, the l2 handler is not advertising the ip anymore, we check l2 cancels the lb
 	state = c.SetBalancer(logger,
 		"testsvc",
@@ -130,8 +128,8 @@ func TestLoadBalancerCreation(t *testing.T) {
 
 	l2MockHandler.reset()
 	bgpMockHandler.reset()
-	l2MockHandler.enabled = false
-	bgpMockHandler.enabled = false
+	l2MockHandler.shouldAnnounce = false
+	bgpMockHandler.shouldAnnounce = false
 
 	// the config changed, no handler is advertising the ip, we check bgp cancels
 	state = c.SetBalancer(logger,
@@ -164,7 +162,6 @@ func TestLoadBalancerCreation(t *testing.T) {
 type MockProtocol struct {
 	config               *config.Config
 	protocol             config.Proto
-	enabled              bool
 	shouldAnnounce       bool
 	setBalancerCalled    bool
 	deleteBalancerCalled bool
@@ -175,7 +172,7 @@ func (m *MockProtocol) SetConfig(l log.Logger, c *config.Config) error {
 	return nil
 }
 
-func (m *MockProtocol) ShouldAnnounce(_ log.Logger, _ string, _ []net.IP, _ *v1.Service, _ epslices.EpsOrSlices) string {
+func (m *MockProtocol) ShouldAnnounce(_ log.Logger, _ string, _ []net.IP, _ *config.Pool, _ *v1.Service, _ epslices.EpsOrSlices) string {
 	if m.shouldAnnounce {
 		return ""
 	}
@@ -196,10 +193,6 @@ func (m *MockProtocol) SetNode(_ log.Logger, _ *v1.Node) error {
 	panic("not implemented") // TODO: Implement
 }
 
-func (m *MockProtocol) PoolEnabledForProtocol(pool *config.Pool) bool {
-	return m.enabled
-}
-
 func (m *MockProtocol) reset() {
 	m.deleteBalancerCalled = false
 	m.setBalancerCalled = false